"""Maker-vs-taker EXECUTION re-costing of the Bybit 4-edge deploy book — a what-if showing the net-Sharpe
lift from filling rebalances PASSIVELY (maker) instead of crossing the spread (taker). READ-ONLY,
memory-bounded.

WHY
---
Cost is the proven dominant lever for this book (the measured-cost re-audit showed the legacy combined book
was a mirage and the Bybit liquid-61 the real edge). The live book is charged ALL-TAKER: Bybit's published
5.5bp taker fee + half each coin's Corwin–Schultz MEASURED spread per unit of turnover (it crosses the
spread). But Bybit's non-VIP MAKER fee is 2bp, and a maker (passive limit) order does NOT cross the spread.
A daily-rebalanced liquid-61 book can work much of its turnover passively, so this harness QUANTIFIES the net
lift — honestly, with adverse-selection + partial-fill conservatism.

THIS IS NOT A SIGNAL CHANGE AND NOT A FIT
-----------------------------------------
The GROSS book return is UNCHANGED — every scenario reuses the IDENTICAL gross series and the IDENTICAL
per-day per-coin turnover from `bybit_liquidity_sweep.per_coin_book_returns` (no edge re-derivation, no
re-weighting). The ONLY thing that varies is the per-turnover COST model. So there is nothing to overfit;
this is a structural execution what-if, not a parameter search.

THE COST MODEL (the only thing that changes)
--------------------------------------------
Per rebalance, per coin, the turnover |Δw| is split by a maker FILL FRACTION `f`:
  * TAKER leg (fraction 1−f): cost_bps = taker_fee_bps(5.5) + 0.5·spread_bps(coin)   ← crosses the spread
    (the CURRENT live model).
  * MAKER leg (fraction f):  cost_bps = maker_fee_bps(2.0) + adverse_sel·spread_bps(coin)   ← does NOT cross
    the spread; `adverse_sel ∈ [0, 0.5]` models adverse selection:
        0    = maker earns the spread / optimistic;
        0.25 = pays a quarter-spread to adverse selection / conservative;
        0.5  = no better than taker on the spread (only the fee differs).
  Blended per-coin cost = f·maker + (1−f)·taker. Daily NET = GROSS − Σ_coin |Δw|·blended_cost/1e4; compound;
  metrics (same `paper_sim._metrics` the live deploy number uses).

BASELINE TIE-OUT (no drift)
---------------------------
`f = 0` ⇒ blended = taker for every coin ⇒ the net series is BYTE-IDENTICAL to the live all-taker
`per_coin_book_returns`/`_bybit_measured_net` net. So `f=0` reproduces the currently-deployed number EXACTLY
— the sanity tie-out.

REUSE: `bybit_liquidity_sweep.per_coin_book_returns(..., return_detail=True)` for the gross + per-coin
spread + per-day per-coin turnover; `paper_sim._metrics` for Sharpe/CAGR/maxDD. READ-ONLY: no PaperRepo, no
`store.write_*` (a WriteTripwire test proves it); memory-bounded by `universe`. Does NOT touch the live book
or any edge's returns.
"""
from __future__ import annotations

import datetime as dt
import statistics as st
from collections.abc import Iterable
from typing import Any, Protocol

from fxhnt.application.bybit_liquidity_sweep import _TAKER_FEE_BPS, per_coin_book_returns
from fxhnt.application.paper_sim import _metrics as _sim_metrics

# Bybit's PUBLISHED non-VIP perp MAKER fee (0.02% = 2.0 bps). A KNOWN exchange fee, not an assumption — the
# rebate-free maker fee every passive limit order pays. The taker fee floor lives in bybit_liquidity_sweep.
_MAKER_FEE_BPS = 2.0

# The sweep grid. f=0.0 is the current ALL-TAKER baseline; 1.0 is fully passive. adverse_sel 0.0 = optimistic
# (maker earns the spread), 0.25 = conservative (pays a quarter-spread to adverse selection).
_FILL_FRACTIONS: tuple[float, ...] = (0.0, 0.3, 0.5, 0.7, 1.0)
_ADVERSE_SELS: tuple[float, ...] = (0.0, 0.25)

