"""
V36 Diagnostic: RSI-14 Pre-Breakout Signal

Hypothesis: RSI-14 in the days before an ORB gap-up predicts follow-through quality.
Two competing sub-hypotheses:
  (A) Momentum: high RSI (>60) = confirmed uptrend, clean breakout. Positive Pearson.
  (B) Mean-reversion: high RSI = overbought, gap sells off. Negative Pearson.
  (C) Sweet spot: moderate RSI (40-60) = breakout from accumulation, not extended.

Features:
  rsi_14     : raw RSI-14 [0, 100] on last prev_bar
  rsi_momentum: (RSI - 50) / 50, centered at midline [-1, +1]

Source: V24 400d run JSON, daily bars from parquet cache.
Uses Cutler's RSI (simple average, same formula as libs/features/market_features.py).
"""
from __future__ import annotations

import concurrent.futures
import datetime as dt
import json
import os
import sys
from pathlib import Path

import pyarrow.parquet as pq

sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../..")))

from zoneinfo import ZoneInfo

_ET = ZoneInfo("America/New_York")
_MKT_OPEN = dt.time(9, 30)
_MKT_CLOSE = dt.time(16, 0)

INTRADAY_CACHE_DIR = "data/cache/intraday"
V24_400D_RUN = "runs/intraday_orb/intraday_20260422_011012_06f59ede.json"


def _parse_ts(ts_raw: object) -> dt.datetime:
    s = str(ts_raw)
    if s.endswith("Z"):
        s = s[:-1] + "+00:00"
    return dt.datetime.fromisoformat(s).astimezone(_ET)


def _build_daily_bar_from_parquet(path: Path, date: str) -> dict | None:
    try:
        table = pq.read_table(str(path))
        rows = table.to_pydict()
    except Exception:
        return None
    opens, highs, lows, closes, vols = [], [], [], [], []
    for i, ts_raw in enumerate(rows.get("timestamp", [])):
        try:
            ts = _parse_ts(ts_raw)
        except Exception:
            continue
        if _MKT_OPEN <= ts.time() < _MKT_CLOSE:
            opens.append(float(rows["open"][i] or 0))
            highs.append(float(rows["high"][i] or 0))
            lows.append(float(rows["low"][i] or 0))
            closes.append(float(rows["close"][i] or 0))
            vols.append(float(rows["volume"][i] or 0))
    if not opens or closes[-1] <= 0:
        return None
    return {
        "date": date, "open": opens[0], "high": max(highs),
        "low": min(lows), "close": closes[-1], "volume": sum(vols),
    }


def load_ticker_daily_bars(ticker: str, needed_dates: list[str]) -> list[dict]:
    root = Path(INTRADAY_CACHE_DIR) / ticker
    if not root.is_dir():
        return []
    bars = []
    for date in needed_dates:
        p = root / f"{date}.parquet"
        if not p.exists():
            continue
        bar = _build_daily_bar_from_parquet(p, date)
        if bar:
            bars.append(bar)
    return sorted(bars, key=lambda b: b["date"])


def compute_rsi_14(bars: list[dict], period: int = 14) -> float | None:
    """Cutler's RSI from daily close prices. bars sorted oldest→newest."""
    if len(bars) < period + 2:
        return None
    tail = bars[-(period + 1):]
    gains, losses = [], []
    for i in range(period):
        change = tail[i + 1]["close"] - tail[i]["close"]
        if change >= 0:
            gains.append(change)
            losses.append(0.0)
        else:
            gains.append(0.0)
            losses.append(abs(change))
    avg_gain = sum(gains) / period
    avg_loss = sum(losses) / period
    if avg_loss == 0:
        return 100.0
    return 100.0 - (100.0 / (1.0 + avg_gain / avg_loss))


