"""READ-ONLY multi-edge evaluator — run ALL fxhnt edges on a given warehouse store.

This mirrors `bybit_carry_revalidate.carry_metrics_from_store` (the carry/xsfunding template) for the OTHER
tracked edges so the SAME validated edge return engines can be evaluated against any warehouse — in
particular the Bybit `bybit_features` table, the venue we'll actually trade — instead of only the default
legacy `features` warehouse. It is PURE given the store + the injected external inputs, and it is strictly
READ-ONLY: it computes each edge's daily-return series + `{cagr, sharpe, max_dd, total_return, days,
symbols, ...}` in memory and reports them. It NEVER writes/clobbers the live paper book tables
(paper_nav/paper_sleeve_ret/...) — there is no PaperRepo here and no `store.write_*` call.

How each edge's REAL return computation is reused (no edge logic is reimplemented):
  * tstrend (crypto_tstrend): `TrendRunner(panel, long_short=False).run()` — the EXACT validated TSMOM
    return engine the live sleeve (`bybit_paper_weights`) drives. We drive it off the store's `crypto_close_panel`.
  * unlock: `UnlockShortRunner(warehouse_unlock_panel(close), events).run()` — the live unlock sleeve's own
    runner, off the store's close panel + the DefiLlama-style unlock calendar (external; INJECTED here so
    tests need no network — on the cluster the CLI loads the real `unlock_calendar.json`). N/A when the
    calendar is absent.
  * stablecoin_rotation: `StableReversionRunner(spot_panel).run()` — the live peg-reversion runner. Its
    input is a STABLECOIN SPOT close panel (FDUSDUSDT/TUSDUSDT/USDPUSDT), which the Bybit *perp* warehouse
    does NOT carry → reported N/A (reason) unless an explicit `stablecoin_spot_panel` is injected.
  * xsfunding (carry): delegated UNCHANGED to `carry_metrics_from_store` (the existing template).

Metric shape: every edge returns the same keys as `carry_metrics_from_store`
(`cagr, sharpe, max_dd, total_return, ann_return, days, symbols, first_day, last_day`) PLUS `available`
(bool) and, when `available is False`, a `reason` string (so a genuinely-uncomputable edge is reported
"N/A (reason)" rather than fabricating a number).
"""
from __future__ import annotations

import datetime as dt
from typing import Any, Callable, Protocol

import numpy as np

from fxhnt.application.bybit_carry_revalidate import _curve_metrics, carry_metrics_from_store

# The tracked edge names this evaluator knows how to drive (xsfunding == carry).
EDGE_NAMES = ("tstrend", "unlock", "stablecoin_rotation", "xsfunding")

# Default per-venue stablecoin spot tickers the live stablecoin sleeve trades (the legacy venue's spot). The
# Bybit perp warehouse does not carry these spot closes, so they are the documented N/A reason on Bybit.
_STABLECOIN_SPOT_TICKERS = ("FDUSDUSDT", "TUSDUSDT", "USDPUSDT")


class _FeatureStore(Protocol):
    """Just the panel reads the edge engines need (TimescaleFeatureStore satisfies this). READ-ONLY."""

    def crypto_close_panel(self, *, suffix: str = "USDT") -> dict[str, dict[int, float]]: ...
    def crypto_funding_panel(self, *, suffix: str = "USDT") -> dict[str, dict[int, float]]: ...
    def crypto_spot_panel(self, *, suffix: str = "USDT") -> dict[str, dict[int, float]]: ...
    def read_panel(self, symbols: list[str], feature: str) -> dict[str, dict[int, float]]: ...


def _na(reason: str) -> dict[str, Any]:
    """A zero-metric dict flagged unavailable with an explicit reason (the honest "N/A (reason)")."""
    return {"cagr": 0.0, "sharpe": 0.0, "max_dd": 0.0, "total_return": 0.0, "ann_return": 0.0,
            "days": 0, "symbols": 0, "first_day": None, "last_day": None,
            "available": False, "reason": reason}


def _metrics_from_run(dates: list[int], returns: "np.ndarray", *, symbols: int) -> dict[str, Any]:
    """Wrap a sleeve runner's (dates, returns) into the standard metric dict via the SAME `_curve_metrics`
    the carry template uses, then tag it available. `dates` are epoch-days each return is realized on."""
    daily = list(zip([int(d) for d in dates], [float(r) for r in returns]))
    meta = {
        "symbols": symbols,
        "first_day": (dt.date(1970, 1, 1) + dt.timedelta(days=daily[0][0])).isoformat() if daily else None,
        "last_day": (dt.date(1970, 1, 1) + dt.timedelta(days=daily[-1][0])).isoformat() if daily else None,
    }
    m = _curve_metrics(daily, days_meta=meta)
    m["available"] = True
    return m