# The realistic operating point called out in the report: half-to-most of the flow worked passively, paying
# a quarter-spread to adverse selection.
_OPERATING_FILL_FRACTION = 0.6
_OPERATING_ADVERSE_SEL = 0.25


class _FeatureStore(Protocol):
    def read_panel(self, symbols: list[str], feature: str) -> dict[str, dict[int, float]]: ...


# --- THE COST MODEL (the only thing that changes) ------------------------------------------------

def blended_cost_bps(*, spread_bps: float, fill_fraction: float, adverse_sel: float,
                     taker_fee_bps: float = _TAKER_FEE_BPS,
                     maker_fee_bps: float = _MAKER_FEE_BPS) -> float:
    """The blended per-coin per-turnover cost (bps) when a fraction `f` of the rebalance fills PASSIVELY:

        taker_leg  = taker_fee_bps + 0.5·spread        (crosses the spread — current live model)
        maker_leg  = maker_fee_bps + adverse_sel·spread (does NOT cross; adverse_sel ∈ [0, 0.5])
        blended    = (1−f)·taker_leg + f·maker_leg

    `f=0` ⇒ blended == the live all-taker cost (taker_fee + half the measured spread), the EXACT baseline.
    A negative measured spread is clamped to 0 (cannot pay you to trade). Pure / deterministic; no
    capital / impact term (the cost is per-bp, so the lift is capital-robust)."""
    sp = max(0.0, spread_bps)
    f = fill_fraction
    taker_leg = taker_fee_bps + 0.5 * sp
    maker_leg = maker_fee_bps + adverse_sel * sp
    return float((1.0 - f) * taker_leg + f * maker_leg)


def recost_net_series(detail: dict[str, Any], *, fill_fraction: float, adverse_sel: float,
                      taker_fee_bps: float = _TAKER_FEE_BPS,
                      maker_fee_bps: float = _MAKER_FEE_BPS,
                      real_spread_by_coin: dict[str, float] | None = None) -> dict[str, Any]:
    """Re-cost the SAME book under a maker fill fraction. `detail` is a `per_coin_book_returns(...,
    return_detail=True)` result (the FIXED gross series + per-coin spread + per-day per-coin turnover).

    NET[d] = GROSS[d] − Σ_coin |Δw|·blended_cost(coin)/1e4. The GROSS is untouched (this is execution-only):
    `net[d] + day_cost[d] == gross[d]` for every scenario. Returns `{net_series, day_cost, cost_drag_bps}`
    where `cost_drag_bps` is the turnover-weighted average blended cost. `f=0` ⇒ `net_series` is identical to
    `detail['net_series']` (the live all-taker number).

    `real_spread_by_coin` (optional): when supplied, the REAL live L1 quoted spread REPLACES the high/low
    Corwin–Schultz estimate (`detail['spread_by_coin']`) in BOTH legs of the cost model, per coin. A coin with
    NO live quote falls back to its CS estimate. `f=0` + a real spread still ties out to the exact cost
    formula `taker_fee + 0.5·real_spread(coin)` — the same blended_cost_bps, only the spread input changes."""
    gross: dict[int, float] = detail["gross_series"]
    cs_spread: dict[str, float] = detail["spread_by_coin"]
    turnover: dict[int, dict[str, float]] = detail["daily_turnover"]

    def _eff_spread(coin: str) -> float:
        # REAL live quote when we have one, else fall back to the CS high/low estimate (note how many fall back
        # is reported by the sweep via `compare_spreads`).
        if real_spread_by_coin is not None and coin in real_spread_by_coin:
            return real_spread_by_coin[coin]
        return cs_spread.get(coin, 0.0)

    net_series: dict[int, float] = {}
    day_cost_series: dict[int, float] = {}
    cost_bps_weighted_sum = 0.0
    traded_w_sum = 0.0
    for d in sorted(gross):
        g = gross[d]
        day_cost = 0.0
        for coin, dw in turnover.get(d, {}).items():
            cbps = blended_cost_bps(spread_bps=_eff_spread(coin), fill_fraction=fill_fraction,
                                    adverse_sel=adverse_sel, taker_fee_bps=taker_fee_bps,
                                    maker_fee_bps=maker_fee_bps)
            day_cost += dw * cbps / 1e4
            cost_bps_weighted_sum += dw * cbps
            traded_w_sum += dw
        net_series[d] = g - day_cost
        day_cost_series[d] = day_cost
    cost_drag_bps = (cost_bps_weighted_sum / traded_w_sum) if traded_w_sum > 0 else 0.0
    return {"net_series": net_series, "day_cost": day_cost_series, "cost_drag_bps": cost_drag_bps}


