"""
Structured signal explainability for the command center.

Each sleeve gets criteria checklists (met / not met), sizing, and trade reminders
so automated signals stay auditable without reading source code.
"""

from __future__ import annotations

import json
from datetime import datetime
from pathlib import Path
from typing import Any
from zoneinfo import ZoneInfo

from RenTech.monitor.strategy_catalog import STRATEGY_DEFINITIONS

NY = ZoneInfo("America/New_York")
_REPO = Path(__file__).resolve().parents[2]
_VRP_PARAMS_PATH = _REPO / "RenTech/strategy_stack/sleeve_risk_fractions.json"

_REGIME_COPY: dict[str, dict[str, Any]] = {
    "pmcc": {
        "band": "VIX < 12 (complacency)",
        "structure": "R1 weekly long strangle (long call ~45Δ + long put ~18Δ OTM)",
        "dte_key": "r1_strangle_dte",
        "time_stop_key": "r1_time_stop_days",
        "exit_rules": "TP +50% debit · SL −50% debit · time stop",
        "risk_note": "2% of capital per entry (if PMCC enabled)",
    },
    "diagonal": {
        "band": "12 ≤ VIX ≤ 20 (normal vol)",
        "structure": "R2a put diagonal (sell 21d ~30Δ / buy 45d ~15Δ) + R2b put credit spread (30d)",
        "dte_key": None,
        "time_stop_key": "r2_time_stop_days",
        "exit_rules": "R2a: $50 TP / −$150 SL per contract · 14d · R2b: 50% credit TP · 21d",
        "risk_note": "R2a diagonal risk budget + R2b at put-spread risk fraction",
    },
    "naked": {
        "band": "20 < VIX ≤ 30 (elevated fear)",
        "structure": "R3 put credit spread (~45 DTE, short ~15Δ / long ~7Δ)",
        "dte_key": None,
        "time_stop_key": "r3_time_stop_days",
        "exit_rules": "TP 50% of max credit · time stop 24d",
        "risk_note": "2% capital max-loss sizing per entry",
    },
    "credit_spread": {
        "band": "VIX > 30 (extreme panic)",
        "structure": "R4 wide put credit spread (~45 DTE, short ~15Δ / long ~5Δ)",
        "dte_key": None,
        "time_stop_key": "r4_time_stop_days",
        "exit_rules": "TP 50% of max credit · time stop 24d",
        "risk_note": "2% capital max-loss sizing per entry",
    },
}


def _criterion(
    cid: str,
    label: str,
    met: bool,
    *,
    value: Any = None,
    threshold: Any = None,
    note: str | None = None,
) -> dict[str, Any]:
    row: dict[str, Any] = {"id": cid, "label": label, "met": bool(met)}
    if value is not None:
        row["value"] = value
    if threshold is not None:
        row["threshold"] = threshold
    if note:
        row["note"] = note
    return row


def _load_vrp_params() -> dict[str, Any]:
    if not _VRP_PARAMS_PATH.is_file():
        return {}
    try:
        raw = json.loads(_VRP_PARAMS_PATH.read_text(encoding="utf-8"))
        return raw.get("strategy_params") or {} if isinstance(raw, dict) else {}
    except json.JSONDecodeError:
        return {}


def _fmt_usd(x: Any) -> str | None:
    try:
        v = float(x)
        if not (v == v):  # NaN
            return None
        return f"${v:,.0f}"
    except (TypeError, ValueError):
        return None


def _leg_dte(expiry: str, as_of: datetime | None = None) -> int | None:
    if not expiry or len(str(expiry)) < 8:
        return None
    try:
        exp = datetime.strptime(str(expiry)[:8], "%Y%m%d").date()
        ref = (as_of or datetime.now(NY)).date()
        return max(0, (exp - ref).days)
    except ValueError:
        return None