def _restrict(panel: dict[str, dict[int, float]],
              universe: set[str] | None) -> dict[str, dict[int, float]]:
    """Restrict a {sym: series} panel to `universe` (None = unchanged). The DOMINANT de-artifacting knob,
    mirroring the carry liquid-subset discipline."""
    if universe is None:
        return panel
    return {s: v for s, v in panel.items() if s in universe}


def _tstrend_metrics(store: _FeatureStore, *, universe: set[str] | None,
                     cost_bps: float) -> dict[str, Any]:
    """crypto_tstrend metrics off the store's close panel, reusing the live sleeve's EXACT return engine
    (`TrendRunner(..., long_short=False).run()`). `cost_bps` is passed straight to the runner's own
    turnover cost model (the same one the live sleeve uses). `universe` restricts the symbol set."""
    from fxhnt.application.trend_runner import TrendRunner

    panel = _restrict(store.crypto_close_panel(), universe)
    if not panel:
        return _na("no crypto close panel on this warehouse")
    # charge the perp funding a LONG trend book pays (longs pay positive funding) — the honest deploy cost.
    funding = _restrict(store.crypto_funding_panel(), universe)
    res = TrendRunner(panel, long_short=False, cost_bps=cost_bps, funding=funding).run()
    if res.returns.size == 0:
        return _na("tstrend produced no booked returns (insufficient history)")
    return _metrics_from_run(res.dates, res.returns, symbols=len(panel))


def _unlock_metrics(store: _FeatureStore, *, universe: set[str] | None, cost_bps: float,
                    unlock_events: list[dict[str, Any]] | None,
                    load_unlock_events: Callable[[], list[dict[str, Any]]] | None) -> dict[str, Any]:
    """unlock (token-unlock dilution shorts) metrics, reusing the live sleeve's own `UnlockShortRunner`
    over the store's close panel adapted via `warehouse_unlock_panel`. The unlock CALENDAR is external
    (DefiLlama emissions) and is INJECTED: `unlock_events` (explicit list) takes priority; else
    `load_unlock_events()` is invoked (the cluster passes a real loader). With NEITHER, the edge is
    genuinely uncomputable → N/A (no calendar)."""
    from fxhnt.application.unlock_runner import UnlockShortRunner, warehouse_unlock_panel

    events = unlock_events
    if events is None and load_unlock_events is not None:
        events = load_unlock_events()
    if not events:
        return _na("no unlock calendar (DefiLlama emissions) — inject unlock_events / a loader")

    panel = _restrict(store.crypto_close_panel(), universe)
    if not panel:
        return _na("no crypto close panel on this warehouse")
    vol_panel = warehouse_unlock_panel(panel)
    res = UnlockShortRunner(vol_panel, events).run()
    if res.returns.size == 0:
        return _na("no unlock-window days in the panel for the given calendar")
    return _metrics_from_run(res.dates, res.returns, symbols=len(panel))


def _stablecoin_metrics(store: _FeatureStore, *, cost_bps: float,
                        stablecoin_spot_panel: dict[str, dict[int, float]] | None) -> dict[str, Any]:
    """stablecoin_rotation (peg-reversion) metrics, reusing the live `StableReversionRunner`. Its input is a
    STABLECOIN SPOT close panel; the Bybit perp warehouse does NOT carry it. `stablecoin_spot_panel` is
    purely INJECTED (no live spot-panel loader is wired — the CLI always passes None, Phase 0b Task 7c
    retired the vestigial legacy-venue-spot feed) — without it the edge is N/A on this warehouse."""
    from fxhnt.application.stablecoin_runner import StableReversionRunner

    if not stablecoin_spot_panel:
        return _na(f"warehouse has no stablecoin spot panel ({', '.join(_STABLECOIN_SPOT_TICKERS)}); "
                   "inject stablecoin_spot_panel (no live loader wired)")
    res = StableReversionRunner(stablecoin_spot_panel).run()
    if res.returns.size == 0:
        return _na("stablecoin runner produced no booked returns")
    return _metrics_from_run(res.dates, res.returns, symbols=len(stablecoin_spot_panel))


