from __future__ import annotations

from dataclasses import asdict, dataclass
from typing import Any

from app.core.orchestration.migration.dual_run import (
    DualRunMode,
    validate_mode_transition,
)


def _required_string(value: Any, label: str, maximum: int = 500) -> str:
    if not isinstance(value, str) or not value.strip():
        raise ValueError(f"{label} is required")
    normalized = value.strip()
    if len(normalized) > maximum:
        raise ValueError(f"{label} exceeds {maximum} characters")
    return normalized


def _safe_engine_result(value: Any) -> dict[str, Any]:
    if not isinstance(value, dict):
        return {"status": "completed"}
    result = {}
    for key in ("status", "execution_id", "revision"):
        item = value.get(key)
        if isinstance(item, (str, int, float, bool)) or item is None:
            result[key] = item
    return result or {"status": "completed"}


@dataclass(frozen=True)
class CutoverGates:
    inventory_complete: bool
    workflow_spec_valid: bool
    rollback_target_present: bool
    shadow_runs: int
    required_shadow_runs: int
    failure_recovery_observed: bool
    reconciliation_passed: bool
    connection_budget_passed: bool
    sla_passed: bool
    n8n_baseline_unchanged: bool

    def __post_init__(self):
        if isinstance(self.shadow_runs, bool) or not isinstance(
            self.shadow_runs, int
        ):
            raise ValueError("shadow_runs must be an integer")
        if isinstance(self.required_shadow_runs, bool) or not isinstance(
            self.required_shadow_runs, int
        ):
            raise ValueError("required_shadow_runs must be an integer")
        if self.shadow_runs < 0 or self.required_shadow_runs < 1:
            raise ValueError("shadow run thresholds are invalid")

    def as_dict(self) -> dict[str, Any]:
        return asdict(self)

    def failed(self) -> list[str]:
        checks = {
            "inventory_complete": self.inventory_complete,
            "workflow_spec_valid": self.workflow_spec_valid,
            "rollback_target_present": self.rollback_target_present,
            "shadow_runs": self.shadow_runs >= self.required_shadow_runs,
            "failure_recovery_observed": self.failure_recovery_observed,
            "reconciliation_passed": self.reconciliation_passed,
            "connection_budget_passed": self.connection_budget_passed,
            "sla_passed": self.sla_passed,
            "n8n_baseline_unchanged": self.n8n_baseline_unchanged,
        }
        return [name for name, passed in checks.items() if not passed]


class EngineRoleController:
    def __init__(
        self,
        *,
        n8n,
        kestra,
        n8n_namespace="dataops",
        kestra_namespace="dataops",
    ):
        self.n8n = n8n
        self.kestra = kestra
        self.n8n_namespace = _required_string(
            n8n_namespace, "n8n_namespace", 255
        )
        self.kestra_namespace = _required_string(
            kestra_namespace, "kestra_namespace", 255
        )

    def deactivate_n8n(self, definition_id):
        return self.n8n.deactivate(self.n8n_namespace, definition_id)

    def activate_n8n(self, definition_id):
        return self.n8n.activate(self.n8n_namespace, definition_id)

    def deactivate_kestra(self, definition_id):
        return self.kestra.deactivate(
            self.kestra_namespace, definition_id
        )

    def activate_kestra(self, definition_id):
        return self.kestra.activate(self.kestra_namespace, definition_id)


