"""MEASURED per-coin cost for the cockpit Backtest's single-book views (Bybit is the sole crypto venue —
Phase 0b venue consolidation retired the legacy combined-book leg and the legacy-vs-Bybit COMPARE mode
this module used to serve; see the mirage finding below for WHY that compare mattered historically).

    cost_bps(coin) = taker_fee_bps + 0.5 · effective_spread_bps(coin)

`taker_fee_bps` is the PUBLISHED perp taker fee; `effective_spread_bps(coin)` is the coin's MEASURED
Corwin–Schultz (2012) high-low spread (`bybit_liquidity_sweep.corwin_schultz_spread_bps`, median per coin,
negatives clamped to 0); you cross HALF per trade. NO assumed market-impact term — so the cost (and the
de-inflation) is CAPITAL-ROBUST in bps.

HISTORICAL FINDING (why per-coin measured cost, not a flat slider): the retired compare showed the flat
`cost_bps` FLATTERED the legacy-venue broad/illiquid combined book relative to the deep/liquid Bybit 4-edge
book — the legacy venue's real per-coin spread was far wider than a flat assumption gave it credit for
(the "measured cost mirage" — see `pearl_fxhnt_measured_cost_mirage_and_postgres`). Bybit's liquid book
holds up under the SAME honest per-coin treatment, which is why it survives as fxhnt's sole crypto venue.
This module now only reuses `per_coin_book_returns` (the Bybit per-symbol weights of each edge, eq-wt-
combined) for that honest per-coin measured curve. READ-ONLY: only repo + warehouse READS; writes nothing.
"""
from __future__ import annotations

import statistics as st
from typing import Any

from fxhnt.adapters.persistence.paper_repo import PaperRepo
from fxhnt.application.bybit_liquidity_sweep import _TAKER_FEE_BPS, per_coin_book_returns
from fxhnt.application.paper_sim import _metrics as _sim_metrics

_PERIODS_PER_YEAR = 365


# --- the per-book measured-cost net return + metrics ---------------------------------------------

