"""Equity honest-reference: the IBKR US-ETF book's cost-netted return series + capacity, the analog of the
crypto honest_reference. Real costs: sqrt market-impact on REAL dollar-ADV (ingested Yahoo volume) + a
documented per-ETF spread + IBKR commission, charged on the book's ACTUAL traded weight (two-way turnover).
Pure; no I/O (callers pass preloaded closes/volumes/weights)."""
from __future__ import annotations

import bisect
import math
import statistics
from collections.abc import Iterable

from fxhnt.application.honest_reference import IMPACT_COEFF_BPS
from fxhnt.domain.gauntlet import per_period_sharpe

# Documented, estimated input (no ETF bid/ask feed available): half-spread in bps per FUND_INSTRUMENTS
# symbol. These are hand-set from typical quoted spreads for these ETFs, NOT measured from a live feed --
# the one estimated input in this cost model. Unlisted symbols fall back to _DEFAULT_SPREAD_BPS.
ETF_HALF_SPREAD_BPS: dict[str, float] = {"SPY": 0.3, "IEF": 1.0, "GLD": 1.0, "PDBC": 4.0, "DBMF": 8.0}
_DEFAULT_SPREAD_BPS = 5.0


def _spread_bps(sym: str, spread_bps: dict[str, float] | None = None) -> float:
    """Half-spread for `sym`: `spread_bps[sym]` when an override dict is supplied AND contains the symbol
    (TASK 2, 2026-07-16 UCITS re-base plan — the regime-selected spread dict from `equity_allocation_inputs`),
    else the module-constant `ETF_HALF_SPREAD_BPS` (falling back to `_DEFAULT_SPREAD_BPS`) — the pre-regime
    "us" behavior. The module constant is NEVER mutated; `spread_bps=None` (the default) is byte-identical to
    the pre-Task-2 lookup."""
    if spread_bps is not None and sym in spread_bps:
        return spread_bps[sym]
    return ETF_HALF_SPREAD_BPS.get(sym, _DEFAULT_SPREAD_BPS)


def dollar_adv(pit_closes: dict[str, dict[str, float]], pit_volumes: dict[str, dict[str, float]],
               *, window: int = 63) -> dict[str, dict[str, float]]:
    """Per-symbol trailing-`window` MEDIAN dollar volume (close*volume), CARRIED across the full close-date
    range. The median is computed on the days that HAVE volume; then EVERY close date is assigned the nearest
    preceding volume-date median (forward-fill), and close dates before the first volume date get the earliest
    median (back-fill). Rationale: capacity is a CURRENT-liquidity concept — a long return series (e.g. the
    ~441d multistrat book) must NOT have its pre-volume-history days zeroed for lack of (irrelevant) old UCITS
    volume; those days are sized at the nearest available liquidity. A symbol with NO usable volume at all ->
    `{}` (the caller's ADV-coverage guard then trips). The median is outlier-robust (a single spike can't
    inflate capacity)."""
    out: dict[str, dict[str, float]] = {}
    for sym, closes in pit_closes.items():
        vols = pit_volumes.get(sym, {})
        dv = {d: closes[d] * vols[d] for d in sorted(vols) if d in closes and vols[d] is not None}
        vdates = sorted(dv)
        if not vdates:
            out[sym] = {}
            continue
        med: dict[str, float] = {}
        for i, d in enumerate(vdates):
            lo = max(0, i - window + 1)
            med[d] = statistics.median(dv[x] for x in vdates[lo:i + 1])
        first_med = med[vdates[0]]
        cur: dict[str, float] = {}
        for d in closes:
            j = bisect.bisect_right(vdates, d) - 1   # nearest volume date <= d
            cur[d] = med[vdates[j]] if j >= 0 else first_med   # forward-fill, else back-fill the earliest
        out[sym] = cur
    return out


