"""
Promotion models for releaseops.

Tracks the state machine for promoting bundles through environments:
DRAFT → CANDIDATE → STAGED → PROD → (ROLLED_BACK)
"""

from __future__ import annotations

from dataclasses import dataclass, field
from enum import Enum
from typing import Dict, List, Optional


class PromotionState(str, Enum):
    """States in the promotion lifecycle."""

    DRAFT = "DRAFT"
    CANDIDATE = "CANDIDATE"
    STAGED = "STAGED"
    PROD = "PROD"
    ROLLED_BACK = "ROLLED_BACK"

    @property
    def next_state(self) -> Optional[PromotionState]:
        """Get the next state in the promotion chain."""
        transitions = {
            PromotionState.DRAFT: PromotionState.CANDIDATE,
            PromotionState.CANDIDATE: PromotionState.STAGED,
            PromotionState.STAGED: PromotionState.PROD,
        }
        return transitions.get(self)

    @property
    def is_terminal(self) -> bool:
        """Whether this is a terminal state (no automatic next step)."""
        return self in (PromotionState.PROD, PromotionState.ROLLED_BACK)


# Maps environments to their corresponding promotion states
ENV_TO_STATE: Dict[str, PromotionState] = {
    "dev": PromotionState.DRAFT,
    "candidate": PromotionState.CANDIDATE,
    "staging": PromotionState.STAGED,
    "prod": PromotionState.PROD,
}

STATE_TO_ENV: Dict[PromotionState, str] = {v: k for k, v in ENV_TO_STATE.items()}


class GateType(str, Enum):
    """Types of quality gates that can block promotion."""

    EVAL = "eval"  # Automated eval suite must pass
    APPROVAL = "approval"  # Manual human approval required
    SOAK = "soak"  # Time-based soak period required


@dataclass(frozen=True)
class GateResult:
    """Result of evaluating a single promotion gate."""

    gate_type: GateType
    passed: bool
    details: str = ""
    metadata: Dict[str, str] = field(default_factory=dict)

    def to_dict(self) -> dict:
        d: dict = {
            "gate_type": self.gate_type.value,
            "passed": self.passed,
        }
        if self.details:
            d["details"] = self.details
        if self.metadata:
            d["metadata"] = dict(self.metadata)
        return d

    @classmethod
    def from_dict(cls, data: dict) -> GateResult:
        for key in ("gate_type", "passed"):
            if key not in data:
                raise ValueError(f"GateResult missing required field: '{key}'")
        return cls(
            gate_type=GateType(data["gate_type"]),
            passed=data["passed"],
            details=data.get("details", ""),
            metadata=data.get("metadata", {}),
        )


@dataclass(frozen=True)
class PromotionRecord:
    """
    Record of a single promotion event in the history.

    Appended to .releaseops/promotion_history.yaml on every promotion/rollback.
    Provides a complete audit trail.
    """

    bundle_id: str
    version: str
    from_env: Optional[str]  # None for initial creation (DRAFT)
    to_env: str
    state: PromotionState
    timestamp: str  # ISO 8601
    promoted_by: str
    reason: str = ""
    is_rollback: bool = False
    rolled_back_from: Optional[str] = None  # Version that was rolled back from
    gate_results: List[GateResult] = field(default_factory=list)
    git_commit: Optional[str] = None  # Git commit SHA for this promotion
    git_tag: Optional[str] = None  # Git tag created

    def __post_init__(self) -> None:
        if not self.bundle_id:
            raise ValueError("bundle_id cannot be empty")
        if not self.version:
            raise ValueError("version cannot be empty")
        if not self.to_env:
            raise ValueError("to_env cannot be empty")

    @property
    def gates_passed(self) -> bool:
        """Whether all gates passed."""
        return all(g.passed for g in self.gate_results)

    def to_dict(self) -> dict:
        d: dict = {
            "bundle_id": self.bundle_id,
            "version": self.version,
            "from_env": self.from_env,
            "to_env": self.to_env,
            "state": self.state.value,
            "timestamp": self.timestamp,
            "promoted_by": self.promoted_by,
        }
        if self.reason:
            d["reason"] = self.reason
        if self.is_rollback:
            d["is_rollback"] = True
            if self.rolled_back_from:
                d["rolled_back_from"] = self.rolled_back_from
        if self.gate_results:
            d["gate_results"] = [g.to_dict() for g in self.gate_results]
        if self.git_commit:
            d["git_commit"] = self.git_commit
        if self.git_tag:
            d["git_tag"] = self.git_tag
        return d

    @classmethod
    def from_dict(cls, data: dict) -> PromotionRecord:
        for key in ("bundle_id", "version", "to_env"):
            if key not in data:
                raise ValueError(f"PromotionRecord missing required field: '{key}'")
        gate_results = [
            GateResult.from_dict(g) for g in data.get("gate_results", [])
        ]
        return cls(
            bundle_id=data["bundle_id"],
            version=data["version"],
            from_env=data.get("from_env"),
            to_env=data["to_env"],
            state=PromotionState(data.get("state", "DRAFT")),
            timestamp=data.get("timestamp", ""),
            promoted_by=data.get("promoted_by", ""),
            reason=data.get("reason", ""),
            is_rollback=data.get("is_rollback", False),
            rolled_back_from=data.get("rolled_back_from"),
            gate_results=gate_results,
            git_commit=data.get("git_commit"),
            git_tag=data.get("git_tag"),
        )


@dataclass
class PromotionHistory:
    """
    Append-only log of all promotion events.

    Stored in .releaseops/promotion_history.yaml.
    """

    records: List[PromotionRecord] = field(default_factory=list)

    def append(self, record: PromotionRecord) -> None:
        self.records.append(record)

    def get_history(
        self,
        bundle_id: Optional[str] = None,
        env: Optional[str] = None,
        limit: Optional[int] = None,
    ) -> List[PromotionRecord]:
        """Filter promotion history by bundle and/or environment."""
        filtered = self.records
        if bundle_id:
            filtered = [r for r in filtered if r.bundle_id == bundle_id]
        if env:
            filtered = [r for r in filtered if r.to_env == env]
        # Most recent first
        filtered = list(reversed(filtered))
        if limit:
            filtered = filtered[:limit]
        return filtered

    def get_previous_version(self, bundle_id: str, env: str) -> Optional[str]:
        """Get the version before the current one for rollback."""
        env_records = [
            r for r in self.records
            if r.bundle_id == bundle_id and r.to_env == env and not r.is_rollback
        ]
        if len(env_records) < 2:
            return None
        return env_records[-2].version

    def to_dict(self) -> dict:
        return {"records": [r.to_dict() for r in self.records]}

    @classmethod
    def from_dict(cls, data: dict) -> PromotionHistory:
        records = [
            PromotionRecord.from_dict(r) for r in data.get("records", [])
        ]
        return cls(records=records)
