from __future__ import annotations

import hashlib
import json
from dataclasses import dataclass
from typing import Any, Mapping

from app.core.orchestration.spec import (
    validate_schedule_plan,
    validate_workflow_spec,
)


RUNNER_TASK_URL = "http://dataops-runner:5600/v1/tasks/execute"
ENVIRONMENTS = {"development", "test", "production"}
INPUT_TYPES = {
    "string": "STRING",
    "integer": "INT",
    "number": "FLOAT",
    "boolean": "BOOLEAN",
    "object": "JSON",
    "array": "JSON",
}
CONFLICT_BEHAVIORS = {
    "skip": "CANCEL",
    "queue": "QUEUE",
}


@dataclass(frozen=True)
class CompiledKestraFlow:
    flow_id: str
    namespace: str
    yaml: str
    definition_hash: str


def _is_scalar(value: Any) -> bool:
    return value is None or isinstance(value, (str, int, float, bool))


def _yaml_scalar(value: Any) -> str:
    if value is None:
        return "null"
    if value is True:
        return "true"
    if value is False:
        return "false"
    if isinstance(value, (int, float)) and not isinstance(value, bool):
        return str(value)
    return json.dumps(str(value), ensure_ascii=False)


def _dump_yaml(value: Any, indent: int = 0) -> list[str]:
    prefix = " " * indent
    if isinstance(value, Mapping):
        lines = []
        for key, item in value.items():
            if item == []:
                lines.append(f"{prefix}{key}: []")
            elif item == {}:
                lines.append(f"{prefix}{key}: {{}}")
            elif _is_scalar(item):
                lines.append(f"{prefix}{key}: {_yaml_scalar(item)}")
            else:
                lines.append(f"{prefix}{key}:")
                lines.extend(_dump_yaml(item, indent + 2))
        return lines
    if isinstance(value, list):
        lines = []
        for item in value:
            if item == []:
                lines.append(f"{prefix}- []")
            elif item == {}:
                lines.append(f"{prefix}- {{}}")
            elif _is_scalar(item):
                lines.append(f"{prefix}- {_yaml_scalar(item)}")
            else:
                lines.append(f"{prefix}-")
                lines.extend(_dump_yaml(item, indent + 2))
        return lines
    return [f"{prefix}{_yaml_scalar(value)}"]


def _compile_inputs(parameters: Mapping[str, Any]) -> list[dict[str, Any]]:
    if "dataops_task_tokens" in parameters:
        raise ValueError("dataops_task_tokens is a reserved workflow input")
    compiled = []
    for parameter_id in sorted(parameters):
        definition = parameters[parameter_id]
        if not isinstance(definition, dict):
            raise ValueError(f"workflow parameter {parameter_id} must be an object")
        parameter_type = definition.get("type")
        if parameter_type not in INPUT_TYPES:
            raise ValueError(
                f"workflow parameter {parameter_id} has unsupported type"
            )
        item = {
            "id": parameter_id,
            "type": INPUT_TYPES[parameter_type],
            "required": bool(definition.get("required", False)),
        }
        if "default" in definition:
            item["defaults"] = definition["default"]
        compiled.append(item)
    compiled.append(
        {
            "id": "dataops_task_tokens",
            "type": "JSON",
            "required": True,
        }
    )
    return compiled


def _compile_labels(
    spec: Mapping[str, Any], environment: str, version_no: int
) -> list[dict[str, str]]:
    labels = {
        "dataflow_uid": spec["dataflow_uid"],
        "environment": environment,
        "workflow_version": str(version_no),
    }
    for key, value in sorted(spec.get("labels", {}).items()):
        labels[f"dataops_{key}"] = (
            value
            if isinstance(value, str)
            else json.dumps(
                value, sort_keys=True, separators=(",", ":"), ensure_ascii=False
            )
        )
    return [{"key": key, "value": labels[key]} for key in sorted(labels)]


