"""Famiglia SHAPE-PIVOT: geometria a punti di svolta (PIP / pivot) -> bias futuro.

Idea (causale, no look-ahead):
  - a ogni barra i comprimo la finestra di L barre terminante a close[i] nei suoi
    P punti percettivamente importanti (PIP, Perceptually Important Points: i punti
    di massima deviazione dalla retta congiungente — Fu et al.);
  - la sequenza di P punti e' una POLILINEA = forma geometrica grezza;
  - la classifico con feature interpretabili e CAUSALI:
      * trend dei pivot interni: higher-highs/higher-lows (HH/HL) vs lower-*  (LH/LL);
      * convergenza/divergenza delle pendenze (triangoli/cunei);
      * distanza % di close[i] dall'ultimo pivot alto/basso (vicino a R / a S);
      * pendenza dell'ultimo segmento (slancio recente);
  - per ogni CLASSE geometrica stimo l'esito medio a H barre usando SOLO occorrenze
    passate il cui esito era gia' realizzato prima di i (statistica causale rolling);
  - entro a close[i] nella direzione del bias di classe se l'edge passato e' netto;
    exit a H barre o TP/SL in ATR.

VINCOLI (CLAUDE.md "metodologia obbligatoria" + "lezione squeeze look-ahead"):
  - PIP/pivot calcolati SOLO su close[i-L+1 .. i]; nessun pivot "confermato dal futuro".
  - ogni statistica per-classe usa solo campioni con esito (entry+H) <= i-1.
  - ingresso eseguibile a close[i]; netto fee (0.10% RT base, sweep a 0.20%); leva 3x,
    pos 0.15; validazione OOS (ultimo 30%) + robustezza griglia + >=2 asset.
  - check di causalita' esplicito (perturbo il futuro: la forma a i non cambia).

Riusa l'engine netto-fee + OOS di explore_lab (simulate/evaluate/robust).
"""
from __future__ import annotations

import sys
from pathlib import Path

import numpy as np

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

from scripts.analysis.explore_lab import (  # noqa: E402
    get_df, evaluate, robust, simulate, atr, ema, _dt, OOS_FRAC,
)


# =========================================================================
#  PIP — Perceptually Important Points (causale, solo su close[a..b])
# =========================================================================
def pip_indices(seg: np.ndarray, p: int) -> list[int]:
    """Estrae p indici PIP dalla serie `seg` (inclusi i 2 estremi).

    Algoritmo Fu et al.: parti dai 2 estremi; aggiungi iterativamente il punto a
    massima distanza VERTICALE dalla retta che unisce i due PIP adiacenti, finche'
    non hai p punti. Tutto sul segmento dato -> nessun look-ahead se seg=close[..i].
    """
    n = len(seg)
    if p >= n:
        return list(range(n))
    pts = [0, n - 1]
    while len(pts) < p:
        best_d, best_k = -1.0, -1
        for s in range(len(pts) - 1):
            l, r = pts[s], pts[s + 1]
            if r - l < 2:
                continue
            x1, y1 = l, seg[l]
            x2, y2 = r, seg[r]
            dx = x2 - x1
            # distanza verticale dalla retta (interpolazione lineare in x)
            for k in range(l + 1, r):
                if dx == 0:
                    dist = abs(seg[k] - y1)
                else:
                    yline = y1 + (y2 - y1) * (k - x1) / dx
                    dist = abs(seg[k] - yline)
                if dist > best_d:
                    best_d, best_k = dist, k
        if best_k < 0:
            break
        # inserisci mantenendo l'ordine
        for s in range(len(pts) - 1):
            if pts[s] < best_k < pts[s + 1]:
                pts.insert(s + 1, best_k)
                break
    return pts


