"""Diversified time-series-momentum research — the deterministic CTA core (no agents).

Builds the canonical Moskowitz-Ooi-Pedersen / AQR diversified TSMOM book on the continuous futures
universe: a multi-lookback trend signal per instrument, VOL-TARGETED to equal risk, combined across a
diversified universe, net of realistic costs. Then judged by the full fxhnt discipline (deflated Sharpe,
IS/OOS, drawdown). Every cent accounted for: turnover costs charged on every position change.
"""
from __future__ import annotations

import glob
import os
from dataclasses import dataclass, field

import numpy as np

from fxhnt.adapters.data.dbn_local import LocalDbnLoader
from fxhnt.domain.gauntlet import deflated_sharpe, per_period_sharpe
from fxhnt.domain.models import PriceSeries
from fxhnt.domain.strategies.base import rolling_std

_ANN = 252.0


def vol_targeted_trend(prices: PriceSeries, lookbacks: tuple[int, ...], *, vol_window: int = 60,
                       target_vol_annual: float = 0.10, cost_bps: float = 1.0,
                       max_leverage: float = 3.0) -> tuple[tuple[str, ...], np.ndarray]:
    """One instrument's vol-targeted multi-lookback TSMOM net return series (causal, cost-charged)."""
    c = np.asarray(prices.close, float)
    n = len(c)
    r = np.zeros(n)
    r[1:] = c[1:] / c[:-1] - 1.0
    r = np.nan_to_num(r)

    signal = np.zeros(n)                                   # mean trend sign across lookbacks
    for lb in lookbacks:
        s = np.zeros(n)
        s[lb:] = np.sign(c[lb:] / c[:-lb] - 1.0)
        signal += s
    signal /= len(lookbacks)

    vol = rolling_std(r, vol_window)                       # per-day realized vol (causal)
    target_day = target_vol_annual / np.sqrt(_ANN)
    raw = np.zeros(n)
    nz = vol > 1e-8
    raw[nz] = np.clip(signal[nz] * target_day / vol[nz], -max_leverage, max_leverage)
    pos = np.zeros(n)
    pos[1:] = raw[:-1]                                     # trade on yesterday's signal (no lookahead)

    gross = pos * r
    turnover = np.abs(np.diff(np.concatenate([[0.0], pos])))
    net = gross - turnover * (cost_bps / 1e4)
    return tuple(prices.dates), net


@dataclass
class TsmomResult:
    n_instruments: int
    n_obs: int
    ann_return: float
    ann_vol: float
    sharpe: float
    max_drawdown: float
    is_sharpe: float
    oos_sharpe: float
    dsr: float
    per_class_sharpe: dict[str, float] = field(default_factory=dict)


def _book_returns(series: list[tuple[tuple[str, ...], np.ndarray]]) -> tuple[tuple[str, ...], np.ndarray]:
    """Equal-risk book: average the (already vol-targeted) sleeves over the UNION of dates — each day is the
    mean of whichever instruments traded that day (using all 16y, not the biased all-overlap intersection)."""
    if not series:
        return (), np.array([])
    all_days = sorted(set().union(*(set(dates) for dates, _ in series)))
    day_to_j = {d: j for j, d in enumerate(all_days)}
    acc = np.zeros(len(all_days))
    count = np.zeros(len(all_days))
    for dates, ret in series:
        for k, d in enumerate(dates):
            j = day_to_j[d]
            acc[j] += ret[k]
            count[j] += 1
    return tuple(all_days), acc / np.maximum(count, 1)


class TsmomResearch:
    def __init__(self, data_dir: str, *, lookbacks: tuple[int, ...] = (20, 60, 120, 250),
                 cost_bps: float = 1.0, oos_fraction: float = 0.40, n_trials: int = 4) -> None:
        self._dir = data_dir
        self._lookbacks = lookbacks
        self._cost_bps = cost_bps
        self._oos_fraction = oos_fraction
        self._n_trials = n_trials
        self._loader = LocalDbnLoader()

    def run(self, symbols: list[str], asset_class_of: dict[str, str] | None = None) -> TsmomResult:
        sleeves, by_class = [], {}
        for sym in symbols:
            path = os.path.join(self._dir, f"{sym}.dbn")
            if not os.path.exists(path):
                continue
            try:
                ps = self._loader.load(path, sym)
            except Exception:
                continue
            dates, net = vol_targeted_trend(ps, self._lookbacks, cost_bps=self._cost_bps)
            sleeves.append((dates, net))
            if asset_class_of:
                by_class.setdefault(asset_class_of.get(sym, "other"), []).append((dates, net))

        days, book = _book_returns(sleeves)
        b = book[np.isfinite(book)]
        ann_ret = float(b.mean() * _ANN)
        ann_vol = float(b.std() * np.sqrt(_ANN))
        sharpe = ann_ret / ann_vol if ann_vol > 0 else 0.0
        eq = np.cumprod(1.0 + b)
        mdd = float((eq / np.maximum.accumulate(eq) - 1.0).min()) if len(eq) else 0.0
        split = int((1.0 - self._oos_fraction) * len(book))
        is_sr = per_period_sharpe(book[:split]) * np.sqrt(_ANN)
        oos_sr = per_period_sharpe(book[split:]) * np.sqrt(_ANN)
        sr_var = 0.0  # single pre-specified strategy; n_trials carries the lookback multiplicity
        dsr = deflated_sharpe(book[:split], self._n_trials, max(sr_var, 1e-9))

        per_class = {}
        for cls, sl in by_class.items():
            _, cb = _book_returns(sl)
            cb = cb[np.isfinite(cb)]
            per_class[cls] = float(cb.mean() / cb.std() * np.sqrt(_ANN)) if cb.std() > 0 else 0.0

        return TsmomResult(n_instruments=len(sleeves), n_obs=len(book), ann_return=ann_ret, ann_vol=ann_vol,
                           sharpe=sharpe, max_drawdown=mdd, is_sharpe=is_sr, oos_sharpe=oos_sr, dsr=dsr,
                           per_class_sharpe=per_class)
