from __future__ import annotations

import json
from typing import Any

from sqlalchemy import text

from app.core.common.identifiers import ensure_governance_uid, new_governance_uid
from app.core.events.outbox import enqueue_outbox
from app.core.orchestration.migration.dual_run import DualRunMode

ENVIRONMENTS = {"development", "test", "production"}


def _json(value: Any) -> str:
    return json.dumps(
        value,
        sort_keys=True,
        separators=(",", ":"),
        ensure_ascii=False,
    )


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 _uid(value: Any, label: str) -> str:
    try:
        return ensure_governance_uid({"uid": str(value)})
    except ValueError as exc:
        raise ValueError(f"{label} must be a valid UUIDv7") from exc


def _state_row(row) -> dict[str, Any]:
    return {
        "state_id": row[0],
        "dataflow_uid": row[1],
        "environment": row[2],
        "mode": row[3],
        "status": row[4],
        "n8n_definition_id": row[5],
        "kestra_definition_id": row[6],
        "migration_batch": row[7],
        "observation_until": row[8],
        "metadata": dict(row[9] or {}),
    }


def _operation_row(row, state) -> dict[str, Any]:
    return {
        "operation_id": row[0],
        "dataflow_uid": state["dataflow_uid"],
        "environment": state["environment"],
        "source_mode": row[1],
        "target_mode": row[2],
        "n8n_definition_id": state["n8n_definition_id"],
        "kestra_definition_id": state["kestra_definition_id"],
        "gates": dict(row[3] or {}),
        "idempotency_key": row[4],
        "correlation_id": row[5],
        "status": row[6],
        "result": dict(row[7] or {}),
    }