# =========================================================================
#  Classe geometrica della polilinea PIP (feature causali interpretabili)
# =========================================================================
def shape_class(seg: np.ndarray, p: int) -> tuple | None:
    """Ritorna una tupla-classe discreta della forma PIP di `seg`, o None se degenere.

    Feature (tutte da seg=close[..i], causali):
      - dir_seq: per ogni pivot interno, segno della variazione vs precedente
                 (sequenza su/giu) -> cattura HH/HL vs LH/LL e zig-zag;
      - conv: convergenza pendenze inizio vs fine (triangolo/cuneo): segno di
              (|slope_last| - |slope_first|) discretizzato;
      - loc: posizione di close[i] nel range della finestra (vicino a max=resistenza,
             vicino a min=supporto), in 3 bucket.
    La classe e' invariante a livello/scala (z-norm implicito su forma).
    """
    idx = pip_indices(seg, p)
    if len(idx) < 3:
        return None
    y = seg[idx]
    rng = y.max() - y.min()
    if rng <= 0:
        return None
    yn = (y - y.min()) / rng                      # forma normalizzata 0..1
    # sequenza direzioni dei segmenti (su=1 / giu=0)
    diffs = np.diff(yn)
    dir_seq = tuple(int(x > 0) for x in diffs)
    # convergenza: pendenza primo vs ultimo segmento
    s_first = abs(diffs[0])
    s_last = abs(diffs[-1])
    if s_last > s_first * 1.3:
        conv = 1            # divergente (slancio finale)
    elif s_last < s_first * 0.77:
        conv = -1           # convergente (compressione, triangolo/cuneo)
    else:
        conv = 0
    # posizione di close[i] (=ultimo punto) nel range: 0..1 in 3 bucket
    last = yn[-1]
    loc = 0 if last < 0.33 else (2 if last > 0.67 else 1)
    return (dir_seq, conv, loc)


# =========================================================================
#  Strategia: bias per-classe stimato CAUSALMENTE (rolling, esito realizzato)
# =========================================================================
def pivot_entries(df, L=48, P=5, H=12, min_lib=1000, min_samples=20,
                  edge=0.0, tp_atr=None, sl_atr=None,
                  trend_max=None, ema_long=200, mode="bias") -> list[dict]:
    """Entries dalla geometria PIP con bias di classe causale.

    L: lunghezza finestra-forma. P: n. punti PIP. H: orizzonte (=max_bars).
    min_lib: barre minime prima di operare. min_samples: campioni minimi per fidarsi
             della statistica di una classe. edge: |rendimento medio classe| minimo
             (frazione, es. 0.002 = 0.2%) per entrare. mode:
       - "bias": entra nel verso del rendimento medio passato della classe (momentum
                 della forma: la classe X storicamente -> su/giu);
       - "fade": entra nel verso OPPOSTO (test mean-reversion della forma).
    Statistica per-classe accumulata SOLO con esiti realizzati < i (causale stretta).
    """
    close = df["close"].values
    high = df["high"].values
    low = df["low"].values
    n = len(close)
    a = atr(df, 14)
    el = ema(close, ema_long) if trend_max is not None else None

    # stato rolling per classe: somma rendimenti e conteggio (solo esiti < i)
    cls_sum: dict[tuple, float] = {}
    cls_cnt: dict[tuple, int] = {}
    # coda di campioni la cui forma e' stata calcolata ma esito non ancora maturo
    # pending[t] = (classe, indice_entry t) -> matura quando t+H <= i-1
    pending: list[tuple] = []           # (mature_at, cls, t)
    pend_ptr = 0

    entries: list[dict] = []

    for i in range(min_lib, n - 1):
        # 1) integra nello storico tutti i campioni il cui esito e' realizzato (< i)
        #    un campione formato a t matura quando t+H <= i-1  => mature_at = t+H+1 <= i
        while pend_ptr < len(pending) and pending[pend_ptr][0] <= i:
            _, cls_p, t = pending[pend_ptr]
            ret_real = (close[t + H] - close[t]) / close[t]
            cls_sum[cls_p] = cls_sum.get(cls_p, 0.0) + ret_real
            cls_cnt[cls_p] = cls_cnt.get(cls_p, 0) + 1
            pend_ptr += 1

        # 2) forma corrente (solo close fino a i)
        seg = close[i - L + 1: i + 1]
        cls = shape_class(seg, P)
        if cls is None:
            continue
        # registra il campione corrente come pending (esito da realizzare in futuro)
        pending.append((i + H + 1, cls, i))

        # 3) decisione con statistica PASSATA della classe
        cnt = cls_cnt.get(cls, 0)
        if cnt < min_samples:
            continue
        mean_ret = cls_sum[cls] / cnt
        if abs(mean_ret) < edge:
            continue
        d = 1 if mean_ret > 0 else -1
        if mode == "fade":
            d = -d
        # filtro trend opzionale
        if trend_max is not None and a[i] > 0:
            if abs(close[i] - el[i]) / a[i] > trend_max:
                continue
        e = {"i": i, "d": d, "max_bars": H}
        if tp_atr is not None and a[i] > 0:
            e["tp"] = close[i] + d * tp_atr * a[i]
        if sl_atr is not None and a[i] > 0:
            e["sl"] = close[i] - d * sl_atr * a[i]
        entries.append(e)
    return entries