def compare_spreads(cs_by_coin: dict[str, float], real_by_coin: dict[str, float], *,
                    coins: Iterable[str] | None = None) -> dict[str, Any]:
    """Compare the REAL live L1 quoted spread against the Corwin–Schultz-from-high/low estimate over the book
    coins. Answers "is the 144bp CS proxy real?" — the over-estimate factor is `median CS / median real`.

    `coins` (default = the keys of `cs_by_coin`) restricts the comparison to the book's traded coins. Medians/
    means are over the coins that HAVE the relevant estimate; `n_real` / `n_fallback` count book coins with /
    without a live quote (the fallbacks pay their CS estimate in the re-cost)."""
    book_coins = sorted(coins) if coins is not None else sorted(cs_by_coin)
    cs_vals = [cs_by_coin[c] for c in book_coins if c in cs_by_coin]
    real_vals = [real_by_coin[c] for c in book_coins if c in real_by_coin]
    n_real = sum(1 for c in book_coins if c in real_by_coin)
    median_cs = float(st.median(cs_vals)) if cs_vals else 0.0
    median_real = float(st.median(real_vals)) if real_vals else 0.0
    ratio = (median_cs / median_real) if median_real > 0 else float("inf")
    return {
        "n_coins": len(book_coins),
        "n_real": n_real,
        "n_fallback": len(book_coins) - n_real,
        "median_cs_spread_bps": median_cs,
        "mean_cs_spread_bps": float(st.fmean(cs_vals)) if cs_vals else 0.0,
        "median_real_spread_bps": median_real,
        "mean_real_spread_bps": float(st.fmean(real_vals)) if real_vals else 0.0,
        "cs_over_real_ratio": ratio,
    }


# --- equity-curve metrics (mirrors _bybit_measured_net so the baseline ties to the deployed number) ----

def _curve_metrics(net_series: dict[int, float], *, capital: float) -> dict[str, Any]:
    """Compound a net daily-return series into an equity curve and summarise it with `paper_sim._metrics`
    (the SAME metric the live deploy `_bybit_measured_net` reports — so the f=0 baseline ties out in the same
    space). Epoch-day keys are converted to proleptic ordinals exactly as `_bybit_measured_net` does."""
    days = sorted(net_series)
    equity = capital
    peak = capital
    dates: list[int] = []
    equity_curve: list[float] = []
    drawdown: list[float] = []
    net_rets: list[float] = []
    for d in days:
        r = net_series[d]
        equity *= (1.0 + r)
        peak = max(peak, equity)
        dd = (equity - peak) / peak if peak > 0 else 0.0
        dates.append((dt.date(1970, 1, 1) + dt.timedelta(days=d)).toordinal())
        equity_curve.append(equity)
        drawdown.append(dd)
        net_rets.append(r)
    return _sim_metrics(dates, equity_curve, net_rets, drawdown, capital)


# --- the sweep -----------------------------------------------------------------------------------

