"""READ-ONLY Phase-1 evaluator: does stablecoin peg-reversion transfer to BYBIT's OWN stablecoin spot pairs?

The legacy venue's `stablecoin_rotation` sleeve shorts rich (above-peg) stablecoins on a legacy-venue SPOT
panel of FDUSDUSDT/TUSDUSDT/USDPUSDT — pairs Bybit does NOT list. But Bybit DOES list genuine near-$1 stablecoin spot
pairs (USDCUSDT, USDEUSDT [Ethena], TUSDT). This module asks the cheap Phase-1 question: does the SAME
validated peg-reversion edge (`StableReversionRunner`, unchanged) produce a positive curve on Bybit's actual
stablecoins, using the `spot_close` data ALREADY in `bybit_features` — BEFORE committing to ingest
USDC-margined perps (Phase 2)?

HOW THE EDGE LOGIC IS REUSED (nothing reinvented):
  * `bybit_stable_spot_panel(store)` auto-detects which symbols in the warehouse are TRUE near-$1
    stablecoins — ones that STAY near peg (tiny dispersion), not merely AVERAGE ~$1: median `spot_close` ∈
    [0.98, 1.02] AND >=90% of daily closes within ±5% of $1 AND >=60 days of history — and returns a
    `{sym: {epoch_day: close}}` panel, the EXACT shape `StableReversionRunner` consumes. (A median-only
    filter wrongly admitted volatile coins averaging ~$1 like SUSHI/THETA/TWT/VIRTUAL → bogus −90% DD.)
  * `stablecoin_reversion_metrics_from_store` feeds that panel straight into `StableReversionRunner(panel)`
    — the live sleeve's own runner, UNCHANGED (same short-rich logic, same threshold, same turnover cost) —
    and wraps its (dates, returns) via the shared `_curve_metrics` (the carry/edges template).

MEMORY-BOUNDED auto-detect: a stablecoin symbol can't be known a-priori, but the full `spot_close` table is
~hundreds of coins. We do a LIGHT pass — `spot_close_peg_stats(store)` reads only `(symbol, value)` (no ts),
streams per-symbol `(median, frac_within_5pct, n)`, and selects the names that STAY near $1 — then read ONLY
those full series via `read_panel(stable_syms, "spot_close")`. The whole multi-day panel is never
materialised for non-stables.

READ-ONLY: there is no PaperRepo and no `store.write_*` / `paper_*` write is ever issued. Both the median
scan and the panel read are SELECTs; `StableReversionRunner` is pure. A write-tripwire store in the tests
proves no write method is touched.

HONESTY CAVEATS (surfaced in the CLI output):
  * Daily-close spot captures only SUSTAINED dislocations. Sharp intraday venue-dislocations (e.g. USDE
    traded $0.65 on 2025-10-10 and reverted within hours) are INVISIBLE at the daily close → this is a
    CONSERVATIVE LOWER BOUND on the edge. Intraday reversion (needs 1m spot) is Phase-2+.
  * The number of near-$1 pairs is reported explicitly. ~2-3 pairs is THIN — this Phase-1 result tells us
    whether to invest in Phase-2 breadth (USDC-margined perps), NOT a deployable book on its own.
"""
from __future__ import annotations

from typing import Any, Protocol

from fxhnt.application.bybit_carry_revalidate import _curve_metrics

# A stablecoin is defined by STAYING near $1 (tiny dispersion), NOT merely by AVERAGING ~$1. A loose
# median-only filter admits VOLATILE coins that happen to average ~$1 (SUSHI/THETA/TWT/VIRTUAL all wander
# $0.5–$5 but median ≈ $1) — treating those as pegged ("short when rich") produced a bogus −90% DD. So a
# symbol qualifies ONLY if it is BOTH centered at peg AND stays there:
#   1. median(spot_close) ∈ [STABLE_MEDIAN_LO, STABLE_MEDIAN_HI]  (centered at the $1 peg), AND
#   2. the FRACTION of days with spot_close ∈ [1±STABLE_PEG_BAND] is >= STABLE_MIN_FRAC_IN_BAND
#      (i.e. >=90% of observations within ±5% of $1 — it STAYS near peg), AND
#   3. n >= STABLE_MIN_HISTORY days (a brand-new coin with a few near-$1 prints isn't admitted).
# This keeps USDC/USDE/BUSD/RLUSD/USD1 (near $1 ~always; brief depegs like USDE's intraday event are
# invisible at the daily close anyway) while EXCLUDING SUSHI/THETA/TWT/VIRTUAL, gold (PAXG/XAUT ~3000),
# and dead pegs (USTC ~0.02).
STABLE_MEDIAN_LO = 0.98
STABLE_MEDIAN_HI = 1.02
STABLE_PEG_BAND = 0.05          # half-width of the ±$1 "stays near peg" band
STABLE_MIN_FRAC_IN_BAND = 0.90  # >=90% of observations must lie within ±5% of $1
STABLE_MIN_HISTORY = 60         # require at least this many daily observations