# =========================================================================
#  Filone (c): distanza da supporto/resistenza locale (ultimo pivot alto/basso)
# =========================================================================
def sr_entries(df, L=48, P=7, H=12, near=0.5, mode="fade",
               tp_atr=None, sl_atr=None, trend_max=None, ema_long=200) -> list[dict]:
    """Filone (c): close[i] vicino all'ultimo pivot alto (R) o basso (S) della forma.

    Usa i PIP per individuare l'ultimo massimo/minimo locale (resistenza/supporto) e
    misura la distanza % di close[i]. Se close e' entro `near`*ATR da R -> bias short
    (mode='fade': rimbalzo da R) o long (mode='break': rottura). Simmetrico per S.
    Tutto causale: PIP su close[..i], decisione a close[i].
    """
    close = df["close"].values
    n = len(close)
    a = atr(df, 14)
    el = ema(close, ema_long) if trend_max is not None else None
    entries: list[dict] = []

    for i in range(L, n - 1):
        seg = close[i - L + 1: i + 1]
        idx = pip_indices(seg, P)
        if len(idx) < 3 or a[i] <= 0:
            continue
        y = seg[idx]
        # pivot interni (escludi i 2 estremi e l'ultimo punto = close[i])
        inner = y[1:-1]
        if len(inner) == 0:
            continue
        res = inner.max()       # resistenza locale
        sup = inner.min()       # supporto locale
        cur = close[i]
        dist_r = (res - cur) / a[i]
        dist_s = (cur - sup) / a[i]
        d = None
        if 0 <= dist_r <= near:          # appena sotto R
            d = -1 if mode == "fade" else 1
        elif 0 <= dist_s <= near:        # appena sopra S
            d = 1 if mode == "fade" else -1
        if d is None:
            continue
        if trend_max is not None and abs(cur - el[i]) / a[i] > trend_max:
            continue
        e = {"i": i, "d": d, "max_bars": H}
        if tp_atr is not None:
            e["tp"] = cur + d * tp_atr * a[i]
        if sl_atr is not None:
            e["sl"] = cur - d * sl_atr * a[i]
        entries.append(e)
    return entries