def pearson(xs: list[float], ys: list[float]) -> float | None:
    n = len(xs)
    if n < 2:
        return None
    xm, ym = sum(xs) / n, sum(ys) / n
    num = sum((xs[i] - xm) * (ys[i] - ym) for i in range(n))
    dx = sum((x - xm) ** 2 for x in xs) ** 0.5
    dy = sum((y - ym) ** 2 for y in ys) ** 0.5
    if dx <= 0 or dy <= 0:
        return None
    return num / (dx * dy)


def tercile_stats(vals: list[float], rs: list[float]) -> dict:
    if len(vals) < 9:
        return {}
    pairs = sorted(zip(vals, rs), key=lambda p: p[0])
    n = len(pairs)
    t = n // 3
    def stats(sub):
        ys = [p[1] for p in sub]
        return {"n": len(ys), "wr": sum(1 for y in ys if y > 0) / len(ys), "avg_r": sum(ys) / len(ys)}
    return {"low": stats(pairs[:t]), "mid": stats(pairs[t:2*t]), "high": stats(pairs[2*t:])}


def report_feature(label: str, vals: list[float], rs: list[float], obv20: list[float] | None = None) -> None:
    n = len(vals)
    p = pearson(vals, rs)
    ts = tercile_stats(vals, rs)
    rho_obv20 = pearson(vals, obv20) if obv20 else None
    if not ts or p is None:
        print(f"  {label}: insufficient data")
        return
    low, mid, high = ts["low"], ts["mid"], ts["high"]
    avg_r_gap = abs(high["avg_r"] - low["avg_r"])
    wr_gap = abs(high["wr"] - low["wr"])
    g1 = abs(p) >= 0.07 and n >= 120
    g2 = avg_r_gap >= 0.30
    g3 = wr_gap >= 0.05
    g5a = rho_obv20 is None or abs(rho_obv20) < 0.70
    print(f"\n  [{label}]  n={n}  Pearson={p:+.3f}")
    print(f"    G1: {'PASS' if g1 else 'FAIL'} (|{abs(p):.3f}| {'≥' if abs(p)>=0.07 else '<'} 0.07, n={n} {'≥' if n>=120 else '<'} 120)")
    print(f"    G2: {'PASS' if g2 else 'FAIL'} (avg_R gap = {avg_r_gap:.3f}R  [threshold 0.30R])")
    print(f"    G3: {'PASS' if g3 else 'FAIL'} (WR gap = {wr_gap*100:.1f}pp  [threshold 5pp])")
    if rho_obv20 is not None:
        print(f"    G5a: {'PASS' if g5a else 'FAIL'} (|ρ(feature, obv_slope_20)| = {abs(rho_obv20):.3f}  [threshold 0.70])")
    print(f"    Bottom tercile: WR {low['wr']*100:.1f}%  avg_R {low['avg_r']:+.3f}  n={low['n']}")
    print(f"    Middle tercile: WR {mid['wr']*100:.1f}%  avg_R {mid['avg_r']:+.3f}  n={mid['n']}")
    print(f"    Top tercile:    WR {high['wr']*100:.1f}%  avg_R {high['avg_r']:+.3f}  n={high['n']}")
    all_pass = g1 and g2 and g3 and g5a
    print(f"    → {'ALL GATES PASS ✓' if all_pass else 'FAIL'}")