def _bybit_measured_net(store: Any, *, capital: float, win_lo: int | None, win_hi: int | None,
                        taker_fee_bps: float, sleeves: list[str] | None = None,
                        universe: set[str] | None = None,
                        unlock_events: list[dict[str, Any]] | None = None,
                        real_spread_by_coin: dict[str, float] | None = None,
                        leverage_shape: dict[str, float] | None = None,
                        max_gross: float = 1.5) -> dict[str, Any]:
    """The Bybit 4-edge naive-eqwt book's per-COIN MEASURED-cost equity curve + metrics over [win_lo, win_hi].

    Reuses `bybit_liquidity_sweep.per_coin_book_returns` — the per-symbol weights of each Bybit edge
    (`bybit_paper_weights`) eq-wt-combined into a per-coin book, charged a per-COIN cost of the published
    5.5bp taker fee + half each coin's MEASURED Corwin–Schultz spread on `bybit_features` high/low (NO
    assumed impact). The GROSS book return is unchanged; only the per-day turnover haircut is the measured
    per-coin one. `store` MUST be a `bybit_features` feature store; `universe` bounds the read to the liquid
    subset; `unlock_events` feeds the unlock sleeve (absent → that sleeve is simply omitted). READ-ONLY.

    `real_spread_by_coin` (optional): when supplied, the REAL live L1 quoted spread REPLACES the high/low
    Corwin–Schultz estimate per coin in the cost model (a coin with NO live quote falls back to its CS
    estimate). This is the forward-realistic cost — the daily-high/low CS proxy over-states the true quoted
    spread ~104× on the liquid book (1.4bp real vs 143.6bp CS), so the override de-inflates the cost drag and
    lifts the net Sharpe. The GROSS series and per-day per-coin turnover are UNCHANGED — only the per-bp cost
    model's spread input changes. Reuses the maker-recost override plumbing (`recost_net_series` at the
    all-taker operating point f=0, which is byte-identical to the default cost when no override is given)."""
    import datetime as dt    # local import (matches this module's pattern); used for the epoch-day→ordinal fix
    use_real = real_spread_by_coin is not None
    pc = per_coin_book_returns(store, universe=universe, sleeves=sleeves, capital=capital,
                               taker_fee_bps=taker_fee_bps, unlock_events=unlock_events or [],
                               return_detail=use_real, leverage_shape=leverage_shape, max_gross=max_gross)
    if use_real and pc.get("available"):
        # Re-cost the SAME book (fixed gross + turnover) with the REAL spread via the maker-recost override at
        # the all-taker operating point (f=0 ⇒ blended == taker_fee + 0.5·spread, the exact measured formula).
        from fxhnt.application.bybit_maker_recost import recost_net_series
        rc = recost_net_series(pc, fill_fraction=0.0, adverse_sel=0.0, taker_fee_bps=taker_fee_bps,
                               real_spread_by_coin=real_spread_by_coin)
        net = rc["net_series"]
        cost_drag_bps = rc["cost_drag_bps"]
        cs_by_coin: dict[str, float] = pc.get("spread_by_coin", {})
        eff = [real_spread_by_coin.get(c, cs_by_coin.get(c, 0.0)) for c in cs_by_coin]  # type: ignore[union-attr]
        median_spread_bps = float(st.median(eff)) if eff else pc.get("median_spread_bps", 0.0)
        used_real_spread = True
    else:
        net = pc.get("net_series", {})
        cost_drag_bps = pc.get("avg_cost_drag_bps", 0.0)
        median_spread_bps = pc.get("median_spread_bps", 0.0)
        used_real_spread = False
    days = sorted(d for d in net
                  if (win_lo is None or d >= win_lo) and (win_hi is None or d <= win_hi))

    equity = capital
    peak = capital
    dates: list[int] = []
    equity_curve: list[float] = []
    drawdown: list[float] = []
    net_rets: list[float] = []
    n_coins = pc.get("n_coins", 0)
    for d in days:
        r = net[d]
        equity *= (1.0 + r)
        peak = max(peak, equity)
        dd = (equity - peak) / peak if peak > 0 else 0.0
        # `d` is a Unix epoch-day (the `bybit_features` read_panel key, ~20627), but the cockpit renders dates
        # via date.fromordinal — which would turn ~20627 into year 0057. Convert to a proleptic ordinal so the
        # backtest shows 2021-2026 (and the worst/best-YEAR metrics bucket correctly). `bybit_sleeve_ret`
        # already stores toordinal, so ONLY this per-coin measured path needed the conversion. Keep `d` itself
        # (above) for the net[d] lookup.
        dates.append((dt.date(1970, 1, 1) + dt.timedelta(days=d)).toordinal())
        equity_curve.append(equity)
        drawdown.append(dd)
        net_rets.append(r)

    metrics = _sim_metrics(dates, equity_curve, net_rets, drawdown, capital)
    return {"dates": dates, "equity": equity_curve, "drawdown": drawdown,
            "leverage": [1.0] * len(dates), "killed": [False] * len(dates),
            "n_active": [n_coins] * len(dates), "metrics": metrics,
            "median_spread_bps": median_spread_bps,
            "cost_drag_bps": cost_drag_bps, "used_real_spread": used_real_spread}


# --- single-book measured curves (the Backtest's DEFAULT cost mode) -------------------------------
# The Backtest's single-book views (bybit_4edge, bybit_4edge_levered) DEFAULT to a per-COIN MEASURED cost
# so each shows its honest de-inflated number, consistently — not a flat-cost slider mirage with a one-off
# "honest caption". The heavy per-coin compute runs in an own-memory precompute Job (it OOM-killed the 4Gi
# dashboard live); the cockpit ONLY reads these small render-ready artifacts and falls back to the flat
# slider when absent.

SINGLE_CACHE_KEYS = {
    "bybit_4edge": "single_bybit_4edge_measured",
    "bybit_4edge_levered": "single_bybit_4edge_levered_measured",
}


