from __future__ import annotations

from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
from enum import StrEnum


class MigrationBatch(StrEnum):
    READ_ONLY_LOW_FREQUENCY = "read_only_low_frequency"
    IDEMPOTENT_PARTITION_WRITE = "idempotent_partition_write"
    CORE_MULTI_NODE = "core_multi_node"
    SPECIAL_HIGH_RISK = "special_high_risk"


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


@dataclass(frozen=True)
class ObservationWindow:
    started_at: datetime
    required_duration: timedelta
    unexplained_differences: int
    duplicate_writes: int
    critical_sla_breaches: int
    rollback_requests: int

    def __post_init__(self):
        if self.started_at.tzinfo is None:
            raise ValueError("started_at must be timezone-aware")
        if self.required_duration <= timedelta(0):
            raise ValueError("required_duration must be positive")
        for name in (
            "unexplained_differences",
            "duplicate_writes",
            "critical_sla_breaches",
            "rollback_requests",
        ):
            value = getattr(self, name)
            if isinstance(value, bool) or not isinstance(value, int) or value < 0:
                raise ValueError(f"{name} must be a non-negative integer")


@dataclass(frozen=True)
class RolloutExitGates:
    observation_window_complete: bool
    no_unexplained_differences: bool
    no_duplicate_writes: bool
    sla_passed: bool
    pause_retry_rollback_exercised: bool
    n8n_standby: bool
    n8n_unauthorized_changes: int


class RolloutPolicy:
    @staticmethod
    def _raise_failed(label: str, checks: dict[str, bool]) -> None:
        failed = [name for name, passed in checks.items() if passed is not True]
        if failed:
            raise ValueError(f"{label} failed: {', '.join(failed)}")

    @classmethod
    def assert_entry(cls, gates: RolloutEntryGates) -> None:
        if not isinstance(gates, RolloutEntryGates):
            raise ValueError("rollout entry gates are required")
        runs_valid = (
            isinstance(gates.shadow_runs, int)
            and not isinstance(gates.shadow_runs, bool)
            and isinstance(gates.required_shadow_runs, int)
            and not isinstance(gates.required_shadow_runs, bool)
            and gates.required_shadow_runs > 0
            and gates.shadow_runs >= gates.required_shadow_runs
        )
        cls._raise_failed(
            "rollout entry gates",
            {
                "inventory_complete": gates.inventory_complete,
                "workflow_spec_valid": gates.workflow_spec_valid,
                "policy_complete": gates.policy_complete,
                "rollback_target_present": gates.rollback_target_present,
                "shadow_runs": runs_valid,
                "failure_recovery_observed": gates.failure_recovery_observed,
                "reconciliation_passed": gates.reconciliation_passed,
                "connection_budget_passed": gates.connection_budget_passed,
                "peak_concurrency_passed": gates.peak_concurrency_passed,
                "sla_passed": gates.sla_passed,
            },
        )

    @classmethod
    def assert_observation(
        cls, window: ObservationWindow, *, now: datetime | None = None
    ) -> None:
        if not isinstance(window, ObservationWindow):
            raise ValueError("observation window is required")
        current = now or datetime.now(UTC)
        if current.tzinfo is None:
            raise ValueError("now must be timezone-aware")
        cls._raise_failed(
            "rollout observation",
            {
                "observation_window_complete": (
                    current >= window.started_at + window.required_duration
                ),
                "unexplained_differences": (
                    window.unexplained_differences == 0
                ),
                "duplicate_writes": window.duplicate_writes == 0,
                "critical_sla_breaches": (
                    window.critical_sla_breaches == 0
                ),
                "rollback_requests": window.rollback_requests == 0,
            },
        )

    @classmethod
    def assert_exit(cls, gates: RolloutExitGates) -> None:
        if not isinstance(gates, RolloutExitGates):
            raise ValueError("rollout exit gates are required")
        cls._raise_failed(
            "rollout exit gates",
            {
                "observation_window_complete": (
                    gates.observation_window_complete
                ),
                "no_unexplained_differences": (
                    gates.no_unexplained_differences
                ),
                "no_duplicate_writes": gates.no_duplicate_writes,
                "sla_passed": gates.sla_passed,
                "pause_retry_rollback_exercised": (
                    gates.pause_retry_rollback_exercised
                ),
                "n8n_standby": gates.n8n_standby,
                "n8n_unauthorized_changes": (
                    isinstance(gates.n8n_unauthorized_changes, int)
                    and not isinstance(gates.n8n_unauthorized_changes, bool)
                    and gates.n8n_unauthorized_changes == 0
                ),
            },
        )
