"""
Motore del gioco-OPZIONI: prezza e backtesta strutture in opzioni proposte dagli
agenti ciechi, sui prezzi REALI ETH/BTC, con Black-Scholes + skew fittato + DVOL storica.

NON usa la chain reale (solo 6 settimane, un regime): prezza sinteticamente con la
vol implicita storica (DVOL Deribit, dal 2021-03) e la curva di skew fittata sulle IV
reali della ricerca credit-spread (iv/atm = 1 - 0.664*k + 3.494*k^2, k=ln(K/S)). Costi:
haircut bid/ask sulle opzioni (il fill reale e' peggiore del mid). Roll giornaliero,
hold-to-expiry (terminale model-free dai prezzi reali). PnL per-trade ADDITIVO.

Caveat onesto (dalla ricerca del progetto): il premium-selling a skew negativo vince nei
campioni calmi e restituisce tutto nei crash -> il gioco lo mostrera'.
"""
from __future__ import annotations

import sys
from pathlib import Path

import numpy as np
import pandas as pd

PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))

import json as _json

from src.data.downloader import load_data
from scripts.analysis.option_overlay_lab import bs_put, bs_call, dvol_for

# skew fittato (fallback se manca la calibrazione reale): iv/atm in funzione di k=ln(K/S).
SKEW_A, SKEW_B = -0.664, 3.494
MIN_TRADES_PER_MONTH = 10.0
TRADING_DAYS_MONTH = 30.0

# --- pricing REALE: superficie premi/spread da cerbero-bite (scripts/games/opt_calibrate.py) ---
_CALIB_DIR = PROJECT_ROOT / "data" / "games"
_CALIB = {}


def _load_calib(asset):
    if asset not in _CALIB:
        f = _CALIB_DIR / f"opt_calib_{asset.lower()}.json"
        _CALIB[asset] = _json.loads(f.read_text()) if f.exists() else None
    return _CALIB[asset]


def _surf_lookup(cal, typ, otm_signed, dte):
    """Premio% e spread reali per (otm firmato, dte): punto di griglia piu' vicino.
    Ritorna (prem_pct, spread, sellable) o None se fuori dalla superficie liquida."""
    s = cal["surface"][typ]
    og = cal["otm_grid"]; tg = cal["ten_grid"]
    o = min(og, key=lambda x: abs(x - otm_signed))
    t = min(tg, key=lambda x: abs(x - dte))
    if abs(o - otm_signed) > 0.06:          # troppo lontano dagli strike reali -> illiquido
        return None
    v = s.get(f"{o:+.2f}|{t}")
    if not v or v["sellable"] < 0.5:
        return None
    return v["prem"], v["spread"], v["sellable"]


def iv_skew(k: float, atm: float) -> float:
    """IV per moneyness k=ln(K/S) dato l'ATM vol. Clamp a [0.3x, 3x] atm."""
    mult = 1.0 + SKEW_A * k + SKEW_B * k * k
    mult = min(max(mult, 0.3), 3.0)
    return atm * mult


def load_opt(asset: str = "ETH"):
    """Prezzi GIORNALIERI (resample 1h->1d) + DVOL allineata. asset reale nascosto."""
    df = load_data(asset, "1h").copy()
    df["dt"] = pd.to_datetime(df["datetime"])
    g = df.set_index("dt").resample("1D").agg(
        {"timestamp": "first", "open": "first", "high": "max", "low": "min",
         "close": "last"}).dropna(subset=["close"]).reset_index(drop=True)
    g["timestamp"] = g["timestamp"].astype("int64")
    dv = dvol_for(g, asset)
    cal = _load_calib(asset)
    dvol_chain = (cal["dvol_chain"] / 100.0) if cal else float(np.nanmedian(dv))
    return {"close": g["close"].to_numpy(float), "high": g["high"].to_numpy(float),
            "low": g["low"].to_numpy(float), "dvol": dv, "asset": asset,
            "dvol_chain": dvol_chain, "real": cal is not None,
            "dt": pd.to_datetime(g["timestamp"], unit="ms", utc=True).to_numpy(),
            "n": len(g)}


# --------------------------------------------------------------------------
# Pricing di una struttura: ritorna (premio_netto_incassato, funzione_payoff(ST))
# premio>0 = struttura a CREDITO (vendi); payoff e' il valore terminale (>=0 per long opt).
# Convenzione PnL trade: net = (premio_incassato - payoff_terminale)/S0 - costi   (per credito)
# Tutto normalizzato sul SPOT (frazione), cosi' e' confrontabile fra asset/epoche.
# --------------------------------------------------------------------------
STRUCTURES = ["short_put", "short_call", "short_strangle", "put_spread",
              "call_spread", "iron_condor", "long_put", "long_call", "long_straddle"]


def _legs_for(struct, S, otm, width):
    kp = S * (1 - otm); kc = S * (1 + otm)
    kp2 = S * (1 - otm - width); kc2 = S * (1 + otm + width)
    return {
        "short_put": [("P", kp, -1)], "short_call": [("C", kc, -1)],
        "short_strangle": [("P", kp, -1), ("C", kc, -1)],
        "put_spread": [("P", kp, -1), ("P", kp2, +1)],
        "call_spread": [("C", kc, -1), ("C", kc2, +1)],
        "iron_condor": [("P", kp, -1), ("P", kp2, +1), ("C", kc, -1), ("C", kc2, +1)],
        "long_put": [("P", kp, +1)], "long_call": [("C", kc, +1)],
        "long_straddle": [("P", S, +1), ("C", S, +1)],
    }[struct]


