"""
Promotion state machine for releaseops.

Promotes bundles through environments with quality gates:
DRAFT → CANDIDATE → STAGED → PROD → (ROLLED_BACK)
"""

from __future__ import annotations

import logging
from datetime import datetime, timezone
from typing import List, Optional

from llmhq_releaseops.core.resolver import Resolver
from llmhq_releaseops.models.promotion import (
    GateResult,
    GateType,
    PromotionHistory,
    PromotionRecord,
    PromotionState,
)
from llmhq_releaseops.storage.git_store import ConcurrentModificationError, GitStore

logger = logging.getLogger(__name__)

# Valid promotion paths (from_env → to_env)
PROMOTION_PATHS = {
    "dev": ["staging"],
    "staging": ["prod"],
    None: ["dev"],  # Initial creation goes to dev
}


class Promoter:
    """
    Promotes bundles through gated environments.

    Enforces the promotion state machine and quality gates.
    """

    def __init__(self, store: GitStore, resolver: Resolver):
        self.store = store
        self.resolver = resolver

    def promote(
        self,
        bundle_id: str,
        version: str,
        target_env: str,
        promoted_by: str = "",
        reason: str = "",
        skip_gates: bool = False,
        dry_run: bool = False,
        gate_results: Optional[List[GateResult]] = None,
    ) -> PromotionRecord:
        """
        Promote a bundle version to a target environment.

        Args:
            bundle_id: Bundle identifier
            version: Bundle version to promote
            target_env: Target environment (dev, staging, prod)
            promoted_by: Who is promoting
            reason: Reason for promotion
            skip_gates: Skip quality gates (dangerous)
            dry_run: Validate without actually promoting
            gate_results: Pre-computed gate results

        Returns:
            PromotionRecord for this event.
        """
        # Validate bundle exists
        if not self.store.bundle_exists(bundle_id, version):
            raise FileNotFoundError(
                f"Bundle '{bundle_id}' version '{version}' does not exist"
            )

        # Determine source environment
        from_env = self._find_current_env(bundle_id, version)

        # Validate promotion path
        valid_targets = PROMOTION_PATHS.get(from_env, [])
        if target_env not in valid_targets and not skip_gates:
            raise ValueError(
                f"Cannot promote from '{from_env}' to '{target_env}'. "
                f"Valid targets: {valid_targets}"
            )

        # Check quality gates
        gate_results = gate_results or []
        if not skip_gates:
            env_config = self.store.load_environment(target_env)
            policy = env_config.promotion_policy
            if policy and policy.requires_eval:
                eval_gate = self._check_eval_gate(bundle_id, version)
                gate_results.append(eval_gate)
                if not eval_gate.passed:
                    logger.warning(
                        f"Eval gate FAILED for {bundle_id} v{version} → {target_env}"
                    )

        # Determine promotion state
        state = self._env_to_state(target_env)

        # Build the record
        record = PromotionRecord(
            bundle_id=bundle_id,
            version=version,
            from_env=from_env,
            to_env=target_env,
            state=state,
            timestamp=datetime.now(timezone.utc).isoformat(),
            promoted_by=promoted_by,
            reason=reason,
            gate_results=gate_results,
        )

        if dry_run:
            logger.info(f"[DRY RUN] Would promote {bundle_id} v{version} → {target_env}")
            return record

        # Check if all gates passed
        if gate_results and not all(g.passed for g in gate_results):
            blocking_failures = [
                g for g in gate_results if not g.passed
            ]
            raise ValueError(
                f"Promotion blocked by failed gates: "
                f"{[g.gate_type.value for g in blocking_failures]}"
            )

        # Execute promotion with optimistic concurrency
        env_config, env_hash = self.store.load_environment_with_hash(target_env)
        history, history_hash = self.store.load_promotion_history_with_hash()

        self.resolver.set_version(
            target_env, bundle_id, version,
            updated_by=promoted_by,
            expected_hash=env_hash,
        )

        # Record in history
        history.append(record)
        self.store.save_promotion_history(history, expected_hash=history_hash)

        logger.info(f"Promoted {bundle_id} v{version} → {target_env}")
        return record

    def rollback(
        self,
        env: str,
        bundle_id: str,
        steps: int = 1,
        to_version: Optional[str] = None,
        promoted_by: str = "",
        reason: str = "",
    ) -> PromotionRecord:
        """
        Roll back a bundle in an environment.

        Rollback = promote previous version forward (creates new history entry).

        Args:
            env: Environment to roll back in
            bundle_id: Bundle to roll back
            steps: How many versions back (default 1)
            to_version: Specific version to roll back to (overrides steps)
            promoted_by: Who is rolling back
            reason: Reason for rollback
        """
        # Get current version
        current_version = self.resolver.resolve_version(bundle_id, env)
        if current_version is None:
            raise ValueError(
                f"Bundle '{bundle_id}' is not deployed in '{env}'"
            )

        # Determine target version
        if to_version:
            target_version = to_version
        else:
            history = self.store.load_promotion_history()
            # Find previous versions from history
            env_records = [
                r for r in history.records
                if r.bundle_id == bundle_id
                and r.to_env == env
                and not r.is_rollback
            ]
            if len(env_records) < steps + 1:
                raise ValueError(
                    f"Cannot roll back {steps} step(s): only {len(env_records)} "
                    f"promotion(s) found for '{bundle_id}' in '{env}'"
                )
            target_version = env_records[-(steps + 1)].version

        # Verify target version exists
        if not self.store.bundle_exists(bundle_id, target_version):
            raise FileNotFoundError(
                f"Rollback target '{bundle_id}' version '{target_version}' not found"
            )

        # Execute rollback with optimistic concurrency (skip gates — emergency action)
        _env_config, env_hash = self.store.load_environment_with_hash(env)
        history, history_hash = self.store.load_promotion_history_with_hash()

        self.resolver.set_version(
            env, bundle_id, target_version,
            updated_by=promoted_by,
            expected_hash=env_hash,
        )

        record = PromotionRecord(
            bundle_id=bundle_id,
            version=target_version,
            from_env=env,
            to_env=env,
            state=PromotionState.ROLLED_BACK,
            timestamp=datetime.now(timezone.utc).isoformat(),
            promoted_by=promoted_by,
            reason=reason or f"Rolled back from {current_version}",
            is_rollback=True,
            rolled_back_from=current_version,
        )

        # Record in history
        history.append(record)
        self.store.save_promotion_history(history, expected_hash=history_hash)

        logger.info(
            f"Rolled back {bundle_id} in {env}: {current_version} → {target_version}"
        )
        return record

    def _find_current_env(self, bundle_id: str, version: str) -> Optional[str]:
        """Find the highest environment a bundle version is currently in."""
        # Check in reverse priority order so we return the highest env
        found = None
        env_priority = {"dev": 0, "staging": 1, "prod": 2}
        for env_name in self.store.list_environments():
            current = self.resolver.resolve_version(bundle_id, env_name)
            if current == version:
                if found is None or env_priority.get(env_name, 0) > env_priority.get(found, 0):
                    found = env_name
        return found

    def _check_eval_gate(self, bundle_id: str, version: str) -> GateResult:
        """Check if the bundle has passing eval results."""
        reports = self.store.list_eval_reports()
        for report_key in reports:
            try:
                report = self.store.load_eval_report(report_key)
                if (
                    report.bundle_id == bundle_id
                    and report.bundle_version == version
                    and report.passed
                ):
                    return GateResult(
                        gate_type=GateType.EVAL,
                        passed=True,
                        details=f"Passed eval suite '{report.suite_id}' "
                        f"({report.summary.pass_rate:.0%} pass rate)",
                    )
            except Exception:
                continue

        return GateResult(
            gate_type=GateType.EVAL,
            passed=False,
            details=f"No passing eval report found for {bundle_id} v{version}",
        )

    def _env_to_state(self, env: str) -> PromotionState:
        """Map environment name to promotion state."""
        mapping = {
            "dev": PromotionState.DRAFT,
            "staging": PromotionState.STAGED,
            "prod": PromotionState.PROD,
        }
        return mapping.get(env, PromotionState.DRAFT)