def explain_vrp(
    *,
    live_state: dict[str, Any] | None,
    spy_gates: dict[str, Any] | None,
    recommendations: list[dict[str, Any]],
    fund_nav: float,
    fund_weight: float,
    kill_switch: bool,
) -> dict[str, Any]:
    params = _load_vrp_params()
    state = live_state or {}
    gates = spy_gates or {}
    spy_last = gates.get("spy_last") or state.get("spy_live") or state.get("spy_last_bar")
    sma200 = gates.get("spy_sma200") or state.get("sma200")
    spy_gt = gates.get("spy_gt_sma200")
    if spy_gt is None and spy_last is not None and sma200 is not None:
        spy_gt = float(spy_last) > float(sma200)
    vix = state.get("vix_live")
    regime = state.get("regime_now") or (recommendations[0].get("regime") if recommendations else None)
    entry_gate = state.get("entry_time_gate_pass")
    entry_after = state.get("entry_after_et", "15:40")

    criteria: list[dict[str, Any]] = [
        _criterion(
            "spy_sma200",
            "SPY above SMA(200) — global risk-on gate",
            bool(spy_gt),
            value=f"SPY {float(spy_last):.2f}" if spy_last is not None else None,
            threshold=f"SMA200 {float(sma200):.2f}" if sma200 is not None else "SMA(200)",
            note="No new option entries when SPY ≤ SMA(200); open legs still managed.",
        ),
        _criterion(
            "entry_time",
            f"Entry window open (after {entry_after} ET)",
            bool(entry_gate) if entry_gate is not None else False,
            value=state.get("created_at", "")[:19] if state.get("created_at") else None,
            threshold=f">= {entry_after} ET",
            note=state.get("decision_hint", ""),
        ),
        _criterion(
            "kill_switch",
            "Kill switch inactive",
            not kill_switch,
            note="RenTech/live/config/KILL_SWITCH file halts new entries.",
        ),
    ]
    if vix is not None:
        r1, r2, r3 = (
            float(params.get("vix_r1_max", 12)),
            float(params.get("vix_r2_max", 20)),
            float(params.get("vix_r3_max", 30)),
        )
        criteria.append(
            _criterion(
                "vix_band",
                "VIX maps to active regime band",
                True,
                value=f"VIX {float(vix):.2f} → {regime}",
                threshold=f"R1<{r1} · R2≤{r2} · R3≤{r3} · R4>{r3}",
            )
        )

    sleeve_nav = fund_nav * fund_weight if fund_nav > 0 else 0.0
    trades: list[dict[str, Any]] = []
    for rec in recommendations:
        reg = str(rec.get("regime", regime or ""))
        copy = _REGIME_COPY.get(reg, {})
        legs = rec.get("legs") or []
        leg_rows = []
        for leg in legs:
            exp = leg.get("expiry", "")
            leg_rows.append(
                {
                    "action": leg.get("open_action"),
                    "symbol": leg.get("symbol"),
                    "expiry": exp,
                    "dte": _leg_dte(str(exp)),
                    "right": leg.get("right"),
                    "strike": leg.get("strike"),
                    "ratio": leg.get("ratio", 1),
                }
            )
        decision = str(rec.get("decision", rec.get("entry_action", "")))
        qty = int(rec.get("proposed_qty") or 0)
        risk_budget = rec.get("risk_budget_usd")
        rpc = rec.get("risk_per_contract_usd")
        max_risk = (float(rpc) * qty) if rpc and qty else risk_budget
        trades.append(
            {
                "source": rec.get("_source", "recommendation"),
                "regime": reg,
                "decision": decision,
                "structure": copy.get("structure", reg),
                "vix_band": copy.get("band", ""),
                "proposed_qty": qty,
                "entry_limit_per_share": rec.get("entry_limit_per_share"),
                "combo_mid_per_share": rec.get("combo_mid_per_share"),
                "risk_budget_usd": risk_budget,
                "risk_per_contract_usd": rpc,
                "max_risk_at_stake_usd": max_risk,
                "time_stop_days": rec.get("time_stop_days"),
                "stop_threshold_unrealized_usd": rec.get("stop_threshold_unrealized_usd"),
                "quote_quality": rec.get("quote_quality"),
                "sizing_formula": rec.get("sizing_formula"),
                "legs": leg_rows,
                "exit_rules": copy.get("exit_rules", ""),
            }
        )
        criteria.append(
            _criterion(
                f"quote_{reg}",
                f"{reg}: live option quotes usable",
                rec.get("quote_quality") in ("live_bag_mid", "synthetic_liquid"),
                value=rec.get("quote_quality"),
                threshold="live_bag_mid or synthetic_liquid",
            )
        )
        criteria.append(
            _criterion(
                f"decision_{reg}",
                f"{reg}: entry decision",
                decision in ("ENTER", "enter", "open"),
                value=decision,
                note=f"qty={qty} · risk budget {_fmt_usd(risk_budget)}",
            )
        )

    reg_copy = _REGIME_COPY.get(str(regime or ""), {})
    ts_key = reg_copy.get("time_stop_key")
    return {
        "id": "spy_theta",
        "label": "SPY Theta (lit4 + VRP)",
        "source": "ibkr_live" if state else "backtest_reference",
        "rules_summary": STRATEGY_DEFINITIONS["spy_theta"]["rules"],
        "criteria": criteria,
        "trade_details": {
            "fund_weight": fund_weight,
            "sleeve_notional_usd": round(sleeve_nav, 2),
            "active_regime": regime,
            "regime_band": reg_copy.get("band"),
            "structure": reg_copy.get("structure"),
            "time_stop_days": params.get(ts_key) if ts_key else None,
            "exit_rules": reg_copy.get("exit_rules"),
            "risk_note": reg_copy.get("risk_note"),
            "execution": "Short at bid / cover at ask (backtest); IB combo limit DAY entry live",
            "overlap": "Lit4 sleeves: one position per spec; VRP may hold open until TP/SL/time",
        },
        "trades": trades,
    }


