"""
Rollout Manager - Handles promotion gates and automatic rollback
Tracks: shadow → paper → canary_10pct → full
Evaluates rollback triggers from release_gates.json
"""

from __future__ import annotations

import json
import os
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional

try:
    from src.ledger import append_ledger_event
except ModuleNotFoundError:
    from ledger import append_ledger_event

try:
    from src.config_snapshot import rollback_snapshot
except ModuleNotFoundError:
    from config_snapshot import rollback_snapshot


def _now() -> str:
    return datetime.now(timezone.utc).isoformat()


# ─── Promotion state machine ──────────────────────────────────────────────

PROMOTION_ORDER = ["shadow", "paper", "canary_10pct", "full"]


def _load_release_gates(root: Path) -> Dict[str, Any]:
    path = root / "ops" / "release_gates.json"
    if not path.exists():
        return {}
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return {}


def _load_runtime_state(root: Path, tenant_id: str) -> Dict[str, Any]:
    from src.tenant_manager import load_runtime_state
    return load_runtime_state(root, tenant_id)


def _save_runtime_state(root: Path, tenant_id: str, state: Dict[str, Any]) -> None:
    from src.tenant_manager import tenant_paths
    paths = tenant_paths(root, tenant_id)
    paths["runtime"].parent.mkdir(parents=True, exist_ok=True)
    paths["runtime"].write_text(
        json.dumps(state, ensure_ascii=False, indent=2),
        encoding="utf-8"
    )


def get_promotion_mode(root: Path, tenant_id: str) -> str:
    state = _load_runtime_state(root, tenant_id)
    return str(state.get("promotion_mode", "shadow"))


def set_promotion_mode(root: Path, tenant_id: str, mode: str) -> Dict[str, Any]:
    """Set promotion mode and log audit event."""
    if mode not in PROMOTION_ORDER:
        raise ValueError(f"Invalid promotion mode: {mode}. Valid: {PROMOTION_ORDER}")

    state = _load_runtime_state(root, tenant_id)
    prev = state.get("promotion_mode", "shadow")
    state["promotion_mode"] = mode
    state["promotion_updated_at"] = _now()
    _save_runtime_state(root, tenant_id, state)

    audit = {
        "promotion_mode": mode,
        "previous_mode": prev,
        "triggered_by": "manual",
        "promotion_updated_at": _now(),
    }
    append_ledger_event(
        root / "logs/chat_tuning_audit.ndjson",
        "promotion_change",
        {"tenant_id": tenant_id, "mode_change": audit},
    )
    return audit


# ─── Gate Evaluation ─────────────────────────────────────────────────────

def _metric_matches(metric: float, operator: str, threshold: float) -> bool:
    if operator == ">":
        return metric > threshold
    if operator == ">=":
        return metric >= threshold
    if operator == "<":
        return metric < threshold
    if operator == "<=":
        return metric <= threshold
    if operator == "==":
        return metric == threshold
    return False


def evaluate_promotion_gates(
    root: Path, tenant_id: str, execution_stats: Dict[str, Any] = None
) -> List[Dict[str, Any]]:
    """
    Evaluate promotion gates for the current promotion mode.
    Returns list of gates that have been met (ready to promote).
    If execution_stats is None, falls back to paper trading ledger stats.
    """
    if execution_stats is None:
        execution_stats = load_paper_trading_stats(root, tenant_id)
    gates = _load_release_gates(root)
    promotion_gates = gates.get("promotion_gates", {})
    current_mode = get_promotion_mode(root, tenant_id)

    next_idx = None
    for i, mode_name in enumerate(PROMOTION_ORDER):
        if mode_name == current_mode:
            next_idx = i + 1
            break
    if next_idx is None or next_idx >= len(PROMOTION_ORDER):
        return []
    next_mode = PROMOTION_ORDER[next_idx]
    gate_key = f"{current_mode}_to_{next_mode}"
    next_gate = promotion_gates.get(gate_key, {})
    if not isinstance(next_gate, dict) or not next_gate:
        return []

    ready = []
    for req_key, req_val in next_gate.items():
        metric_val = execution_stats.get(req_key, 0)
        operator = "<=" if str(req_key).startswith("max_") else ">="
        if _metric_matches(float(metric_val), operator, float(req_val)):
            ready.append({
                "gate": req_key,
                "required": req_val,
                "current": metric_val,
                "ready": True,
                "next_mode": next_mode,
                "operator": operator,
            })
        else:
            ready.append({
                "gate": req_key,
                "required": req_val,
                "current": metric_val,
                "ready": False,
                "next_mode": next_mode,
                "operator": operator,
            })
    return ready


# ─── Rollback Trigger Evaluation ─────────────────────────────────────────

def evaluate_rollback_triggers(
    root: Path, execution_stats: Dict[str, Any], runtime_state: Dict[str, Any]
) -> List[Dict[str, Any]]:
    """
    Check current metrics against rollback triggers.
    Returns triggered gates.
    """
    gates = _load_release_gates(root)
    triggers = gates.get("rollback_triggers", [])
    if not triggers:
        return []

    triggered = []
    metrics = {**execution_stats, **runtime_state}

    for t in triggers:
        metric_key = t["metric"]
        operator = t["operator"]
        threshold = float(t["threshold"])
        value = float(metrics.get(metric_key, 0))
        if _metric_matches(value, operator, threshold):
            triggered.append({
                "id": t["id"],
                "metric": metric_key,
                "operator": operator,
                "threshold": threshold,
                "current_value": round(value, 4),
                "triggered_at": _now(),
                "trigger_type": "rollback",
            })
    return triggered