def true_spread_report(store: _FeatureStore, *, universe: set[str] | None,
                       real_spread_by_coin: dict[str, float], sleeves: list[str] | None = None,
                       capital: float = 100_000.0, taker_fee_bps: float = _TAKER_FEE_BPS,
                       unlock_events: list[dict[str, Any]] | None = None) -> dict[str, Any]:
    """READ-ONLY Part-A report: the book's REAL median/mean L1 quoted spread vs the median Corwin–Schultz
    high/low estimate, and the over-estimate ratio — restricted to the coins the live book actually trades.

    Builds the per-coin book ONCE (`per_coin_book_returns(..., return_detail=True)`) to get the book's coins +
    their CS spread, then `compare_spreads` against the injected REAL live quotes (`real_spread_by_coin`).
    Answers "is the 144bp CS proxy real?" without touching the live book. Memory-bounded by `universe`."""
    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=True)
    if not pc.get("available"):
        return {"available": False,
                "reason": pc.get("reason", "no usable per-coin Bybit book (run the Phase-A/B ingests first)")}
    cmp = compare_spreads(pc["spread_by_coin"], real_spread_by_coin, coins=pc["spread_by_coin"].keys())
    return {"available": True, "median_book_spread_bps": pc["median_spread_bps"], **cmp}


def _build_grid(pc: dict[str, Any], *, capital: float, taker_fee_bps: float, maker_fee_bps: float,
                fill_fractions: tuple[float, ...], adverse_sels: tuple[float, ...],
                real_spread_by_coin: dict[str, float] | None = None) -> dict[str, Any]:
    """Build one (f × adverse) grid for the FIXED book `pc`, under either the CS spread (`real_spread_by_coin=
    None`) or the REAL live spread override. Returns `{baseline, table, operating_point}`. The GROSS series is
    untouched; only the per-turnover cost model (and which spread it reads) varies — nothing to overfit."""
    base = recost_net_series(pc, fill_fraction=0.0, adverse_sel=0.0, taker_fee_bps=taker_fee_bps,
                             maker_fee_bps=maker_fee_bps, real_spread_by_coin=real_spread_by_coin)
    base_m = _curve_metrics(base["net_series"], capital=capital)
    base_sharpe = base_m["sharpe"]
    base_drag = base["cost_drag_bps"]
    baseline = {"sharpe": base_sharpe, "total_return": base_m["total_return"], "cagr": base_m["cagr"],
                "max_dd": base_m["max_dd"], "cost_drag_bps": base_drag, "net_series": base["net_series"]}

    table: list[dict[str, Any]] = []
    for adverse in adverse_sels:
        for f in fill_fractions:
            r = recost_net_series(pc, fill_fraction=f, adverse_sel=adverse, taker_fee_bps=taker_fee_bps,
                                  maker_fee_bps=maker_fee_bps, real_spread_by_coin=real_spread_by_coin)
            m = _curve_metrics(r["net_series"], capital=capital)
            table.append({
                "fill_fraction": f, "adverse_sel": adverse, "sharpe": m["sharpe"],
                "total_return": m["total_return"], "cagr": m["cagr"], "max_dd": m["max_dd"],
                "cost_drag_bps": r["cost_drag_bps"], "delta_sharpe": m["sharpe"] - base_sharpe,
                "delta_cost_drag_bps": r["cost_drag_bps"] - base_drag, "is_baseline": (f == 0.0),
            })

    op = recost_net_series(pc, fill_fraction=_OPERATING_FILL_FRACTION, adverse_sel=_OPERATING_ADVERSE_SEL,
                           taker_fee_bps=taker_fee_bps, maker_fee_bps=maker_fee_bps,
                           real_spread_by_coin=real_spread_by_coin)
    op_m = _curve_metrics(op["net_series"], capital=capital)
    operating_point = {
        "fill_fraction": _OPERATING_FILL_FRACTION, "adverse_sel": _OPERATING_ADVERSE_SEL,
        "sharpe": op_m["sharpe"], "total_return": op_m["total_return"], "cagr": op_m["cagr"],
        "max_dd": op_m["max_dd"], "cost_drag_bps": op["cost_drag_bps"],
        "delta_sharpe": op_m["sharpe"] - base_sharpe,
        "delta_cost_drag_bps": op["cost_drag_bps"] - base_drag,
    }
    return {"baseline": baseline, "table": table, "operating_point": operating_point}


