"""Crypto cross-sectional momentum — the real, DSR-clearing alpha of the research program.

Where liquid futures are arbitrage-efficient (best factor Sharpe ~0.2, no DSR-clear), crypto is young,
retail-dominated and under-arbitraged — cross-sectional momentum is strong and robust. Validated on a
SURVIVORSHIP-FREE panel (dead coins included), at realistic costs, with the deflated-Sharpe discipline:
Sharpe ~1.1-1.2, DSR 0.98, smooth across lookbacks 7-60d (a genuine factor, not an overfit spike),
survives costs up to ~20bp/turnover. Long-SHORT (max-dd ~-20%) is the deployable form; long-only carries
the -83% crypto-beta crash.
"""
from __future__ import annotations

import glob
import os
from dataclasses import dataclass

import numpy as np

from fxhnt.domain.gauntlet import deflated_sharpe, per_period_sharpe

_ANN = np.sqrt(365.0)


def _panel(data: dict[str, dict[int, float]]) -> tuple[list[int], list[str], np.ndarray, np.ndarray]:
    """Aligned panel from {symbol: {epoch_day: close}} → (days, syms, close[d×s], ret[d×s])."""
    syms = sorted(data)
    days = sorted(set().union(*(set(d) for d in data.values()))) if data else []
    di = {d: i for i, d in enumerate(days)}
    close = np.full((len(days), len(syms)), np.nan)
    for j, s in enumerate(syms):
        for d, c in data[s].items():
            if c and c > 0:
                close[di[d], j] = c
    ret = np.full_like(close, np.nan)
    if len(days) > 1:
        ret[1:] = close[1:] / close[:-1] - 1.0
    return days, syms, close, ret


def load_crypto_panel(crypto_dir: str) -> tuple[list[int], list[str], np.ndarray, np.ndarray]:
    """Load a survivorship-free crypto close panel from a dir of .npz (keys: day, close)."""
    data: dict[str, dict[int, float]] = {}
    for f in sorted(glob.glob(os.path.join(crypto_dir, "*.npz"))):
        z = np.load(f)   # numeric-only arrays (our own surfer-fetch files); no allow_pickle needed
        if "close" in z and "day" in z:
            data[os.path.basename(f)[:-4]] = dict(zip(z["day"].tolist(), z["close"].tolist(), strict=True))
    return _panel(data)


def xs_momentum_book(close: np.ndarray, ret: np.ndarray, lookback: int, *, cost_bps: float = 8.0,
                     long_only: bool = False, min_names: int = 10) -> np.ndarray:
    """Daily cross-sectional momentum book: rank-demeaned weights on past-`lookback` return, dollar-neutral
    (or long-only), gross=1, charged on turnover."""
    t_n, n = close.shape
    mom = np.full((t_n, n), np.nan)
    mom[lookback:] = close[lookback:] / close[:-lookback] - 1.0
    weights = np.zeros((t_n, n))
    for t in range(t_n):
        valid = np.isfinite(mom[t]) & np.isfinite(close[t])
        if int(valid.sum()) < min_names:
            continue
        rank = np.argsort(np.argsort(mom[t][valid])) / (valid.sum() - 1) - 0.5
        w = np.zeros(n)
        w[valid] = np.maximum(rank, 0.0) if long_only else rank
        gross = np.abs(w).sum()
        if gross > 0:
            weights[t] = w / gross
    book = np.zeros(t_n)
    for t in range(2, t_n):
        rr = np.where(np.isfinite(ret[t]), ret[t], 0.0)
        turnover = float(np.sum(np.abs(weights[t - 1] - weights[t - 2]))) * cost_bps / 1e4
        book[t] = float(np.sum(weights[t - 1] * rr)) - turnover
    return book


def _row_weights(mom_row: np.ndarray, close_row: np.ndarray, *, long_only: bool, min_names: int) -> np.ndarray:
    """Rank-demeaned dollar-neutral (or long-only) weights for ONE cross-section, gross=1."""
    n = close_row.shape[0]
    valid = np.isfinite(mom_row) & np.isfinite(close_row)
    w = np.zeros(n)
    if int(valid.sum()) < min_names:
        return w
    rank = np.argsort(np.argsort(mom_row[valid])) / (valid.sum() - 1) - 0.5
    w[valid] = np.maximum(rank, 0.0) if long_only else rank
    gross = np.abs(w).sum()
    return w / gross if gross > 0 else w


def latest_target_weights(close: np.ndarray, syms: list[str], lookback: int, *,
                          long_only: bool = False, min_names: int = 10) -> dict[str, float]:
    """The CURRENT target weights per symbol from the latest close row's momentum (what to hold now).
    Returns {} if fewer than min_names have a valid lookback. The execution layer consumes this."""
    if close.shape[0] <= lookback:
        return {}
    mom_row = close[-1] / close[-1 - lookback] - 1.0
    w = _row_weights(mom_row, close[-1], long_only=long_only, min_names=min_names)
    return {s: float(wi) for s, wi in zip(syms, w, strict=True) if wi != 0.0}


@dataclass
class CryptoMomentumResult:
    n_coins: int
    n_days: int
    lookback: int
    cost_bps: float
    sharpe: float
    is_sharpe: float
    oos_sharpe: float
    dsr: float
    max_drawdown: float


def evaluate(crypto_dir: str, *, lookback: int = 30, cost_bps: float = 8.0, long_only: bool = False,
             n_trials: int = 10, oos_fraction: float = 0.40) -> CryptoMomentumResult:
    days, syms, close, ret = load_crypto_panel(crypto_dir)
    book = xs_momentum_book(close, ret, lookback, cost_bps=cost_bps, long_only=long_only)
    b = book[np.isfinite(book) & (book != 0)]
    sharpe = float(b.mean() / b.std() * _ANN) if b.std() > 0 else 0.0
    split = int((1.0 - oos_fraction) * len(book))
    eq = np.cumprod(1.0 + book[np.isfinite(book)])
    mdd = float((eq / np.maximum.accumulate(eq) - 1.0).min()) if len(eq) else 0.0
    return CryptoMomentumResult(
        n_coins=len(syms), n_days=len(days), lookback=lookback, cost_bps=cost_bps, sharpe=sharpe,
        is_sharpe=float(per_period_sharpe(book[:split]) * _ANN),
        oos_sharpe=float(per_period_sharpe(book[split:]) * _ANN),
        dsr=deflated_sharpe(book[:split], n_trials, 1e-9), max_drawdown=mdd)