def explain_tactical(
    *,
    tac: dict[str, Any],
    fund_nav: float,
    fund_weight: float,
    ib_mv: dict[str, float],
) -> dict[str, Any]:
    targets = tac.get("targets") or {}
    config = tac.get("config") or {}
    gates = tac.get("gate_checks") or []
    sma_w = int(config.get("sma_window", 200))
    mom_lb = int(config.get("mom_lookback_days", 252))
    mom_skip = int(config.get("mom_skip_days", 21))
    bond_mult = float(config.get("bond_baseline_mult", 1.0))
    sleeve_nav = fund_nav * fund_weight if fund_nav > 0 else 0.0

    criteria: list[dict[str, Any]] = [
        _criterion(
            "rebalance",
            "Weights apply next session (shift-1)",
            True,
            note=f"As of {tac.get('as_of', 'latest bar')}",
        ),
        _criterion(
            "cash_yield",
            "Uninvested sleeve weight earns cash yield",
            True,
            value=f"{float(config.get('cash_annual_yield', 0.04)):.1%} annual / 252",
        ),
    ]
    if bond_mult != 1.0:
        criteria.append(
            _criterion(
                "bond70",
                f"TLT/IEF baseline × {bond_mult}",
                True,
                note="Production bond70_sma200 variant",
            )
        )

    positions: list[dict[str, Any]] = []
    for g in gates:
        trend_ok = bool(g.get("trend_ok"))
        mom_ok = bool(g.get("mom_ok"))
        invested = float(g.get("target_weight") or 0) > 1e-9
        sym = g.get("ticker", "")
        criteria.append(
            _criterion(
                f"gate_{sym}",
                f"{sym}: close > SMA({sma_w}) AND {mom_skip}d-{mom_lb}d momentum > 0",
                trend_ok and mom_ok if str(config.get("weight_mode", "binary")) == "binary" else invested,
                value=(
                    f"close {g.get('close')} · SMA {g.get('sma')} · mom {float(g.get('momentum', 0)):.2%}"
                    if g.get("close") is not None
                    else None
                ),
                threshold="trend + momentum",
                note=f"baseline {float(g.get('baseline_weight', 0)):.1%} → target {float(g.get('target_weight', 0)):.1%}",
            )
        )
        tgt_usd = sleeve_nav * float(g.get("target_weight") or 0)
        have = float(ib_mv.get(sym, 0))
        positions.append(
            {
                "symbol": sym,
                "target_weight": g.get("target_weight"),
                "target_notional_usd": round(tgt_usd, 2),
                "ib_market_value_usd": round(have, 2),
                "gap_usd": round(tgt_usd - have, 2),
                "trend_ok": trend_ok,
                "mom_ok": mom_ok,
            }
        )

    if not gates and targets:
        for sym, w in targets.items():
            tgt_usd = sleeve_nav * float(w)
            positions.append(
                {
                    "symbol": sym,
                    "target_weight": w,
                    "target_notional_usd": round(tgt_usd, 2),
                    "ib_market_value_usd": round(float(ib_mv.get(sym, 0)), 2),
                }
            )

    return {
        "id": "tactical_aw",
        "label": "Tactical All Weather",
        "source": tac.get("source", "backtest_csv"),
        "rules_summary": STRATEGY_DEFINITIONS["tactical_aw"]["rules"],
        "criteria": criteria,
        "trade_details": {
            "fund_weight": fund_weight,
            "sleeve_notional_usd": round(sleeve_nav, 2),
            "cash_weight": tac.get("cash_weight"),
            "total_invested_weight": tac.get("total_invested_weight"),
            "config": config,
            "instrument": "ETF shares (SPY, TLT, IEF, GLD, DBC)",
            "rebalance": "Daily signal; weights execute next bar",
        },
        "positions": positions,
    }