def _compile_dag_tasks(
    spec: Mapping[str, Any],
    plan: Mapping[str, Any],
    version_no: int,
) -> list[dict[str, Any]]:
    dependencies = {node["id"]: [] for node in spec["nodes"]}
    for edge in sorted(
        spec["edges"], key=lambda item: (item["from"], item["to"])
    ):
        if edge.get("condition"):
            raise ValueError(
                "conditional edges require the V52 governed Runner contract"
            )
        dependencies[edge["to"]].append(edge["from"])

    tasks = []
    retry = {
        "type": "constant",
        "interval": f"PT{plan['retry']['delay_seconds']}S",
        "maxAttempts": plan["retry"]["max_attempts"],
    }
    for node in sorted(spec["nodes"], key=lambda item: item["id"]):
        node_id = node["id"]
        token_expression = (
            f"{{{{ inputs.dataops_task_tokens.{node_id} }}}}"
            if node_id.replace("_", "").isalnum()
            else f"{{{{ inputs.dataops_task_tokens['{node_id}'] }}}}"
        )
        runner_payload = {
            "dataflow_uid": spec["dataflow_uid"],
            "kestra_execution_id": "{{ execution.id }}",
            "node": node,
            "parameters": {
                key: f"{{{{ inputs.{key} }}}}"
                for key in sorted(spec["parameters"])
            },
            "task_token": token_expression,
            "workflow_version": version_no,
        }
        task = {
            "id": node["id"],
            "type": "io.kestra.plugin.core.http.Request",
            "uri": RUNNER_TASK_URL,
            "method": "POST",
            "contentType": "application/json",
            "timeout": f"PT{plan['timeout_seconds']}S",
            "retry": retry,
            "body": json.dumps(
                runner_payload,
                sort_keys=True,
                separators=(",", ":"),
                ensure_ascii=False,
            ),
        }
        dag_task: dict[str, Any] = {"task": task}
        if dependencies[node["id"]]:
            dag_task["dependsOn"] = sorted(dependencies[node["id"]])
        tasks.append(dag_task)
    return tasks


def _compile_triggers(plan: Mapping[str, Any]) -> list[dict[str, Any]]:
    triggers = []
    cron_index = 0
    for trigger in plan["triggers"]:
        trigger_type = trigger["type"]
        if trigger_type == "manual":
            continue
        if trigger_type != "cron":
            raise ValueError(
                f"{trigger_type} trigger has no equivalent in the V51 compiler"
            )
        cron_index += 1
        triggers.append(
            {
                "id": f"schedule_{cron_index}",
                "type": "io.kestra.plugin.core.trigger.Schedule",
                "cron": trigger["expression"],
                "timezone": plan["timezone"],
                "disabled": True,
            }
        )
    return triggers


def compile_kestra_flow(
    workflow_spec: Mapping[str, Any],
    schedule_plan: Mapping[str, Any],
    environment: str,
    version_no: int,
) -> CompiledKestraFlow:
    if environment not in ENVIRONMENTS:
        raise ValueError("unsupported environment")
    if isinstance(version_no, bool) or not isinstance(version_no, int) or version_no < 1:
        raise ValueError("version_no must be a positive integer")

    spec = validate_workflow_spec(workflow_spec)
    plan = validate_schedule_plan(schedule_plan)
    conflict_policy = plan["conflict_policy"]
    if conflict_policy not in CONFLICT_BEHAVIORS:
        raise ValueError(
            f"{conflict_policy} has no equivalent Kestra concurrency behavior"
        )

    flow_id = f"df_{spec['dataflow_uid'].replace('-', '')}_v{version_no}"
    namespace = f"dataops.{environment}"
    flow: dict[str, Any] = {
        "id": flow_id,
        "namespace": namespace,
    }
    if spec.get("description"):
        flow["description"] = spec["description"]
    flow.update(
        {
            "disabled": True,
            "labels": _compile_labels(spec, environment, version_no),
            "inputs": _compile_inputs(spec["parameters"]),
            "concurrency": {
                "limit": plan["max_concurrency"],
                "behavior": CONFLICT_BEHAVIORS[conflict_policy],
            },
            "tasks": [
                {
                    "id": "dataops_dag",
                    "type": "io.kestra.plugin.core.flow.Dag",
                    "concurrent": plan["max_concurrency"],
                    "tasks": _compile_dag_tasks(spec, plan, version_no),
                }
            ],
        }
    )
    triggers = _compile_triggers(plan)
    if triggers:
        flow["triggers"] = triggers

    source = "\n".join(_dump_yaml(flow)) + "\n"
    return CompiledKestraFlow(
        flow_id=flow_id,
        namespace=namespace,
        yaml=source,
        definition_hash=hashlib.sha256(source.encode("utf-8")).hexdigest(),
    )