def trigger_auto_rollback(
    root: Path, tenant_id: str, trigger: Dict[str, Any]
) -> Dict[str, Any]:
    """
    Execute automatic rollback to previous known-good state.
    Returns rollback result.
    """
    state = _load_runtime_state(root, tenant_id)
    prev_tag = state.get("last_known_good_tag", "")

    rollback_result = {
        "rollback_id": f"auto_{trigger['id']}_{int(datetime.now(timezone.utc).timestamp())}",
        "trigger": trigger,
        "previous_tag": prev_tag,
        "timestamp": _now(),
        "success": False,
        "error": "",
    }

    try:
        if prev_tag:
            result = rollback_snapshot(root, tenant_id, prev_tag)
            rollback_result["success"] = True
            rollback_result["snapshot"] = result
        else:
            # No snapshot to rollback to – activate kill switch
            state["kill_switch"] = True
            state["kill_switch_reason"] = f"rollback_triggered:{trigger['id']}_no_snapshot"
            _save_runtime_state(root, tenant_id, state)
            rollback_result["success"] = True
            rollback_result["kill_switch_activated"] = True
    except Exception as exc:
        rollback_result["error"] = str(exc)
        rollback_result["success"] = False

    # Log audit event
    append_ledger_event(
        root / "logs/chat_tuning_audit.ndjson",
        "auto_rollback",
        {
            "tenant_id": tenant_id,
            "rollback_id": rollback_result["rollback_id"],
            "trigger": trigger,
            "previous_tag": prev_tag,
            "success": rollback_result["success"],
            "error": rollback_result["error"],
        },
    )
    return rollback_result


# ─── Per-decision gate check ─────────────────────────────────────────────

def check_rollout_safety(
    root: Path,
    tenant_id: str,
    execution_stats: Dict[str, Any],
    runtime_state: Dict[str, Any],
) -> Dict[str, Any]:
    """
    Called before each decision. Returns:
      {safe: True}  – proceed normally
      {safe: False, reason: "...", rollback: {...}} – trigger auto-rollback
    """
    promotion_mode = get_promotion_mode(root, tenant_id)

    # Check rollback triggers only for active modes (not shadow)
    if promotion_mode != "shadow":
        triggered = evaluate_rollback_triggers(root, execution_stats, runtime_state)
        if triggered:
            t = triggered[0]
            rollback_result = trigger_auto_rollback(root, tenant_id, t)
            return {
                "safe": False,
                "reason": f"ROLLBACK_TRIGGER:{t['id']}",
                "trigger": t,
                "rollback": rollback_result,
                "action": "AUTO_ROLLBACK",
            }

    return {"safe": True, "promotion_mode": promotion_mode}


def load_paper_trading_stats(root: Path, tenant_id: str = "default") -> Dict[str, Any]:
    """Bridge PaperTradingGate → RolloutManager: read paper trading ledger stats.

    Reads the paper trading ledger (paper_trading_ledger.ndjson) and computes
    standard execution stats used by evaluate_promotion_gates().
    """
    import config_manager
    data_dir = config_manager.get_data_dir()
    logs_dir = config_manager.get_logs_dir()
    ledger_candidates = [
        logs_dir / "paper_trading_ledger.ndjson",
        data_dir / "bridge" / "paper_trading_ledger.ndjson",
    ]
    ledger_path = next((path for path in ledger_candidates if path.exists()), ledger_candidates[0])

    empty = {"total_trades": 0, "win_rate": 0, "profit_factor": 0,
             "max_drawdown_pct": 0, "total_pnl": 0}

    if not ledger_path.exists():
        return empty

    trades = []
    with open(ledger_path, "r", encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            try:
                trades.append(json.loads(line))
            except json.JSONDecodeError:
                continue

    if not trades:
        return empty

    wins = [t for t in trades if t.get("pnl", 0) > 0]
    losses = [t for t in trades if t.get("pnl", 0) < 0]
    gross_profit = sum(t.get("pnl", 0) for t in wins)
    gross_loss = abs(sum(t.get("pnl", 0) for t in losses))

    equity = 0.0
    peak_equity = 0.0
    max_drawdown_pct = 0.0
    for trade in trades:
        equity += float(trade.get("pnl", 0) or 0)
        peak_equity = max(peak_equity, equity)
        if peak_equity > 0:
            drawdown_pct = max(0.0, (peak_equity - equity) / peak_equity * 100.0)
            max_drawdown_pct = max(max_drawdown_pct, drawdown_pct)

    return {
        "total_trades": len(trades),
        "win_rate": round(len(wins) / len(trades) * 100, 1),
        "profit_factor": round(gross_profit / gross_loss, 2) if gross_loss > 0 else float("inf"),
        "total_pnl": round(sum(t.get("pnl", 0) for t in trades), 2),
        "max_drawdown_pct": round(max_drawdown_pct, 2),
    }