def maker_recost_sweep(store: _FeatureStore, *, universe: set[str] | None, capital: float = 100_000.0,
                       taker_fee_bps: float = _TAKER_FEE_BPS, maker_fee_bps: float = _MAKER_FEE_BPS,
                       sleeves: list[str] | None = None,
                       unlock_events: list[dict[str, Any]] | None = None,
                       fill_fractions: tuple[float, ...] = _FILL_FRACTIONS,
                       adverse_sels: tuple[float, ...] = _ADVERSE_SELS,
                       real_spread_by_coin: dict[str, float] | None = None) -> dict[str, Any]:
    """Sweep the maker FILL FRACTION × adverse-selection grid for the Bybit 4-edge book and report the
    net-Sharpe lift from passive fills. The GROSS book return is UNCHANGED across the whole grid; only the
    per-turnover cost model varies (execution-only, nothing to overfit). READ-ONLY, memory-bounded.

    Builds the per-coin book ONCE (`per_coin_book_returns(..., return_detail=True)`), then re-costs it for
    every (f, adverse_sel). The `f=0` row is the all-taker BASELINE and reproduces the live deployed net
    EXACTLY; each row reports `delta_sharpe` / `delta_cost_drag_bps` vs that baseline.

    `real_spread_by_coin` (optional): when supplied, the sweep ALSO builds a SECOND grid in which the REAL live
    L1 quoted spread replaces the high/low Corwin–Schultz estimate in both cost legs (coins without a live
    quote fall back to CS). The result then carries a `real` sub-report `{baseline, table, operating_point,
    + compare_spreads(...) keys}` alongside the CS grid, so the CS-vs-REAL difference is explicit. CAVEAT: the
    real spread is a CURRENT snapshot applied across history — it is the FORWARD-realistic cost (what we'd pay
    deploying NOW), not the 2021–22 cost (spreads were wider then); the CS grid is a worst-case upper bound.

    Returns `{available, capital, taker_fee_bps, maker_fee_bps, n_coins, median_spread_bps,
    avg_daily_turnover, days, baseline {...}, table [per-(f,adverse) rows], operating_point {...},
    gross_series, gross_metrics, [real {...}]}`. Writes NOTHING."""
    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=True)
    if not pc.get("available"):
        return {"available": False, "capital": capital, "taker_fee_bps": taker_fee_bps,
                "maker_fee_bps": maker_fee_bps, "table": [],
                "reason": pc.get("reason", "no usable per-coin Bybit book (run the Phase-A/B ingests first)")}

    gross_series = pc["gross_series"]
    n_days = len(gross_series)
    avg_daily_turnover = (pc["turnover"] / n_days) if n_days else 0.0
    gross_metrics = _curve_metrics(gross_series, capital=capital)

    # The CS-spread grid (the current high/low Corwin–Schultz model). f=0 is the all-taker BASELINE.
    cs_grid = _build_grid(pc, capital=capital, taker_fee_bps=taker_fee_bps, maker_fee_bps=maker_fee_bps,
                          fill_fractions=fill_fractions, adverse_sels=adverse_sels)

    result = {"available": True, "capital": capital, "taker_fee_bps": taker_fee_bps,
              "maker_fee_bps": maker_fee_bps, "n_coins": pc["n_coins"],
              "median_spread_bps": pc["median_spread_bps"], "avg_daily_turnover": avg_daily_turnover,
              "days": n_days, "baseline": cs_grid["baseline"], "table": cs_grid["table"],
              "operating_point": cs_grid["operating_point"], "gross_series": gross_series,
              "gross_metrics": gross_metrics}

    if real_spread_by_coin is not None:
        # The REAL-spread grid: the SAME book + turnover, re-costed with the live quoted spread (CS fallback
        # for unquoted coins). The forward-realistic number; the CS grid is the worst-case upper bound.
        real_grid = _build_grid(pc, capital=capital, taker_fee_bps=taker_fee_bps, maker_fee_bps=maker_fee_bps,
                                fill_fractions=fill_fractions, adverse_sels=adverse_sels,
                                real_spread_by_coin=real_spread_by_coin)
        cmp = compare_spreads(pc["spread_by_coin"], real_spread_by_coin, coins=pc["spread_by_coin"].keys())
        result["real"] = {**real_grid, **cmp}

    return result