def explain_sector(
    *,
    sec: dict[str, Any],
    fund_nav: float,
    fund_weight: float,
    ib_mv: dict[str, float],
) -> dict[str, Any]:
    targets = sec.get("targets") or {}
    rankings = sec.get("rankings") or []
    top_k = int(sec.get("top_k", 3))
    sleeve_nav = fund_nav * fund_weight if fund_nav > 0 else 0.0

    criteria: list[dict[str, Any]] = [
        _criterion(
            "signal_month",
            "Month-end momentum signal (12−1 on close)",
            True,
            value=sec.get("signal_date") or sec.get("effective_date"),
            note=f"Effective {sec.get('effective_date')} · mode {sec.get('mode', '')}",
        ),
        _criterion(
            "top_k",
            f"Hold top {top_k} sectors at equal weight",
            len(targets) == top_k or (len(targets) > 0 and len(targets) <= top_k),
            value=", ".join(targets.keys()) if targets else "none",
            threshold=f"{top_k} names @ {1.0 / top_k:.1%} each" if top_k else None,
        ),
    ]
    if sec.get("new_entries"):
        criteria.append(
            _criterion(
                "new_entries",
                "New sector entries vs prior month",
                True,
                value=", ".join(sec["new_entries"]),
            )
        )
    if sec.get("exits_from_prior"):
        criteria.append(
            _criterion(
                "exits",
                "Sectors exited vs prior month — action required",
                False,
                value=", ".join(sec["exits_from_prior"]),
                note="Reduce or remove these names",
            )
        )

    positions: list[dict[str, Any]] = []
    for r in rankings:
        sym = r.get("ticker", "")
        held = bool(r.get("held"))
        w = float(targets.get(sym, 0)) if held else 0.0
        tgt_usd = sleeve_nav * w
        positions.append(
            {
                "symbol": sym,
                "rank": r.get("rank"),
                "aqr_mom": r.get("aqr_mom"),
                "held": held,
                "target_weight": w if held else 0,
                "target_notional_usd": round(tgt_usd, 2),
                "ib_market_value_usd": round(float(ib_mv.get(sym, 0)), 2),
            }
        )
    if not positions and targets:
        for sym, w in targets.items():
            positions.append(
                {
                    "symbol": sym,
                    "held": True,
                    "target_weight": w,
                    "target_notional_usd": round(sleeve_nav * float(w), 2),
                    "ib_market_value_usd": round(float(ib_mv.get(sym, 0)), 2),
                }
            )

    return {
        "id": "sector_momentum",
        "label": "Sector Momentum",
        "source": sec.get("source", "backtest_csv"),
        "rules_summary": STRATEGY_DEFINITIONS["sector_momentum"]["rules"],
        "criteria": criteria,
        "trade_details": {
            "fund_weight": fund_weight,
            "sleeve_notional_usd": round(sleeve_nav, 2),
            "top_k": top_k,
            "rebalance": "Monthly (business month-end); trade effective next session",
            "instrument": "SPDR sector ETFs (long only)",
            "slack_cash": "4% annual on uninvested weight",
        },
        "positions": positions,
    }


