"""Refresh + read the REAL per-coin L1 quoted spread — the forward-realistic cost input for the Bybit liquid
book, replacing the daily-high/low Corwin–Schultz proxy that over-states the true quoted spread ~104×
(measured 1.4bp real vs 143.6bp CS).

ROBUSTNESS: the L1 spread is a momentary snapshot, so a single `/v5/market/tickers` read can catch one weird
moment (a wide tick, a settling auction). `sample_median_spreads` SAMPLES the snapshot several times a few
seconds apart and takes the MEDIAN per coin — robust against any one bad sample. The wall-clock `sleep`
between samples is INJECTED (no bare `time.sleep` on the test path; tests pass a no-op), so the median is
computed deterministically off fixtured multi-sample snapshots with NO network.

The median per coin is persisted to `bybit_real_spread` (coin, spread_bps, fetched_at) via the operational
repo's idempotent upsert; the measured-cost precompute reads it via `read_real_spread_by_coin` and charges the
liquid book the REAL spread. A coin with no stored real spread (delisted / illiquid) falls back to CS.
"""
from __future__ import annotations

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


class _SpreadSource(Protocol):
    def real_spread_bps(self, *, universe: set[str] | None = ...) -> dict[str, float]: ...


class _SpreadRepo(Protocol):
    def upsert_real_spreads(self, spreads: dict[str, float], *, at: dt.datetime) -> None: ...
    def read_real_spread_by_coin(self) -> dict[str, float]: ...


def sample_median_spreads(source: _SpreadSource, *, samples: int = 5, interval_s: float = 3.0,
                          sleep: Callable[[float], None] = time.sleep,
                          universe: set[str] | None = None) -> dict[str, float]:
    """Sample the live L1 quoted spread `samples` times (sleeping `interval_s` between, INJECTED) and return
    the MEDIAN per coin (robust against one weird moment). Each sample is `source.real_spread_bps(universe=...)`
    (`{coin: spread_bps}`), which ALREADY skips missing / zero / crossed quotes. A coin present in only some
    samples uses the median of the samples it DOES have; a coin with no valid sample at all is omitted. Pure
    aside from the injected `sleep`; deterministic for fixtured multi-sample sources."""
    n = max(1, int(samples))
    per_coin: dict[str, list[float]] = {}
    for i in range(n):
        if i > 0:
            sleep(interval_s)
        snap = source.real_spread_bps(universe=universe)
        if not isinstance(snap, dict):
            continue
        for coin, sp in snap.items():
            if coin is None or sp is None:
                continue
            try:
                v = float(sp)
            except (TypeError, ValueError):
                continue
            per_coin.setdefault(str(coin), []).append(v)
    return {c: float(st.median(v)) for c, v in per_coin.items() if v}


def refresh_real_spreads(repo: _SpreadRepo, source: _SpreadSource, *, samples: int = 5,
                         interval_s: float = 3.0, sleep: Callable[[float], None] = time.sleep,
                         at: dt.datetime | None = None,
                         universe: set[str] | None = None) -> dict[str, float]:
    """Sample → median → persist: take the median-of-`samples` L1 spread per coin and idempotently upsert it
    into `bybit_real_spread` (one row per coin, replaced). Returns the persisted `{coin: median spread bps}`.
    `at` defaults to now(UTC) — the snapshot's asof time (a CURRENT snapshot applied across history)."""
    medians = sample_median_spreads(source, samples=samples, interval_s=interval_s, sleep=sleep,
                                    universe=universe)
    repo.upsert_real_spreads(medians, at=at or dt.datetime.now(dt.UTC))
    return medians


def read_real_spread_by_coin(repo: _SpreadRepo) -> dict[str, float]:
    """`{coin: real L1 quoted spread bps}` from the operational store — the override the measured cost model
    charges the Bybit liquid book instead of the CS estimate (coins absent fall back to CS). Empty when no
    refresh has run yet. Read-only."""
    out: Any = repo.read_real_spread_by_coin()
    return dict(out) if out else {}