def equity_honest_returns(book_ret_by_day: dict[str, float], weights_by_day: dict[str, dict[str, float]],
                          adv_usd: dict[str, dict[str, float]], *, aum: float,
                          participation_cap: float = 0.03, impact_k: float = 1.0,
                          commission_per_share: float = 0.0035,
                          price_by_day: dict[str, dict[str, float]] | None = None,
                          spread_bps: dict[str, float] | None = None) -> dict[str, float]:
    """Cost-netted daily book return. Cost = Sigma_sym traded_w * (max(impact, spread) + commission)/1e4, on
    the two-way traded weight; positions exceeding participation_cap*ADV are scaled down (capacity).

    `spread_bps` (TASK 2, 2026-07-16 UCITS re-base plan): optional per-symbol half-spread override (the
    regime-selected spread dict from `equity_allocation_inputs`) — a symbol's half-spread comes from this
    dict when present, else falls back to `_spread_bps(sym)` (the module-constant `ETF_HALF_SPREAD_BPS`
    lookup). `None` (the default) is byte-identical to the pre-Task-2 behavior; the module constant itself
    is never mutated.

    NOTE (approximation): `scale_hit` -- the fraction of the book's target weight that capacity forced us to
    drop -- is applied to the day's AGGREGATE book_ret_by_day (book_ret_by_day[day] * (1 - scale_hit)), not
    attributed per symbol. This is a book-level approximation (the book_ret series doesn't carry a
    per-symbol breakdown here); it is directionally correct and monotone (more capacity pressure -> higher
    scale_hit -> lower net return/Sharpe) but does not credit/debit the EXACT symbol whose position was
    capped. Acceptable for v1; a per-symbol attribution would need book_series's internal per-symbol P&L.
    """
    out: dict[str, float] = {}
    prev: dict[str, float] = {}
    for day in sorted(book_ret_by_day):
        row = weights_by_day.get(day, {})
        cur: dict[str, float] = {}
        scale_hit = 0.0
        for sym, w in row.items():
            adv = adv_usd.get(sym, {}).get(day, 0.0)
            notional = abs(w) * aum
            if adv <= 0.0:
                cur[sym] = 0.0            # no ADV -> zero capacity (honest, not infinite)
                scale_hit += abs(w)       # this weight can't be held -> its return is forgone
            elif notional > participation_cap * adv:
                cur[sym] = w * (participation_cap * adv) / notional
                scale_hit += abs(w) - abs(cur[sym])
            else:
                cur[sym] = w
        cost = 0.0
        for sym in set(cur) | set(prev):
            traded_w = abs(cur.get(sym, 0.0) - prev.get(sym, 0.0))
            if traded_w <= 0.0:
                continue
            adv = adv_usd.get(sym, {}).get(day, 0.0)
            traded_notional = traded_w * aum
            if adv > 0.0 and traded_notional > 0.0:
                impact = impact_k * IMPACT_COEFF_BPS * math.sqrt(traded_notional / adv)
            else:
                impact = 0.0
            price = (price_by_day or {}).get(sym, {}).get(day, 0.0)
            commission_bps = (100.0 * commission_per_share / price) if price > 0.0 else 0.0
            slip_bps = max(impact, _spread_bps(sym, spread_bps)) + commission_bps
            cost += traded_w * (slip_bps / 1e4)
        # capacity-forgone return: reduce the day's gross by the scaled-out fraction of the book's own return
        out[day] = book_ret_by_day[day] * (1.0 - scale_hit) - cost if scale_hit < 1.0 else -cost
        prev = cur
    return out


def equity_capacity_curve(book_ret_by_day: dict[str, float], weights_by_day: dict[str, dict[str, float]],
                          adv_usd: dict[str, dict[str, float]], *, aums: Iterable[float],
                          participation_cap: float = 0.03, impact_k: float = 1.0,
                          commission_per_share: float = 0.0035,
                          price_by_day: dict[str, dict[str, float]] | None = None,
                          spread_bps: dict[str, float] | None = None) -> dict[float, dict[str, float]]:
    """{aum: equity_honest_returns(...at aum)} -- load-once inputs, reprice per AUM. Reveals the capacity
    curve (Sharpe vs AUM): higher AUM -> larger traded notionals -> more capacity scaling + impact -> lower
    (or equal) net Sharpe, in the regime where capacity is progressively engaging. NOTE: because
    `equity_honest_returns`'s scale_hit is a book-level approximation (not a per-symbol P&L attribution),
    once a position is so far past the participation cap that the held weight collapses toward zero for
    EVERY day (deep saturation), the daily net series degenerates toward `-cost` with cost itself shrinking
    too (traded weight -> 0) -- at that point the Sharpe-vs-AUM relationship is no longer reliably monotone.
    The monotone-decreasing property holds while capacity is still meaningfully binding, not in that
    fully-saturated tail."""
    return {aum: equity_honest_returns(book_ret_by_day, weights_by_day, adv_usd, aum=aum,
                                       participation_cap=participation_cap, impact_k=impact_k,
                                       commission_per_share=commission_per_share, price_by_day=price_by_day,
                                       spread_bps=spread_bps)
            for aum in aums}


__all__ = [
    "ETF_HALF_SPREAD_BPS",
    "dollar_adv",
    "equity_capacity_curve",
    "equity_honest_returns",
    "per_period_sharpe",
]