class WorkflowCutoverService:
    def __init__(self, *, store, engines):
        self.store = store
        self.engines = engines

    def request_cutover(
        self,
        *,
        dataflow_uid,
        environment,
        target_mode,
        gates,
        idempotency_key,
        correlation_id,
        retirement_decision=None,
    ):
        dataflow_uid = _required_string(dataflow_uid, "dataflow_uid", 255)
        environment = _required_string(environment, "environment", 100)
        idempotency_key = _required_string(
            idempotency_key, "idempotency_key", 255
        )
        correlation_id = _required_string(correlation_id, "correlation_id", 255)
        if not isinstance(gates, CutoverGates):
            raise ValueError("cutover gates are required")

        state = self.store.get_state(dataflow_uid, environment)
        if not state:
            raise ValueError("workflow migration state was not found")
        source_mode = DualRunMode(state["mode"])
        destination = DualRunMode(target_mode)
        validate_mode_transition(source_mode, destination)

        failed = gates.failed()
        if failed:
            raise ValueError(f"cutover gates failed: {', '.join(failed)}")
        gate_evidence = gates.as_dict()
        if destination is DualRunMode.KESTRA_PRIMARY:
            valid_retirement = (
                isinstance(retirement_decision, dict)
                and retirement_decision.get("status") == "approved"
                and retirement_decision.get("retirement_approved") is True
                and retirement_decision.get("missing") == []
            )
            if not valid_retirement:
                raise ValueError("independent retirement approval is required")
            gate_evidence = {
                "cutover": gate_evidence,
                "retirement": {
                    "status": "approved",
                    "retirement_approved": True,
                    "missing": [],
                },
            }

        record = {
            "dataflow_uid": dataflow_uid,
            "environment": environment,
            "source_mode": source_mode.value,
            "target_mode": destination.value,
            "n8n_definition_id": state.get("n8n_definition_id"),
            "kestra_definition_id": state.get("kestra_definition_id"),
            "gates": gate_evidence,
            "idempotency_key": idempotency_key,
            "correlation_id": correlation_id,
        }
        _, operation = self.store.request_transition(record)
        return operation

    def apply_requested_cutover(self, operation):
        if not isinstance(operation, dict):
            raise ValueError("cutover operation must be an object")
        operation_id = _required_string(
            operation.get("operation_id"), "operation_id", 255
        )
        source = DualRunMode(operation.get("source_mode"))
        target = DualRunMode(operation.get("target_mode"))
        validate_mode_transition(source, target)

        primary_cutover = (
            source is DualRunMode.N8N_PRIMARY_KESTRA_SHADOW
            and target is DualRunMode.KESTRA_PRIMARY_N8N_STANDBY
        )
        final_retirement = (
            source is DualRunMode.KESTRA_PRIMARY_N8N_STANDBY
            and target is DualRunMode.KESTRA_PRIMARY
        )
        if not primary_cutover and not final_retirement:
            raise ValueError("operation is not an executable Kestra cutover")

        n8n_id = _required_string(
            operation.get("n8n_definition_id"), "n8n_definition_id", 255
        )
        kestra_id = _required_string(
            operation.get("kestra_definition_id"), "kestra_definition_id", 255
        )
        try:
            n8n_result = self.engines.deactivate_n8n(n8n_id)
            kestra_result = self.engines.activate_kestra(kestra_id)
        except Exception:
            if final_retirement:
                result = {
                    "status": "rolled_back",
                    "mode": source.value,
                    "reason_code": "retirement_finalization_failed",
                }
                self.store.rollback_transition(
                    operation_id, source.value, result
                )
                return result
            restore_result = self.engines.activate_n8n(n8n_id)
            result = {
                "status": "rolled_back",
                "mode": source.value,
                "n8n_restore": _safe_engine_result(restore_result),
                "reason_code": "target_activation_failed",
            }
            self.store.rollback_transition(operation_id, source.value, result)
            return result

        result = {
            "status": "succeeded",
            "mode": target.value,
            "n8n": _safe_engine_result(n8n_result),
            "kestra": _safe_engine_result(kestra_result),
        }
        self.store.complete_transition(operation_id, target.value, result)
        return result

    def rollback_to_n8n(
        self,
        *,
        dataflow_uid,
        environment,
        idempotency_key,
        correlation_id,
    ):
        dataflow_uid = _required_string(dataflow_uid, "dataflow_uid", 255)
        environment = _required_string(environment, "environment", 100)
        state = self.store.get_state(dataflow_uid, environment)
        if not state:
            raise ValueError("workflow migration state was not found")

        source = DualRunMode(state["mode"])
        target = DualRunMode.N8N_PRIMARY_KESTRA_SHADOW
        validate_mode_transition(source, target)
        if source is not DualRunMode.KESTRA_PRIMARY_N8N_STANDBY:
            raise ValueError("workflow is not in Kestra primary standby mode")

        record = {
            "dataflow_uid": dataflow_uid,
            "environment": environment,
            "source_mode": source.value,
            "target_mode": target.value,
            "n8n_definition_id": state.get("n8n_definition_id"),
            "kestra_definition_id": state.get("kestra_definition_id"),
            "gates": {},
            "idempotency_key": _required_string(
                idempotency_key, "idempotency_key", 255
            ),
            "correlation_id": _required_string(
                correlation_id, "correlation_id", 255
            ),
        }
        claimed, operation = self.store.request_transition(record)
        if not claimed and operation.get("status") in {"succeeded", "rolled_back"}:
            return {
                "status": operation["status"],
                "mode": operation.get("target_mode", target.value),
            }

        operation_id = _required_string(
            operation.get("operation_id"), "operation_id", 255
        )
        kestra_result = self.engines.deactivate_kestra(
            _required_string(
                operation.get("kestra_definition_id"),
                "kestra_definition_id",
                255,
            )
        )
        n8n_result = self.engines.activate_n8n(
            _required_string(
                operation.get("n8n_definition_id"), "n8n_definition_id", 255
            )
        )
        result = {
            "status": "succeeded",
            "mode": target.value,
            "kestra": _safe_engine_result(kestra_result),
            "n8n": _safe_engine_result(n8n_result),
        }
        self.store.complete_transition(operation_id, target.value, result)
        return result
