"""Deflated-Sharpe validation math (Bailey & López de Prado 2014).

The single most important guardrail in fxhnt: an agentic search over thousands of (strategy × market)
combos is a false-positive factory unless the statistics correct for the full search size. The
Deflated Sharpe Ratio answers "is this strategy real, GIVEN I tried N of them?". Pure stdlib + numpy.
"""
from __future__ import annotations

import math
from statistics import NormalDist

import numpy as np

from fxhnt.domain.models import Verdict

_N = NormalDist()
_EULER = 0.5772156649015329


def _per_period_moments(r: np.ndarray) -> tuple[float, float, float, int]:
    r = np.asarray(r, float)
    r = r[np.isfinite(r)]
    if len(r) < 3 or r.std(ddof=1) == 0:
        return 0.0, 0.0, 3.0, len(r)
    mu, sd = float(r.mean()), float(r.std(ddof=1))
    z = (r - mu) / sd
    return mu / sd, float((z ** 3).mean()), float((z ** 4).mean()), len(r)


def per_period_sharpe(returns: np.ndarray) -> float:
    """Per-period (not annualized) Sharpe — the unit the deflated-Sharpe trial variance is measured in."""
    return _per_period_moments(np.asarray(returns, float))[0]


def annualized_sharpe(returns: np.ndarray, periods_per_year: int = 252) -> float:
    r = np.asarray(returns, float)
    r = r[np.isfinite(r)]
    if len(r) < 2 or r.std() == 0:
        return 0.0
    return float(r.mean() / r.std() * math.sqrt(periods_per_year))


def probabilistic_sharpe(returns: np.ndarray, sr_benchmark: float = 0.0) -> float:
    """P(true per-period Sharpe > sr_benchmark), correcting for skew, kurtosis and sample length."""
    sr, g1, g2, t = _per_period_moments(np.asarray(returns, float))
    if t < 3:
        return float("nan")
    denom = math.sqrt(max(1e-12, 1.0 - g1 * sr + (g2 - 1.0) / 4.0 * sr * sr))
    return _N.cdf((sr - sr_benchmark) * math.sqrt(t - 1) / denom)


def effective_trials(trial_returns: list[np.ndarray]) -> float:
    """Effective number of INDEPENDENT trials, correcting for correlation among candidate return streams.

    A blind grid (trend-150/200/250 on the same asset; buy-hold on correlated assets) tests far fewer
    *independent* hypotheses than its raw count, so deflating by the raw N over-penalises. Design-effect:
        N_eff = N / (1 + (N-1)·rho_bar),   rho_bar = mean off-diagonal correlation (clamped ≥ 0)
    In [1, N]: rho_bar=0 (independent) → N; rho_bar=1 (identical) → 1. Tail-aligned approximation.
    """
    arrs = [np.asarray(r, float) for r in trial_returns]
    n = len(arrs)
    if n <= 1:
        return float(n)
    length = min((a.size for a in arrs), default=0)
    if length < 3:
        return float(n)
    m = np.column_stack([np.nan_to_num(a[-length:]) for a in arrs])
    valid = m.std(axis=0) > 1e-12
    if int(valid.sum()) < 2:
        return float(n)
    c = np.corrcoef(m[:, valid].T)
    k = int(valid.sum())
    rho = max(0.0, float((np.nansum(c) - k) / (k * k - k)))
    return float(min(n, max(1.0, n / (1.0 + (n - 1) * rho))))


def expected_max_sharpe(n_trials: int, sr_variance: float) -> float:
    """Expected MAX per-period Sharpe under n_trials independent strategies with the given cross-trial
    Sharpe variance — the bar a 'winner' must clear to not be luck of the search."""
    if n_trials < 2:
        return 0.0
    a = _N.inv_cdf(1.0 - 1.0 / n_trials)
    b = _N.inv_cdf(1.0 - 1.0 / (n_trials * math.e))
    return math.sqrt(max(sr_variance, 1e-12)) * ((1.0 - _EULER) * a + _EULER * b)


def deflated_sharpe(returns: np.ndarray, n_trials: int, sr_variance: float) -> float:
    """DSR ≈ P(this strategy is real | you searched n_trials). The anti-multiple-testing statistic."""
    return probabilistic_sharpe(returns, expected_max_sharpe(n_trials, sr_variance))


def evaluate(
    is_returns: np.ndarray,
    oos_returns: np.ndarray,
    n_trials: int,
    sr_variance: float,
    *,
    dsr_min: float = 0.95,
    oos_min_sharpe: float = 0.0,
    max_is_oos_decay: float = 0.50,
    has_economic_rationale: bool | None = None,
) -> Verdict:
    """Full verdict. PASSES only if ALL hold:
      1. DSR (over the FULL search) ≥ dsr_min       — not a multiple-testing artifact
      2. OOS Sharpe > oos_min_sharpe                — works out of sample
      3. OOS Sharpe ≥ (1 - max_is_oos_decay) × IS   — doesn't collapse OOS
      4. (advisory) has_economic_rationale          — a structural reason the edge exists
    """
    dsr = deflated_sharpe(is_returns, n_trials, sr_variance)
    is_sr, oos_sr = annualized_sharpe(is_returns), annualized_sharpe(oos_returns)
    reasons: list[str] = []
    ok = True
    if not (dsr >= dsr_min):
        ok = False
        reasons.append(f"DSR {dsr:.3f} < {dsr_min} — likely a search artifact ({n_trials} trials)")
    else:
        reasons.append(f"DSR {dsr:.3f} ≥ {dsr_min} — survives multiple-testing correction")
    if oos_sr <= oos_min_sharpe:
        ok = False
        reasons.append(f"OOS Sharpe {oos_sr:+.2f} ≤ {oos_min_sharpe} — no out-of-sample edge")
    elif oos_sr < (1 - max_is_oos_decay) * is_sr:
        ok = False
        reasons.append(f"OOS Sharpe {oos_sr:+.2f} collapsed from IS {is_sr:+.2f}")
    else:
        reasons.append(f"OOS Sharpe {oos_sr:+.2f} holds vs IS {is_sr:+.2f}")
    if has_economic_rationale is False:
        ok = False
        reasons.append("no structural rationale — refusing a purely statistical fit")
    elif has_economic_rationale is None:
        reasons.append("⚠ economic rationale UNVERIFIED — justify before deploying")
    pvalue = max(0.0, min(1.0, 1.0 - dsr))   # for across-search FDR correction
    return Verdict(passed=ok, dsr=dsr, is_sharpe=is_sr, oos_sharpe=oos_sr, n_trials=n_trials,
                   reasons=reasons, pvalue=pvalue)