class PostgresMigrationStore:
    def __init__(self, engine):
        self.engine = engine

    def create_state(self, record):
        dataflow_uid = _uid(record.get("dataflow_uid"), "dataflow_uid")
        environment = _required_string(
            record.get("environment"), "environment", 20
        )
        if environment not in ENVIRONMENTS:
            raise ValueError("unsupported environment")
        mode = DualRunMode(record.get("mode")).value
        n8n_id = record.get("n8n_definition_id")
        kestra_id = record.get("kestra_definition_id")
        if mode != DualRunMode.KESTRA_PRIMARY.value:
            n8n_id = _required_string(n8n_id, "n8n_definition_id", 255)
        if mode != DualRunMode.N8N_PRIMARY.value:
            kestra_id = _required_string(kestra_id, "kestra_definition_id", 255)
        migration_batch = record.get("migration_batch")
        if migration_batch is not None:
            migration_batch = _required_string(
                migration_batch, "migration_batch", 50
            )
        metadata = record.get("metadata") or {}
        if not isinstance(metadata, dict):
            raise ValueError("metadata must be an object")

        state_id = new_governance_uid()
        with self.engine.begin() as connection:
            connection.execute(
                text("SELECT pg_advisory_xact_lock(hashtext(:key))"),
                {"key": f"{dataflow_uid}:{environment}:migration"},
            )
            connection.execute(
                text(
                    "INSERT INTO public.workflow_migration_states "
                    "(id, dataflow_uid, environment, mode, status, "
                    "n8n_definition_id, kestra_definition_id, "
                    "migration_batch, metadata) "
                    "VALUES (CAST(:id AS uuid), CAST(:dataflow_uid AS uuid), "
                    ":environment, :mode, 'stable', :n8n_id, :kestra_id, "
                    ":migration_batch, CAST(:metadata AS jsonb))"
                ),
                {
                    "id": state_id,
                    "dataflow_uid": dataflow_uid,
                    "environment": environment,
                    "mode": mode,
                    "n8n_id": n8n_id,
                    "kestra_id": kestra_id,
                    "migration_batch": migration_batch,
                    "metadata": _json(metadata),
                },
            )
        return self.get_state(dataflow_uid, environment)

    def get_state(self, dataflow_uid, environment):
        dataflow_uid = _uid(dataflow_uid, "dataflow_uid")
        environment = _required_string(environment, "environment", 20)
        with self.engine.connect() as connection:
            row = connection.execute(
                text(
                    "SELECT id::text, dataflow_uid::text, environment, mode, "
                    "status, n8n_definition_id, kestra_definition_id, "
                    "migration_batch, observation_until, metadata "
                    "FROM public.workflow_migration_states "
                    "WHERE dataflow_uid = CAST(:dataflow_uid AS uuid) "
                    "AND environment = :environment"
                ),
                {
                    "dataflow_uid": dataflow_uid,
                    "environment": environment,
                },
            ).one_or_none()
        return _state_row(row) if row else None

    @staticmethod
    def _load_state(connection, dataflow_uid, environment):
        row = connection.execute(
            text(
                "SELECT id::text, dataflow_uid::text, environment, mode, "
                "status, n8n_definition_id, kestra_definition_id, "
                "migration_batch, observation_until, metadata "
                "FROM public.workflow_migration_states "
                "WHERE dataflow_uid = CAST(:dataflow_uid AS uuid) "
                "AND environment = :environment FOR UPDATE"
            ),
            {
                "dataflow_uid": dataflow_uid,
                "environment": environment,
            },
        ).one_or_none()
        if row is None:
            raise ValueError("workflow migration state was not found")
        return _state_row(row)

    @staticmethod
    def _find_operation(connection, state, idempotency_key):
        row = connection.execute(
            text(
                "SELECT id::text, source_mode, target_mode, gates, "
                "idempotency_key, correlation_id::text, status, result "
                "FROM public.workflow_cutover_operations "
                "WHERE migration_state_id = CAST(:state_id AS uuid) "
                "AND idempotency_key = :idempotency_key"
            ),
            {
                "state_id": state["state_id"],
                "idempotency_key": idempotency_key,
            },
        ).one_or_none()
        return _operation_row(row, state) if row else None

    def request_transition(self, record):
        dataflow_uid = _uid(record.get("dataflow_uid"), "dataflow_uid")
        environment = _required_string(
            record.get("environment"), "environment", 20
        )
        source = DualRunMode(record.get("source_mode")).value
        target = DualRunMode(record.get("target_mode")).value
        idempotency_key = _required_string(
            record.get("idempotency_key"), "idempotency_key", 500
        )
        correlation_id = _uid(record.get("correlation_id"), "correlation_id")
        gates = record.get("gates") or {}
        if not isinstance(gates, dict):
            raise ValueError("gates must be an object")

        with self.engine.begin() as connection:
            connection.execute(
                text("SELECT pg_advisory_xact_lock(hashtext(:key))"),
                {"key": f"{dataflow_uid}:{environment}:migration"},
            )
            state = self._load_state(connection, dataflow_uid, environment)
            previous = self._find_operation(
                connection, state, idempotency_key
            )
            if previous:
                return False, previous
            if state["mode"] != source or state["status"] != "stable":
                raise ValueError("workflow migration state changed")

            operation_id = new_governance_uid()
            connection.execute(
                text(
                    "INSERT INTO public.workflow_cutover_operations "
                    "(id, migration_state_id, idempotency_key, source_mode, "
                    "target_mode, status, gates, correlation_id) "
                    "VALUES (CAST(:id AS uuid), CAST(:state_id AS uuid), "
                    ":idempotency_key, :source_mode, :target_mode, 'claimed', "
                    "CAST(:gates AS jsonb), CAST(:correlation_id AS uuid))"
                ),
                {
                    "id": operation_id,
                    "state_id": state["state_id"],
                    "idempotency_key": idempotency_key,
                    "source_mode": source,
                    "target_mode": target,
                    "gates": _json(gates),
                    "correlation_id": correlation_id,
                },
            )
            connection.execute(
                text(
                    "UPDATE public.workflow_migration_states "
                    "SET status = 'transition_pending', "
                    "updated_at = CURRENT_TIMESTAMP "
                    "WHERE id = CAST(:state_id AS uuid)"
                ),
                {"state_id": state["state_id"]},
            )
            event_payload = {
                "operation_id": operation_id,
                "dataflow_uid": dataflow_uid,
                "environment": environment,
                "source_mode": source,
                "target_mode": target,
                "n8n_definition_id": state["n8n_definition_id"],
                "kestra_definition_id": state["kestra_definition_id"],
                "idempotency_key": idempotency_key,
                "correlation_id": correlation_id,
            }
            enqueue_outbox(
                connection,
                aggregate_type="workflow_cutover",
                aggregate_id=operation_id,
                event_type="workflow.engine.cutover.requested",
                payload=event_payload,
                correlation_id=correlation_id,
            )
            operation = {
                **event_payload,
                "gates": gates,
                "status": "claimed",
            }
        return True, operation

    def _finish_transition(
        self, operation_id, mode, status, result, *, expected_mode
    ):
        operation_id = _uid(operation_id, "operation_id")
        mode = DualRunMode(mode).value
        if not isinstance(result, dict):
            raise ValueError("transition result must be an object")
        with self.engine.begin() as connection:
            row = connection.execute(
                text(
                    "SELECT migration_state_id::text "
                    "FROM public.workflow_cutover_operations "
                    "WHERE id = CAST(:id AS uuid) AND status = 'claimed' "
                    "FOR UPDATE"
                ),
                {"id": operation_id},
            ).one_or_none()
            if row is None:
                raise ValueError("cutover operation is not in claimed state")
            state_id = row[0]
            definitions = connection.execute(
                text(
                    "SELECT n8n_definition_id, kestra_definition_id "
                    "FROM public.workflow_migration_states "
                    "WHERE id = CAST(:state_id AS uuid)"
                ),
                {"state_id": state_id},
            ).one()
            connection.execute(
                text(
                    "UPDATE public.workflow_cutover_operations "
                    "SET status = :status, result = CAST(:result AS jsonb), "
                    "updated_at = CURRENT_TIMESTAMP "
                    "WHERE id = CAST(:id AS uuid)"
                ),
                {
                    "id": operation_id,
                    "status": status,
                    "result": _json(result),
                },
            )
            updated = connection.execute(
                text(
                    "UPDATE public.workflow_migration_states "
                    "SET mode = :mode, status = 'stable', "
                    "updated_at = CURRENT_TIMESTAMP "
                    "WHERE id = CAST(:state_id AS uuid) "
                    "AND mode = :expected_mode "
                    "AND status = 'transition_pending'"
                ),
                {
                    "state_id": state_id,
                    "mode": mode,
                    "expected_mode": expected_mode,
                },
            )
            if updated.rowcount != 1:
                raise ValueError("workflow migration state changed")
            self._sync_engine_bindings(
                connection,
                mode,
                n8n_definition_id=definitions[0],
                kestra_definition_id=definitions[1],
            )

    @staticmethod
    def _sync_engine_bindings(
        connection,
        mode,
        *,
        n8n_definition_id,
        kestra_definition_id,
    ):
        roles = {
            DualRunMode.N8N_PRIMARY.value: {
                "n8n": ("primary", "enabled"),
                "kestra": ("standby", "disabled"),
            },
            DualRunMode.N8N_PRIMARY_KESTRA_SHADOW.value: {
                "n8n": ("primary", "enabled"),
                "kestra": ("shadow", "disabled"),
            },
            DualRunMode.KESTRA_PRIMARY_N8N_STANDBY.value: {
                "n8n": ("standby", "disabled"),
                "kestra": ("primary", "enabled"),
            },
            DualRunMode.KESTRA_PRIMARY.value: {
                "n8n": ("archived", "disabled"),
                "kestra": ("primary", "enabled"),
            },
        }[mode]
        definitions = {
            "n8n": n8n_definition_id,
            "kestra": kestra_definition_id,
        }
        engine_order = (
            ("kestra", "n8n")
            if mode
            in {
                DualRunMode.N8N_PRIMARY.value,
                DualRunMode.N8N_PRIMARY_KESTRA_SHADOW.value,
            }
            else ("n8n", "kestra")
        )
        for engine_type in engine_order:
            definition_id = definitions[engine_type]
            if definition_id is None:
                continue
            role, binding_status = roles[engine_type]
            connection.execute(
                text(
                    "UPDATE public.workflow_engine_bindings "
                    "SET role = :role, status = :status, "
                    "updated_at = CURRENT_TIMESTAMP "
                    "WHERE engine_type = :engine_type "
                    "AND engine_definition_id = :definition_id"
                ),
                {
                    "role": role,
                    "status": binding_status,
                    "engine_type": engine_type,
                    "definition_id": definition_id,
                },
            )

    def complete_transition(self, operation_id, target_mode, result):
        with self.engine.connect() as connection:
            source = connection.execute(
                text(
                    "SELECT source_mode "
                    "FROM public.workflow_cutover_operations "
                    "WHERE id = CAST(:id AS uuid)"
                ),
                {"id": _uid(operation_id, "operation_id")},
            ).scalar_one_or_none()
        if source is None:
            raise ValueError("cutover operation was not found")
        self._finish_transition(
            operation_id,
            target_mode,
            "succeeded",
            result,
            expected_mode=source,
        )

    def rollback_transition(self, operation_id, source_mode, result):
        self._finish_transition(
            operation_id,
            source_mode,
            "rolled_back",
            result,
            expected_mode=DualRunMode(source_mode).value,
        )

    def record_dual_run(self, record):
        state_id = record.get("state_id")
        if state_id is None:
            state = self.get_state(
                record.get("dataflow_uid"), record.get("environment")
            )
            if state is None:
                raise ValueError("workflow migration state was not found")
            state_id = state["state_id"]
        state_id = _uid(state_id, "state_id")
        isolation = record.get("isolation") or {}
        isolation_mode = (
            isolation.get("strategy")
            if isinstance(isolation, dict)
            else record.get("isolation_mode")
        )
        dual_run_id = new_governance_uid()
        with self.engine.begin() as connection:
            connection.execute(
                text(
                    "INSERT INTO public.workflow_dual_runs "
                    "(id, migration_state_id, input_hash, n8n_execution_id, "
                    "kestra_execution_id, n8n_status, kestra_status, "
                    "isolation_mode, formal_engine_changed, correlation_id, "
                    "safe_metrics) VALUES (CAST(:id AS uuid), "
                    "CAST(:state_id AS uuid), :input_hash, :n8n_execution_id, "
                    ":kestra_execution_id, :n8n_status, :kestra_status, "
                    ":isolation_mode, :formal_engine_changed, "
                    "CAST(:correlation_id AS uuid), CAST(:safe_metrics AS jsonb))"
                ),
                {
                    "id": dual_run_id,
                    "state_id": state_id,
                    "input_hash": _required_string(
                        record.get("input_hash")
                        or record.get("input_snapshot_hash"),
                        "input_hash",
                        64,
                    ),
                    "n8n_execution_id": record.get("n8n_execution_id")
                    or record.get("primary_execution_id"),
                    "kestra_execution_id": record.get("kestra_execution_id")
                    or record.get("shadow_execution_id"),
                    "n8n_status": _required_string(
                        record.get("n8n_status")
                        or record.get("primary_status"),
                        "n8n_status",
                        30,
                    ),
                    "kestra_status": _required_string(
                        record.get("kestra_status")
                        or record.get("shadow_status"),
                        "kestra_status",
                        30,
                    ),
                    "isolation_mode": isolation_mode,
                    "formal_engine_changed": bool(
                        record.get("formal_engine_changed", False)
                    ),
                    "correlation_id": _uid(
                        record.get("correlation_id"), "correlation_id"
                    ),
                    "safe_metrics": _json(record.get("safe_metrics") or {}),
                },
            )
        return dual_run_id

    def record_reconciliation(self, record):
        state_id = _uid(record.get("state_id"), "state_id")
        report_id = new_governance_uid()
        with self.engine.begin() as connection:
            connection.execute(
                text(
                    "INSERT INTO public.workflow_reconciliation_reports "
                    "(id, migration_state_id, dual_run_id, status, policy, "
                    "safe_metrics, differences, correlation_id) VALUES ("
                    "CAST(:id AS uuid), CAST(:state_id AS uuid), "
                    "CAST(:dual_run_id AS uuid), :status, "
                    "CAST(:policy AS jsonb), CAST(:safe_metrics AS jsonb), "
                    "CAST(:differences AS jsonb), "
                    "CAST(:correlation_id AS uuid))"
                ),
                {
                    "id": report_id,
                    "state_id": state_id,
                    "dual_run_id": record.get("dual_run_id"),
                    "status": record.get("status"),
                    "policy": _json(record.get("policy") or {}),
                    "safe_metrics": _json(record.get("safe_metrics") or {}),
                    "differences": _json(record.get("differences") or []),
                    "correlation_id": _uid(
                        record.get("correlation_id"), "correlation_id"
                    ),
                },
            )
        return report_id

    def delete_test_state(self, state_id):
        state_id = _uid(state_id, "state_id")
        with self.engine.begin() as connection:
            operation_ids = [
                row[0]
                for row in connection.execute(
                    text(
                        "SELECT id::text "
                        "FROM public.workflow_cutover_operations "
                        "WHERE migration_state_id = CAST(:state_id AS uuid)"
                    ),
                    {"state_id": state_id},
                )
            ]
            if operation_ids:
                connection.execute(
                    text(
                        "DELETE FROM public.outbox_events "
                        "WHERE aggregate_type = 'workflow_cutover' "
                        "AND aggregate_id = ANY(:operation_ids)"
                    ),
                    {"operation_ids": operation_ids},
                )
            connection.execute(
                text(
                    "DELETE FROM public.workflow_migration_states "
                    "WHERE id = CAST(:state_id AS uuid)"
                ),
                {"state_id": state_id},
            )