# =========================================================================
#  Check causalita' esplicito
# =========================================================================
def check_no_lookahead(df, L=48, P=5) -> bool:
    """La classe-forma a i non deve cambiare se perturbo il FUTURO (>i)."""
    close = df["close"].values.copy()
    i = len(close) // 2
    seg0 = close[i - L + 1: i + 1].copy()
    c0 = shape_class(seg0, P)
    close2 = close.copy()
    close2[i + 1:] *= 1.7                       # stravolge il futuro
    seg1 = close2[i - L + 1: i + 1]
    c1 = shape_class(seg1, P)
    ok = (c0 == c1)
    print(f"  no-lookahead classe-forma a i={i}: {'OK' if ok else 'VIOLATO'} "
          f"(c0={c0} c1={c1})")
    # check su PIP indices
    p0 = pip_indices(seg0, P)
    p1 = pip_indices(seg1, P)
    ok2 = (p0 == p1)
    print(f"  no-lookahead indici PIP: {'OK' if ok2 else 'VIOLATO'}")
    return ok and ok2


# =========================================================================
#  run() riproducibile
# =========================================================================
def run():
    print("=" * 100)
    print("  SHAPE-PIVOT RESEARCH — geometria PIP/pivot -> bias futuro | netto fee, OOS")
    print("=" * 100)

    df_btc = get_df("BTC", "1h")
    print("\n[CAUSALITA']")
    check_no_lookahead(df_btc, L=48, P=5)

    assets = ["BTC", "ETH", "SOL", "ADA"]
    dfs = {a: get_df(a, "1h") for a in assets}

    # ---- A) bias di classe PIP (momentum della forma) ----
    print("\n[A] BIAS di classe PIP (entra nel verso del rendimento medio passato della classe)")
    print("    sweep L/P/H, edge=0.002, min_samples=25, time-exit a H")
    A_grid = [(48, 5, 12), (48, 5, 24), (72, 6, 24), (36, 5, 12), (96, 7, 24), (48, 7, 12)]
    for L, P, H in A_grid:
        print(f"  -- L{L} P{P} H{H} --")
        for a in assets:
            ents = pivot_entries(dfs[a], L=L, P=P, H=H, edge=0.002, min_samples=25, mode="bias")
            evaluate(f"{a} bias L{L}P{P}H{H}", ents, dfs[a])

    # ---- B) fade di classe PIP (mean-reversion della forma) ----
    print("\n[B] FADE di classe PIP (entra opposto al bias storico -> test mean-reversion)")
    for L, P, H in A_grid:
        print(f"  -- L{L} P{P} H{H} --")
        for a in assets:
            ents = pivot_entries(dfs[a], L=L, P=P, H=H, edge=0.002, min_samples=25, mode="fade")
            evaluate(f"{a} fade L{L}P{P}H{H}", ents, dfs[a])

    # ---- C) supporto/resistenza locale dai pivot ----
    print("\n[C] S/R locale dai PIP — FADE (rimbalzo da R/S) vs BREAK (rottura)")
    for mode in ("fade", "break"):
        for near in (0.5, 1.0):
            print(f"  -- mode={mode} near={near} ATR, TP/SL 1.5/1.5 ATR, H=12 --")
            for a in assets:
                ents = sr_entries(dfs[a], L=48, P=7, H=12, near=near, mode=mode,
                                  tp_atr=1.5, sl_atr=1.5)
                evaluate(f"{a} SR-{mode} near{near}", ents, dfs[a])

    # ---- D) miglior candidato con TP/SL ATR + filtro trend (se A o B mostra segnali) ----
    print("\n[D] FADE di classe con TP/SL ATR (2.0/1.5) + filtro trend 3.0, L48 P5 H24")
    for a in assets:
        ents = pivot_entries(dfs[a], L=48, P=5, H=24, edge=0.002, min_samples=25,
                             mode="fade", tp_atr=2.0, sl_atr=1.5, trend_max=3.0)
        res = evaluate(f"{a} fadeTPSL L48P5H24", ents, dfs[a])
        if robust(res):
            print(f"       ^^^ {a} ROBUSTO")

    print("\n" + "=" * 100)
    print("  Verdetto: cerca righe con FULL>0 E OOS>0 E fee0.2% OOS>0 su >=2 asset.")
    print("=" * 100)


if __name__ == "__main__":
    run()