def edge_metrics_from_store(store: _FeatureStore, edge: str, *, universe: set[str] | None = None,
                            cost_bps: float = 0.0,
                            unlock_events: list[dict[str, Any]] | None = None,
                            load_unlock_events: Callable[[], list[dict[str, Any]]] | None = None,
                            stablecoin_spot_panel: dict[str, dict[int, float]] | None = None,
                            ) -> dict[str, Any]:
    """READ-ONLY metrics for ONE edge on `store`. Same return shape as `carry_metrics_from_store`, plus
    `available` (and `reason` when N/A).

    `edge` ∈ {tstrend, unlock, stablecoin_rotation, xsfunding}. `universe` restricts the crypto symbol set
    (the de-artifacting liquid-subset discipline). `cost_bps` is the per-edge turnover cost. External inputs
    (unlock calendar, stablecoin spot panel) are INJECTED so this stays pure + network-free; a genuinely
    uncomputable edge (missing a required input) is reported N/A (reason), never fabricated.

    This function NEVER writes — there is no PaperRepo and no `store.write_*` call; the live paper_* tables
    are untouched."""
    if edge == "xsfunding":
        m = carry_metrics_from_store(store, cost_bps=cost_bps, universe=universe)
        m["available"] = True   # carry is always computable from a (possibly-empty) store
        return m
    if edge == "tstrend":
        return _tstrend_metrics(store, universe=universe, cost_bps=cost_bps)
    if edge == "unlock":
        return _unlock_metrics(store, universe=universe, cost_bps=cost_bps,
                               unlock_events=unlock_events, load_unlock_events=load_unlock_events)
    if edge == "stablecoin_rotation":
        return _stablecoin_metrics(store, cost_bps=cost_bps,
                                   stablecoin_spot_panel=stablecoin_spot_panel)
    raise ValueError(f"unknown edge {edge!r} (known: {', '.join(EDGE_NAMES)})")


def _equal_weight_combined(per_edge: dict[str, dict[str, Any]]) -> dict[str, Any]:
    """An EQUAL-WEIGHT combined-book metric over the edges that ARE available, rebuilt from each edge's
    daily-return series re-derived from its own runner is not retained here — so we combine on the metric
    level: we re-run nothing. Instead we build a simple equal-weight daily blend from the per-edge curves
    that carry days; if fewer than two edges are available the combined is just the single available edge's
    metrics (or N/A when none). Pure."""
    avail = {e: m for e, m in per_edge.items() if m.get("available") and m.get("days", 0) > 0}
    if not avail:
        return _na("no available edge to combine")
    # We don't retain per-day series in the metric dicts, so the honest "combined" here is the equal-weight
    # average of the per-edge SUMMARY metrics across available edges — a transparent, cheap blend (NOT a
    # re-simulated portfolio). Documented so the number isn't mistaken for a vol-targeted book.
    keys = ("cagr", "sharpe", "max_dd", "total_return", "ann_return")
    blended = {k: float(np.mean([avail[e][k] for e in avail])) for k in keys}
    blended["days"] = max(avail[e]["days"] for e in avail)
    blended["symbols"] = sum(avail[e]["symbols"] for e in avail)
    blended["first_day"] = min((avail[e]["first_day"] for e in avail if avail[e]["first_day"]),
                               default=None)
    blended["last_day"] = max((avail[e]["last_day"] for e in avail if avail[e]["last_day"]),
                              default=None)
    blended["available"] = True
    blended["combined_edges"] = sorted(avail)
    blended["combine_method"] = "equal-weight average of available per-edge summary metrics"
    return blended


def evaluate_all_edges(store: _FeatureStore, *, universe: set[str] | None = None,
                       cost_bps: float = 5.5,
                       unlock_events: list[dict[str, Any]] | None = None,
                       load_unlock_events: Callable[[], list[dict[str, Any]]] | None = None,
                       stablecoin_spot_panel: dict[str, dict[int, float]] | None = None,
                       ) -> dict[str, dict[str, Any]]:
    """READ-ONLY evaluate EVERY edge on `store` and return {edge: metrics, ..., "combined": metrics}.

    Each per-edge value is the `edge_metrics_from_store` result (with `available`/`reason`). The "combined"
    entry is an equal-weight blend over the AVAILABLE edges (or N/A when none are available). `universe`
    restricts the crypto symbol set (the liquid-subset discipline); external inputs are injected (so the
    function is pure + network-free). Writes NOTHING."""
    per_edge = {
        "tstrend": edge_metrics_from_store(store, "tstrend", universe=universe, cost_bps=cost_bps),
        "unlock": edge_metrics_from_store(store, "unlock", universe=universe, cost_bps=cost_bps,
                                          unlock_events=unlock_events,
                                          load_unlock_events=load_unlock_events),
        "stablecoin_rotation": edge_metrics_from_store(
            store, "stablecoin_rotation", universe=universe, cost_bps=cost_bps,
            stablecoin_spot_panel=stablecoin_spot_panel),
        "xsfunding": edge_metrics_from_store(store, "xsfunding", universe=universe, cost_bps=cost_bps),
    }
    per_edge["combined"] = _equal_weight_combined(per_edge)
    return per_edge