def _price_real(struct, S, dte, scale, otm, width, cal):
    """Pricing REALE dalla superficie cerbero-bite. Ritorna (entry_cf_frac, legs, ok).
    entry_cf_frac = cassa d'ingresso in frazione di spot (>0 = incassi); side-aware bid/ask;
    ok=False se una gamba e' fuori dagli strike liquidi reali."""
    legs = _legs_for(struct, S, otm, width)
    entry = 0.0
    for typ, K, sgn in legs:
        q = _surf_lookup(cal, typ, K / S - 1.0, dte)
        if q is None:
            return 0.0, legs, False
        prem, spread, _ = q
        pf = prem / 100.0 * scale                 # premio frazione di spot, scalato a DVOL del giorno
        if sgn < 0:                               # short: incassi il BID (~ ask*(1-spread))
            entry += pf * (1 - spread)
        else:                                     # long: paghi l'ASK
            entry -= pf
    return entry, legs, True


def _price(struct, S, T, atm, otm, width):
    """Fallback SINTETICO (BS+skew). Usato solo se manca la calibrazione reale."""
    legs = _legs_for(struct, S, otm, width)
    prem = gross = 0.0
    for typ, K, sgn in legs:
        px = bs_put(S, K, T, iv_skew(np.log(K / S), atm)) if typ == "P" \
            else bs_call(S, K, T, iv_skew(np.log(K / S), atm))
        prem += -sgn * px / S
        gross += abs(px) / S
    return prem - 0.06 * gross, legs, True


def _payoff(legs, ST):
    v = 0.0
    for typ, K, sgn in legs:
        intr = max(K - ST, 0.0) if typ == "P" else max(ST - K, 0.0)
        v += sgn * intr              # valore terminale delle opzioni che POSSIEDI/devi
    return v        # per le short questo e' cio' che PAGHI (sgn<0 -> negativo = debito)


def evaluate(data, spec, sl=None):
    """Backtest della struttura: roll giornaliero, hold dte giorni, PnL additivo.
    spec = {structure, otm, width, dte}. Ritorna metriche con scoring PNL+%win, >=10 tr/mese.
    """
    c, dv = data["close"], data["dvol"]
    n = data["n"]
    s, e = (sl if sl else (0, n))
    struct = spec["structure"]
    otm = float(spec["otm"]); width = float(spec.get("width", 0.05))
    dte = int(spec["dte"])
    T = dte / 365.0
    cal = _load_calib(data["asset"]); dvol_chain = data["dvol_chain"]
    rets = []
    i = s
    while i < e - dte:
        S0 = c[i]; atm = dv[i]
        if S0 <= 0 or atm <= 0:
            i += 1; continue
        if cal is not None:           # PRICING REALE (cerbero-bite), scalato a DVOL del giorno
            scale = min(max(atm / dvol_chain, 0.3), 4.0)
            entry, legs, ok = _price_real(struct, S0, dte, scale, otm, width, cal)
            if not ok:                # strike fuori dalla superficie liquida reale -> non eseguibile
                i += 1; continue
            net = entry + _payoff(legs, c[i + dte]) / S0
        else:                         # fallback sintetico
            prem, legs, _ = _price(struct, S0, T, atm, otm, width)
            net = prem + _payoff(legs, c[i + dte]) / S0
        rets.append(net)
        i += 1                        # roll giornaliero (posizioni sovrapposte)
    rets = np.array(rets)
    nbars = e - s
    months = nbars / TRADING_DAYS_MONTH
    n_tr = len(rets)
    tpm = n_tr / months if months > 0 else 0.0
    if n_tr == 0:
        return dict(n_trades=0, win_rate=0.0, pnl_pct=0.0, tpm=0.0, sharpe=0.0,
                    avg_ret=0.0, qualified=False, fitness=-1e6)
    win = float(np.mean(rets > 0))
    pnl = float(np.sum(rets)) * 100
    avg = float(np.mean(rets)) * 100
    sharpe = float(np.mean(rets) / (np.std(rets) + 1e-12) * np.sqrt(tpm * 12)) \
        if np.std(rets) > 0 else 0.0
    qualified = tpm >= MIN_TRADES_PER_MONTH
    fitness = pnl + 50.0 * win
    if not qualified:
        fitness = -1e6 + pnl
    return dict(n_trades=n_tr, win_rate=win, pnl_pct=pnl, tpm=tpm, sharpe=sharpe,
                avg_ret=avg, qualified=qualified, fitness=fitness)


def splits3(data, train_frac=0.60, valid_frac=0.20):
    n = data["n"]
    c1 = int(n * train_frac); c2 = int(n * (train_frac + valid_frac))
    return (0, c1), (c1, c2), (c2, n)


if __name__ == "__main__":
    d = load_opt("ETH")
    print("loaded", d["n"], "giorni", str(d["dt"][0])[:10], "->", str(d["dt"][-1])[:10],
          "| dvol", round(float(np.nanmean(d["dvol"])), 2))
    tr, va, te = splits3(d)
    for st in ["short_put", "short_strangle", "iron_condor", "long_straddle", "put_spread"]:
        sp = {"structure": st, "otm": 0.05, "width": 0.05, "dte": 14}
        f = evaluate(d, sp, None); o = evaluate(d, sp, te)
        print(f"{st:14} FULL pnl{f['pnl_pct']:8.0f} win{f['win_rate']*100:4.0f} tpm{f['tpm']:5.0f} "
              f"Sh{f['sharpe']:6.1f} | OOS pnl{o['pnl_pct']:8.0f} win{o['win_rate']*100:4.0f} Sh{o['sharpe']:6.1f}")
