"""Assemble the (close, qvol, funding) panel the cross-sectional funding domain needs, from the warehouse
close panel + the ingested funding panel. Real funding now (was a 0.0 fallback); qvol uses a fallback
(the warehouse close-only bars carry no quote-volume). Pure."""
from __future__ import annotations

from fxhnt.domain.cross_sectional_funding import Panel


def warehouse_funding_panel(close_panel: dict[str, dict[int, float]],
                            funding_panel: dict[str, dict[int, float]], *,
                            qvol_fallback: float = 1e7) -> Panel:
    """{sym: {day: (close, qvol_fallback, funding)}} over the close panel's (sym, day) keys. funding is the
    ingested value for that (sym, day), or 0.0 where none was ingested. qvol uses `qvol_fallback` (>= the
    domain min_qvol, so the liquidity gate is effectively off in replay — same as the unlock sleeve)."""
    out: Panel = {}
    for sym, days in close_panel.items():
        fsym = funding_panel.get(sym, {})
        out[sym] = {d: (float(c), float(qvol_fallback), float(fsym.get(d, 0.0))) for d, c in days.items()}
    return out


def delta_neutral_return_panel(spot_panel: dict[str, dict[int, float]],
                               perp_panel: dict[str, dict[int, float]],
                               funding_panel: dict[str, dict[int, float]]) -> dict[str, dict[int, float]]:
    """{sym: {day: dn_ret}} where dn_ret(d) = (spot_ret − perp_ret) + funding(d), the live delta-neutral
    carry return. Requires a perp close at d AND d-1; the spot leg is used when spot has d AND d-1 (else
    funding-only fallback — the live strategy's documented behavior for coins without a spot pair). Pure."""
    out: dict[str, dict[int, float]] = {}
    for sym, pdays in perp_panel.items():
        sp = spot_panel.get(sym, {})
        fnd = funding_panel.get(sym, {})
        row: dict[int, float] = {}
        for d in pdays:
            if (d - 1) not in pdays or pdays[d - 1] <= 0:
                continue
            perp_ret = pdays[d] / pdays[d - 1] - 1.0
            f = fnd.get(d, 0.0)
            if (d in sp) and ((d - 1) in sp) and sp[d - 1] > 0:
                row[d] = (sp[d] / sp[d - 1] - 1.0 - perp_ret) + f
            else:
                row[d] = f
        if row:
            out[sym] = row
    return out


def _ret_with_gap(series: dict[int, float], d: int, *, max_gap: int) -> float | None:
    """1-day return `series[d]/series[d_prev] − 1` where `d_prev` is the NEAREST prior day with a
    positive value within `max_gap` days of `d` (d-1, d-2, …, d-max_gap). Returns None when `d` itself
    has no value, or no usable prior is found in the window (so the leg contributes nothing). Pure."""
    cur = series.get(d)
    if cur is None or cur <= 0:
        return None
    for back in range(1, max_gap + 1):
        prev = series.get(d - back)
        if prev is not None and prev > 0:
            return cur / prev - 1.0
    return None


def basis_change_by_symbol(spot_panel: dict[str, dict[int, float]],
                           perp_panel: dict[str, dict[int, float]], day: int, *,
                           max_gap: int = 4) -> dict[str, float]:
    """Per-symbol DAILY BASIS CHANGE for one `day` (epoch-day): `spot_ret − perp_ret`, where each leg is a
    1-day return vs the nearest prior day with data within `max_gap` (handles weekend / sparse-bar gaps).
    This is the delta-neutral position's price P&L the funding-only carry accounting dropped — the term
    where the basis (LUNA-class) tail lives.

    A symbol contributes a basis change ONLY when BOTH legs resolve (spot ret AND perp ret) for `day`;
    if either leg is missing (no spot pair, gap beyond the window, or no value on `day`) the symbol is
    OMITTED → 0 basis contribution for that symbol that day (documented: the funding-only fallback, same
    as `delta_neutral_return_panel`'s no-spot path). Keyed by symbol. Pure; no I/O."""
    out: dict[str, float] = {}
    for sym, pdays in perp_panel.items():
        perp_ret = _ret_with_gap(pdays, day, max_gap=max_gap)
        if perp_ret is None:
            continue
        spot_ret = _ret_with_gap(spot_panel.get(sym, {}), day, max_gap=max_gap)
        if spot_ret is None:
            continue
        out[sym] = spot_ret - perp_ret
    return out