def _single_book_payload(book: str, view: dict[str, Any], *, capital: float,
                         taker_fee_bps: float) -> dict[str, Any]:
    """The small render-ready single-book artifact: the per-day arrays the cockpit slider/curve consume
    (dates as ISO strings, equity/drawdown/leverage/killed/n_active) + the metrics + the measured-cost
    audit fields + the start capital the curve was compounded from (so a different capital rescales it)."""
    import datetime as dt

    iso = [dt.date.fromordinal(d).isoformat() for d in view["dates"]]
    return {
        "book": book,
        "dates_iso": iso,
        "equity": view["equity"],
        "drawdown": view["drawdown"],
        "leverage": view["leverage"],
        "killed": view["killed"],
        "n_active": view["n_active"],
        "metrics": view["metrics"],
        "median_spread_bps": view["median_spread_bps"],
        "cost_drag_bps": view["cost_drag_bps"],
        "used_real_spread": bool(view.get("used_real_spread", False)),
        "capital": capital,
        "taker_fee_bps": taker_fee_bps,
    }


def precompute_single_book_measured(repo: PaperRepo, bybit_store: Any, *, capital: float,
                                    taker_fee_bps: float = _TAKER_FEE_BPS, at: Any,
                                    bybit_sleeves: list[str] | None = None,
                                    bybit_universe: set[str] | None = None,
                                    unlock_events: list[dict[str, Any]] | None = None,
                                    bybit_real_spread_by_coin: dict[str, float] | None = None
                                    ) -> dict[str, dict[str, Any]]:
    """OWN-MEMORY WRITER: run the (heavy) per-COIN MEASURED-cost curve for the Bybit 4-edge book (naive +
    levered) and idempotently persist a small render-ready payload per book into `compare_measured_cache`
    (keyed by `SINGLE_CACHE_KEYS[book]`). Returns `{book: payload}`. READ-ONLY against the warehouse (only
    the cache rows are written, via the repo).

    `bybit_real_spread_by_coin` (optional): when supplied, both curves are costed with the REAL live L1
    quoted spread (CS fallback for unquoted coins) — the forward-realistic cost."""
    out: dict[str, dict[str, Any]] = {}

    by_view = _bybit_measured_net(bybit_store, capital=capital, win_lo=None, win_hi=None,
                                  taker_fee_bps=taker_fee_bps, sleeves=bybit_sleeves,
                                  universe=bybit_universe, unlock_events=unlock_events,
                                  real_spread_by_coin=bybit_real_spread_by_coin)
    if by_view["dates"]:
        by_payload = _single_book_payload("bybit_4edge", by_view, capital=capital,
                                          taker_fee_bps=taker_fee_bps)
        repo.upsert_compare_measured_cache(SINGLE_CACHE_KEYS["bybit_4edge"], by_payload, at=at)
        out["bybit_4edge"] = by_payload

    # The LEVERED shadow book: the SAME measured per-coin cost model + universe, but the sleeves are
    # weighted by the static leverage shape (carry-heavy, book-gross <= 1.5x) instead of naive eq-wt — so
    # the cockpit can show the levered curve at the honest measured cost (never a flat/cost-blind curve).
    from fxhnt.application.bybit_forward_track import _LEVERED_SLEEVE_SHAPE, MAX_BOOK_GROSS
    lev_view = _bybit_measured_net(bybit_store, capital=capital, win_lo=None, win_hi=None,
                                   taker_fee_bps=taker_fee_bps, sleeves=bybit_sleeves,
                                   universe=bybit_universe, unlock_events=unlock_events,
                                   real_spread_by_coin=bybit_real_spread_by_coin,
                                   leverage_shape=_LEVERED_SLEEVE_SHAPE, max_gross=MAX_BOOK_GROSS)
    if lev_view["dates"]:
        lev_payload = _single_book_payload("bybit_4edge_levered", lev_view, capital=capital,
                                           taker_fee_bps=taker_fee_bps)
        repo.upsert_compare_measured_cache(SINGLE_CACHE_KEYS["bybit_4edge_levered"], lev_payload, at=at)
        out["bybit_4edge_levered"] = lev_payload

    return out


def read_cached_single_measured(repo: PaperRepo, book: str) -> dict[str, Any] | None:
    """The cockpit's single-book measured read: the precomputed render-ready payload for `book`, or None
    when absent / unknown (→ the Backtest falls back to the flat-cost slider for that book, never a 500)."""
    key = SINGLE_CACHE_KEYS.get(book)
    if key is None:
        return None
    hit = repo.read_compare_measured_cache(key)
    if hit is None:
        return None
    payload, _computed_at = hit
    return payload