def main() -> None:
    print("=== V36 RSI-14 Pre-Breakout Diagnostic ===\n")

    with open(V24_400D_RUN) as f:
        run_data = json.load(f)
    trades = run_data.get("trades", [])
    m = run_data.get("metrics", {})
    print(f"Loaded V24 400d run: {m.get('total_trades')} trades, "
          f"{m.get('start_date')} → {m.get('end_date')}")
    print(f"Return: {m.get('total_return_pct', 0)*100:.2f}%  "
          f"DD: {m.get('max_drawdown_pct', 0)*100:.2f}%  "
          f"Sharpe: {m.get('sharpe_ratio', 0):.3f}\n")

    trade_records = [
        {"ticker": t["ticker"], "date": t["date"][:10],
         "r_multiple": float(t["r_multiple_at_exit"])}
        for t in trades
        if t.get("r_multiple_at_exit") is not None
    ]
    print(f"Trades with r_multiple: {len(trade_records)}")

    # Gather all unique tickers and dates needed (extended window for RSI lookback)
    tickers_needed = sorted(set(r["ticker"] for r in trade_records))
    dates_by_ticker: dict[str, set[str]] = {t: set() for t in tickers_needed}
    for rec in trade_records:
        dates_by_ticker[rec["ticker"]].add(rec["date"])

    print(f"Loading daily bars for {len(tickers_needed)} tickers...")

    # Determine needed calendar range per ticker (need 30+ prior trading days)
    import datetime as dt
    min_date = min(r["date"] for r in trade_records)
    max_date = max(r["date"] for r in trade_records)
    # Build full calendar range with buffer
    start_cal = (dt.date.fromisoformat(min_date) - dt.timedelta(days=90)).isoformat()
    all_dates = []
    d = dt.date.fromisoformat(start_cal)
    end_d = dt.date.fromisoformat(max_date)
    while d <= end_d:
        all_dates.append(d.isoformat())
        d += dt.timedelta(days=1)

    def _load(ticker: str) -> tuple[str, list[dict]]:
        return ticker, load_ticker_daily_bars(ticker, all_dates)

    ticker_bars: dict[str, list[dict]] = {}
    with concurrent.futures.ThreadPoolExecutor(max_workers=8) as ex:
        for ticker, bars in ex.map(_load, tickers_needed):
            if bars:
                ticker_bars[ticker] = bars

    print(f"Loaded bars for {len(ticker_bars)} / {len(tickers_needed)} tickers\n")

    # Compute features per trade
    rsi_vals, rsi_mom_vals, obv20_vals, r_mults = [], [], [], []
    missing = 0

    for rec in trade_records:
        ticker = rec["ticker"]
        date = rec["date"]
        r = rec["r_multiple"]

        bars = ticker_bars.get(ticker, [])
        prev_bars = [b for b in bars if b["date"] < date]

        rsi = compute_rsi_14(prev_bars, period=14) if len(prev_bars) >= 16 else None
        if rsi is None:
            missing += 1
            continue

        # OBV slope 20d for redundancy check
        from libs.intraday.features import compute_obv_slope_approx
        obv20 = compute_obv_slope_approx(prev_bars, lookback=20) if len(prev_bars) >= 22 else None

        rsi_vals.append(rsi)
        rsi_mom_vals.append((rsi - 50.0) / 50.0)
        obv20_vals.append(obv20 if obv20 is not None else 0.0)
        r_mults.append(r)

    n_valid = len(r_mults)
    print(f"Valid trades (RSI computable): {n_valid} / {len(trade_records)}  (missing: {missing})")

    print("\n" + "=" * 60)
    print("GATE RESULTS (G1: |P|≥0.07 & n≥120; G2: avg_R≥0.30R; G3: WR≥5pp; G5a: ρ<0.70 vs obv_slope_20)")

    report_feature("rsi_14", rsi_vals, r_mults, obv20_vals)
    report_feature("rsi_momentum (RSI-50)/50", rsi_mom_vals, r_mults, obv20_vals)

    # Distribution stats
    mean_rsi = sum(rsi_vals) / len(rsi_vals) if rsi_vals else 0
    print(f"\n  RSI distribution: mean={mean_rsi:.1f}  min={min(rsi_vals):.1f}  max={max(rsi_vals):.1f}")

    print("\n=== Summary ===")
    p = pearson(rsi_vals, r_mults)
    if p is not None:
        direction = "Momentum (high RSI = better)" if p > 0 else "Mean-reversion (low RSI = better)"
        print(f"Signal direction: {direction}  (Pearson={p:+.3f})")
    print("Context: V24's only passing axis: OBV-slope G2=0.394R. Target: G2 ≥ 0.30R.")


if __name__ == "__main__":
    main()