class _FeatureStore(Protocol):
    """Just the reads this evaluator needs (TimescaleFeatureStore satisfies it). READ-ONLY."""

    def read_panel(self, symbols: list[str], feature: str) -> dict[str, dict[int, float]]: ...
    def spot_close_peg_stats(
        self, *, suffix: str = "USDT", band: float = STABLE_PEG_BAND
    ) -> dict[str, tuple[float, float, int]]: ...


def _is_stablecoin(median: float, frac_in_band: float, n: int) -> bool:
    """A symbol qualifies as a stablecoin only if it is centered at peg AND stays there AND has history."""
    return (
        STABLE_MEDIAN_LO <= median <= STABLE_MEDIAN_HI
        and frac_in_band >= STABLE_MIN_FRAC_IN_BAND
        and n >= STABLE_MIN_HISTORY
    )


def bybit_stable_spot_panel(store: _FeatureStore, *, suffix: str = "USDT") -> dict[str, dict[int, float]]:
    """Auto-detect Bybit's near-$1 stablecoin spot pairs and return the panel `StableReversionRunner` wants.

    Step 1 (LIGHT, memory-bounded): `store.spot_close_peg_stats()` streams per-symbol `(median,
    frac_within_5pct, n)` WITHOUT materialising the multi-day panel; we keep ONLY symbols that BOTH center at
    peg (median ∈ [0.98, 1.02]) AND stay there (>=90% of observations within ±5% of $1) AND have >=60 days of
    history. This excludes VOLATILE coins that merely average ~$1 (SUSHI/THETA/TWT/VIRTUAL), gold (~3000),
    dead pegs (~0.02), and brand-new coins — keeping real pegs (USDC/USDE/BUSD/RLUSD/USD1).
    Step 2: read ONLY those qualifying symbols' full series via `read_panel(near, "spot_close")` — so the
    heavy panel materialises for a HANDFUL of stablecoins, never the whole table.

    Returns `{symbol: {epoch_day: spot_close}}` (empty {} dropped). READ-ONLY (two SELECTs)."""
    stats = store.spot_close_peg_stats(suffix=suffix, band=STABLE_PEG_BAND)
    near = sorted(s for s, (med, frac, n) in stats.items() if _is_stablecoin(med, frac, n))
    if not near:
        return {}
    panel = store.read_panel(near, "spot_close")
    return {s: v for s, v in panel.items() if v}


def stablecoin_reversion_metrics_from_store(
    store: _FeatureStore, *, cost_bps: float = 5.5, suffix: str = "USDT",
) -> dict[str, Any]:
    """READ-ONLY peg-reversion metrics on BYBIT's OWN near-$1 stablecoin spot pairs.

    Builds the Bybit stable panel via `bybit_stable_spot_panel`, runs the live `StableReversionRunner`
    UNCHANGED on it (same short-rich edge), and returns the standard metric dict via the shared
    `_curve_metrics`: `{cagr, sharpe, max_dd, total_return, ann_return, days, symbols, first_day, last_day,
    pairs, available}` (+ `reason` when not computable). `cost_bps` is the runner's own turnover cost.

    Writes NOTHING — there is no PaperRepo and no `store.write_*` call."""
    from fxhnt.application.stablecoin_runner import StableReversionRunner

    panel = bybit_stable_spot_panel(store, suffix=suffix)
    pairs = sorted(panel)
    if not panel:
        return _na("no near-$1 stablecoin spot pair in bybit_features (median spot_close in "
                   f"[{STABLE_MEDIAN_LO}, {STABLE_MEDIAN_HI}] AND >={STABLE_MIN_FRAC_IN_BAND:.0%} of days "
                   f"within +-{STABLE_PEG_BAND:.0%} of $1 over >={STABLE_MIN_HISTORY} days); "
                   "ingest spot_close first", pairs)

    res = StableReversionRunner(panel, cost_bps=cost_bps).run()
    if res.returns.size == 0:
        return _na("near-$1 stable panel has no rich-name reversion days (no sustained above-peg "
                   "dislocation at daily close)", pairs)

    m = _wrap(res.dates, res.returns, symbols=len(panel))
    m["pairs"] = pairs
    return m


def _na(reason: str, pairs: list[str]) -> dict[str, Any]:
    """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": len(pairs), "first_day": None, "last_day": None,
            "pairs": pairs, "available": False, "reason": reason}


def _wrap(dates: list[int], returns: Any, *, symbols: int) -> dict[str, Any]:
    """Wrap a runner's (dates, returns) into the standard metric dict via the SAME `_curve_metrics` the
    carry/edges templates use. `dates` are the epoch-day each return is realized on."""
    import datetime as dt

    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
