"""Pure cross-sectional crypto funding-dispersion domain: PIT eligibility, the
cross-sectional funding score, construction weights, and signed delta-neutral
carry book-return. No I/O. Panel = {symbol: {epoch_day: (close, qvol, funding)}}."""
from __future__ import annotations

import datetime as dt

from fxhnt.domain.strategies.equity_factor import robust_z

Panel = dict[str, dict[int, tuple[float, float, float]]]


def _epoch_day(iso_date: str) -> int:
    return (dt.date.fromisoformat(iso_date) - dt.date(1970, 1, 1)).days


def eligible_asof(panel: Panel, asof_day: int, *, min_qvol: float,
                  min_history: int, vol_window: int = 30) -> list[str]:
    """Coins tradeable at `asof_day` (PIT): have >= `min_history` bars on/before asof,
    AND trailing-`vol_window` mean qvol >= `min_qvol`. Uses ONLY data <= asof_day."""
    out: list[str] = []
    lo = asof_day - vol_window
    for sym, series in panel.items():
        hist = [d for d in series if d <= asof_day]
        if len(hist) < min_history:
            continue
        vols = [series[d][1] for d in hist if d > lo]
        if vols and (sum(vols) / len(vols)) >= min_qvol:
            out.append(sym)
    out.sort()
    return out


def funding_score(panel: Panel, eligible: list[str], asof_day: int,
                  lookback_days: int = 7) -> dict[str, float]:
    """Cross-sectional carry score: robust_z (across `eligible`) of each coin's
    trailing-`lookback_days` mean funding (<= asof_day). Short window keeps it
    responsive (avoids stale-level bias). Returns {symbol: score} aligned to `eligible`."""
    lo = asof_day - lookback_days
    tf: list[float | None] = []
    for sym in eligible:
        series = panel[sym]
        vals = [series[d][2] for d in series if lo < d <= asof_day]
        tf.append(sum(vals) / len(vals) if vals else None)
    z = robust_z(tf)
    return {eligible[i]: z[i] for i in range(len(eligible))}


def _quantile_cut(scores: dict[str, float], quantile: float) -> tuple[list[str], list[str]]:
    """Return (top, bottom) DISJOINT symbol lists at the given quantile (by score desc).
    k is clamped to floor(n/2) so the top and bottom slices never overlap."""
    ranked = sorted(scores, key=lambda s: (-scores[s], s))
    n = len(ranked)
    k = max(1, min(int(quantile * n), n // 2))
    return ranked[:k], ranked[-k:]


def construction_weights(scores: dict[str, float], mode: str, *, quantile: float = 0.2,
                         borrowable: set[str] | None = None) -> dict[str, float]:
    """Map cross-sectional funding scores -> delta-neutral weights.
    long_tilt: equal-weight long the top quantile, sum=1.
    market_neutral: long top + short bottom, dollar-neutral (sum=0, gross=2).
    executable: long top (sum=1) + short bottom ONLY for names in `borrowable`.

    The top/bottom slices are disjoint (k clamped to floor(n/2)); at quantile>0.5
    long_tilt therefore caps its long book at n//2 names (acceptable dispersion tilt).

    NOTE: gross exposure differs by mode (long_tilt=1.0, market_neutral=2.0, executable=varies).
    Sharpe/DSR comparisons are scale-invariant; CAGR/maxDD are NOT comparable across modes
    without normalising to a common gross."""
    top, bot = _quantile_cut(scores, quantile)
    w: dict[str, float] = {}
    if mode == "long_tilt":
        for s in top:
            w[s] = 1.0 / len(top)
    elif mode == "market_neutral":
        for s in top:
            w[s] = 1.0 / len(top)
        for s in bot:
            w[s] = w.get(s, 0.0) - 1.0 / len(bot)
    elif mode == "executable":
        for s in top:
            w[s] = 1.0 / len(top)
        shorts = [s for s in bot if borrowable is None or s in borrowable]
        # If no bottom-quantile name is borrowable, `shorts` is empty -> short leg is empty and
        # the book degrades to long_tilt (sum=1, no shorts). Intended: you cannot short what you
        # cannot borrow, so the executable book holds only the long leg.
        for s in shorts:
            w[s] = w.get(s, 0.0) - 1.0 / len(shorts)
    else:
        raise ValueError(f"unknown construction mode: {mode}")
    return {s: v for s, v in w.items() if v != 0.0}


def book_return_xs(prev_weights: dict[str, float], funding_today: dict[str, float],
                   new_weights: dict[str, float], coin_cost_rt: dict[str, float]) -> float:
    """Signed delta-neutral carry on the PRIOR book at today's funding, minus round-trip
    cost on turnover. realized = sum(w_i * funding_i); a short (w<0) on a negative-funding
    coin earns w*funding>0. cost = sum(|Δw_i| * cost_i)/2 over the union.

    NOTE: domain primitive (prior-book/hold-then-rebalance convention) used in unit tests. The
    walk-forward runner uses its own `_book` (new-book/rebalance-then-carry convention) — the two
    encode the SAME accounting under different time-indexing; keep them in sync if either changes."""
    realized = sum(w * funding_today.get(s, 0.0) for s, w in prev_weights.items())
    cost = sum(abs(new_weights.get(s, 0.0) - prev_weights.get(s, 0.0)) * coin_cost_rt.get(s, 0.0)
               for s in set(new_weights) | set(prev_weights)) / 2.0
    return realized - cost


SpotPanel = dict[str, dict[int, float]]