def explain_reference_sleeve(sleeve_id: str, *, fund_weight: float, fund_nav: float) -> dict[str, Any]:
    defn = STRATEGY_DEFINITIONS.get(sleeve_id, {})
    return {
        "id": sleeve_id,
        "label": defn.get("role", sleeve_id),
        "source": "backtest_reference",
        "rules_summary": defn.get("rules", []),
        "criteria": [
            _criterion(
                "live_plugin",
                "IBKR live signal plugin",
                False,
                note="Not yet driven from IB bars in run_live_fund_signals; use backtest runner + manual review",
            ),
            _criterion(
                "fund_weight",
                "Allocated in fund combine",
                fund_weight > 0,
                value=f"{fund_weight:.1%}",
                threshold="> 0",
            ),
        ],
        "trade_details": {
            "fund_weight": fund_weight,
            "sleeve_notional_usd": round(fund_nav * fund_weight, 2) if fund_nav > 0 else 0,
            "runner": defn.get("runner", ""),
            "summary": defn.get("summary", ""),
        },
        "positions": [],
    }


def build_signal_explanations(snap: dict[str, Any]) -> dict[str, Any]:
    """Build ``signal_explain`` block merged into command_center_snapshot."""
    sig = snap.get("signals") or {}
    fund = snap.get("fund") or {}
    weights = fund.get("target_weights") or {}
    model = fund.get("model") or {}
    fund_nav = float(
        model.get("fund_nav_usd_live_ibkr")
        or model.get("fund_nav_usd")
        or (snap.get("ibkr") or {}).get("portfolio", {}).get("net_liquidation_usd")
        or 100_000.0
    )
    ibp = (snap.get("ibkr") or {}).get("portfolio") or {}
    ib_mv: dict[str, float] = {}
    for p in ibp.get("positions") or []:
        if str(p.get("sec_type", "")).upper() not in ("STK", "ETF"):
            continue
        sym = str(p.get("symbol", ""))
        if sym:
            ib_mv[sym] = ib_mv.get(sym, 0.0) + float(p.get("market_value") or 0)

    kill = bool((snap.get("risk") or {}).get("kill_switch_active"))
    recs = sig.get("recommendations") or []
    live_state = sig.get("live_signal_state") or _read_live_state()
    spy_gates = sig.get("spy_gates") or {}

    sleeves: list[dict[str, Any]] = []
    if weights.get("spy_theta", 0) > 0:
        sleeves.append(
            explain_vrp(
                live_state=live_state,
                spy_gates=spy_gates,
                recommendations=recs,
                fund_nav=fund_nav,
                fund_weight=float(weights.get("spy_theta", 0)),
                kill_switch=kill,
            )
        )
    if weights.get("tactical_aw", 0) > 0:
        sleeves.append(
            explain_tactical(
                tac=sig.get("tactical_aw") or {},
                fund_nav=fund_nav,
                fund_weight=float(weights["tactical_aw"]),
                ib_mv=ib_mv,
            )
        )
    if weights.get("sector_momentum", 0) > 0:
        sleeves.append(
            explain_sector(
                sec=sig.get("sector_momentum") or {},
                fund_nav=fund_nav,
                fund_weight=float(weights["sector_momentum"]),
                ib_mv=ib_mv,
            )
        )
    for ref_id in ("vxx_regime", "vxx_long_call", "macro_aw", "tsmom", "equity_dip"):
        w = float(weights.get(ref_id, 0))
        if w > 0:
            sleeves.append(explain_reference_sleeve(ref_id, fund_weight=w, fund_nav=fund_nav))

    return {
        "generated_at": datetime.now(NY).isoformat(),
        "fund_nav_usd": fund_nav,
        "blurb": (
            "Green checks = criterion currently satisfied. "
            "Use this panel to audit automated signals before trading."
        ),
        "sleeves": sleeves,
    }


def _read_live_state() -> dict[str, Any] | None:
    path = _REPO / "RenTech/data/logs/live_signal_state.json"
    if not path.is_file():
        return None
    try:
        raw = json.loads(path.read_text(encoding="utf-8"))
        return raw if isinstance(raw, dict) else None
    except json.JSONDecodeError:
        return None
