"""Diagnostic Experiments: Fixed-holding & entry-timing analysis.

Experiment 1 — Stop-off + fixed holding period exit (3/5/10/20d)
  Purpose: Does the raw signal carry time-based alpha?
  Method:  Use pre-computed fwd_return_Xd (no stop, no target, pure hold).
  Report:  Mean return, median, win rate, by score bucket and event type.

Experiment 2 — Reaction-day close entry vs next-open entry
  Purpose: Is the overnight gap eating the edge?
  Method:  Compare fwd_return_Xd (next-open base) with
           close_entry_return = (entry_price / event_close) * (1 + fwd_return_Xd) - 1
  Constraint: Only pre-market/intraday filings (reaction_date == event_date)
              are eligible for close entry; after-close filings shown separately.

Usage:
  python -m dev.analysis.entry_timing_experiment \
    --snapshot-dir data/datasets/snapshots/a6687401-afdd-4bb7-9bb1-4cd32c5190bb \
    --split train
"""
from __future__ import annotations

import argparse
import statistics
from pathlib import Path
from typing import Any

import pyarrow.parquet as pq

from libs.backtest.scoring import compute_entry_score


FWD_HORIZONS = ["fwd_return_3d", "fwd_return_5d", "fwd_return_10d", "fwd_return_20d"]
HORIZON_LABELS = ["3d", "5d", "10d", "20d"]


def _load_rows(snapshot_dir: Path, split: str) -> list[dict[str, Any]]:
    table = pq.read_table(snapshot_dir / f"{split}.parquet")
    return table.to_pylist()


def _stats(values: list[float]) -> dict[str, float | None]:
    if not values:
        return {"mean": None, "median": None, "win_rate": None, "n": 0}
    return {
        "mean": statistics.mean(values),
        "median": statistics.median(values),
        "win_rate": sum(1 for v in values if v > 0) / len(values),
        "n": len(values),
    }


def _fmt(val: float | None, pct: bool = True) -> str:
    if val is None:
        return "     N/A"
    if pct:
        return f"{val:+.2%}"
    return f"{val:.1%}"


def _score_bucket(score: float) -> str:
    if score < 0.3:
        return "low(<0.3)"
    if score < 0.5:
        return "mid(0.3-0.5)"
    if score < 0.7:
        return "high(0.5-0.7)"
    return "top(>=0.7)"


BUCKET_ORDER = ["low(<0.3)", "mid(0.3-0.5)", "high(0.5-0.7)", "top(>=0.7)"]


def _is_intraday_filing(row: dict) -> bool:
    """Pre-market / regular-hours filing: reaction_date == event_date."""
    return row.get("reaction_date") == row.get("event_date")


# ── Experiment 1 ──────────────────────────────────────────────────────

def experiment_1(rows: list[dict[str, Any]]) -> None:
    """Stop-off + fixed holding period: raw forward returns."""
    print(f"\n{'='*100}")
    print("EXPERIMENT 1: Stop-off + Fixed Holding Period Exit")
    print(f"{'='*100}")

    # Enrich with score
    for r in rows:
        r["_score"] = compute_entry_score(r)
        r["_bucket"] = _score_bucket(r["_score"])

    # ── 1a. ALL events ──
    print(f"\n--- 1a. ALL events (N={len(rows)}) ---")
    _print_horizon_table("ALL", rows)

    # ── 1b. By score bucket ──
    print(f"\n--- 1b. By score bucket ---")
    by_bucket: dict[str, list] = {b: [] for b in BUCKET_ORDER}
    for r in rows:
        by_bucket[r["_bucket"]].append(r)

    header = f"{'Bucket':<16} {'N':>5}"
    for lbl in HORIZON_LABELS:
        header += f"  {lbl+'_mean':>8} {lbl+'_med':>8} {lbl+'_wr':>7}"
    print(header)
    print("-" * len(header))

    for bucket in BUCKET_ORDER:
        items = by_bucket[bucket]
        line = f"{bucket:<16} {len(items):>5}"
        for h in FWD_HORIZONS:
            vals = [float(r[h]) for r in items if r.get(h) is not None]
            s = _stats(vals)
            line += f"  {_fmt(s['mean']):>8} {_fmt(s['median']):>8} {_fmt(s['win_rate'], pct=False):>7}"
        print(line)

    # ── 1c. By event_type ──
    print(f"\n--- 1c. By event_type ---")
    by_type: dict[str, list] = {}
    for r in rows:
        et = r.get("event_type", "unknown")
        by_type.setdefault(et, []).append(r)

    header = f"{'EventType':<25} {'N':>5} {'AvgScr':>7}"
    for lbl in HORIZON_LABELS:
        header += f"  {lbl+'_mean':>8} {lbl+'_wr':>7}"
    print(header)
    print("-" * len(header))

    for et in sorted(by_type, key=lambda k: -len(by_type[k])):
        items = by_type[et]
        avg_score = statistics.mean(r["_score"] for r in items)
        line = f"{et:<25} {len(items):>5} {avg_score:>7.3f}"
        for h in FWD_HORIZONS:
            vals = [float(r[h]) for r in items if r.get(h) is not None]
            s = _stats(vals)
            line += f"  {_fmt(s['mean']):>8} {_fmt(s['win_rate'], pct=False):>7}"
        print(line)

    # ── 1d. MFE/MAE analysis (reward:risk) ──
    print(f"\n--- 1d. MFE/MAE analysis (max favorable / max adverse excursion) ---")
    mfe_mae_horizons = [("3d", "mfe_3d", "mae_3d"),
                        ("5d", "mfe_5d", "mae_5d"),
                        ("10d", "mfe_10d", "mae_10d"),
                        ("20d", "mfe_20d", "mae_20d")]

    header = f"{'Horizon':<10} {'MFE_mean':>10} {'MAE_mean':>10} {'R:R':>8} {'MFE_med':>10} {'MAE_med':>10}"
    print(header)
    print("-" * len(header))

    for lbl, mfe_col, mae_col in mfe_mae_horizons:
        mfe_vals = [float(r[mfe_col]) for r in rows if r.get(mfe_col) is not None]
        mae_vals = [float(r[mae_col]) for r in rows if r.get(mae_col) is not None]
        if mfe_vals and mae_vals:
            mfe_mean = statistics.mean(mfe_vals)
            mae_mean = statistics.mean(mae_vals)
            rr = mfe_mean / abs(mae_mean) if mae_mean != 0 else 0
            mfe_med = statistics.median(mfe_vals)
            mae_med = statistics.median(mae_vals)
            print(f"{lbl:<10} {mfe_mean:>+10.2%} {mae_mean:>+10.2%} {rr:>8.2f} {mfe_med:>+10.2%} {mae_med:>+10.2%}")

    # ── 1e. Score monotonicity per horizon ──
    print(f"\n--- 1e. Score monotonicity (does higher score → better return?) ---")
    for h, lbl in zip(FWD_HORIZONS, HORIZON_LABELS):
        bucket_means = []
        for bucket in BUCKET_ORDER:
            vals = [float(r[h]) for r in by_bucket[bucket] if r.get(h) is not None]
            bucket_means.append(statistics.mean(vals) if vals else None)

        valid_means = [m for m in bucket_means if m is not None]
        monotonic = all(a <= b for a, b in zip(valid_means, valid_means[1:])) if len(valid_means) >= 2 else False
        direction = "MONOTONIC" if monotonic else "NOT monotonic"
        means_str = " → ".join(f"{m:+.2%}" if m is not None else "N/A" for m in bucket_means)
        print(f"  {lbl}: {means_str}  [{direction}]")


# ── Experiment 2 ──────────────────────────────────────────────────────

def experiment_2(rows: list[dict[str, Any]]) -> None:
    """Reaction-close entry vs next-open entry."""
    print(f"\n{'='*100}")
    print("EXPERIMENT 2: Reaction-day Close Entry vs Next-Open Entry")
    print(f"{'='*100}")

    # Split by filing timing
    intraday = [r for r in rows if _is_intraday_filing(r)]
    after_close = [r for r in rows if not _is_intraday_filing(r)]

    print(f"\nFiling timing split:")
    print(f"  Pre-market / intraday (reaction_date == event_date): {len(intraday)}")
    print(f"  After-close (reaction_date > event_date):            {len(after_close)}")

    # ── 2a. Overnight gap analysis (intraday filings only) ──
    print(f"\n--- 2a. Overnight gap cost (intraday filings, N={len(intraday)}) ---")
    gaps = []
    for r in intraday:
        ep = r.get("entry_price")
        ec = r.get("event_close")
        if ep and ec and ec > 0:
            gap = ep / ec - 1.0
            gaps.append(gap)

    if gaps:
        s = _stats(gaps)
        print(f"  Overnight gap (next_open / reaction_close - 1):")
        print(f"    Mean:   {_fmt(s['mean'])}")
        print(f"    Median: {_fmt(s['median'])}")
        print(f"    Gap up rate (open > close): {_fmt(s['win_rate'], pct=False)}")
        print(f"    Std:    {statistics.stdev(gaps):+.2%}" if len(gaps) > 1 else "")

        # Distribution
        gap_buckets = {"< -2%": 0, "-2% to 0%": 0, "0% to +1%": 0,
                       "+1% to +3%": 0, "+3% to +5%": 0, "> +5%": 0}
        for g in gaps:
            if g < -0.02:
                gap_buckets["< -2%"] += 1
            elif g < 0:
                gap_buckets["-2% to 0%"] += 1
            elif g < 0.01:
                gap_buckets["0% to +1%"] += 1
            elif g < 0.03:
                gap_buckets["+1% to +3%"] += 1
            elif g < 0.05:
                gap_buckets["+3% to +5%"] += 1
            else:
                gap_buckets["> +5%"] += 1

        print(f"\n  Gap distribution:")
        for label, count in gap_buckets.items():
            pct = count / len(gaps) * 100
            bar = "#" * int(pct / 2)
            print(f"    {label:>12}: {count:>4} ({pct:5.1f}%) {bar}")

    # ── 2b. Close entry vs next-open returns (intraday only) ──
    print(f"\n--- 2b. Close entry vs Next-open entry returns (intraday, N={len(intraday)}) ---")

    header = f"{'Horizon':<10} {'NextOpen_mean':>13} {'NextOpen_wr':>11} {'CloseEntry_mean':>15} {'CloseEntry_wr':>13} {'Diff':>8}"
    print(header)
    print("-" * len(header))

    for h, lbl in zip(FWD_HORIZONS, HORIZON_LABELS):
        next_open_vals = []
        close_entry_vals = []

        for r in intraday:
            fwd = r.get(h)
            ep = r.get("entry_price")
            ec = r.get("event_close")
            if fwd is not None and ep and ec and ec > 0:
                fwd_f = float(fwd)
                next_open_vals.append(fwd_f)
                # close_entry_return = (entry_price / event_close) * (1 + fwd) - 1
                close_ret = (ep / ec) * (1.0 + fwd_f) - 1.0
                close_entry_vals.append(close_ret)

        no_s = _stats(next_open_vals)
        ce_s = _stats(close_entry_vals)
        diff = (ce_s["mean"] - no_s["mean"]) if ce_s["mean"] is not None and no_s["mean"] is not None else None
        diff_str = _fmt(diff) if diff is not None else "     N/A"

        print(f"{lbl:<10} {_fmt(no_s['mean']):>13} {_fmt(no_s['win_rate'], pct=False):>11}"
              f" {_fmt(ce_s['mean']):>15} {_fmt(ce_s['win_rate'], pct=False):>13} {diff_str:>8}")

    # ── 2c. After-close filings (separate analysis) ──
    if after_close:
        print(f"\n--- 2c. After-close filings (N={len(after_close)}) ---")
        print(f"  (These events filed after market close; reaction_date = next trading day)")

        header = f"{'Horizon':<10} {'NextOpen_mean':>13} {'NextOpen_wr':>11}"
        print(f"  {header}")
        print(f"  {'-' * len(header)}")

        for h, lbl in zip(FWD_HORIZONS, HORIZON_LABELS):
            vals = [float(r[h]) for r in after_close if r.get(h) is not None]
            s = _stats(vals)
            print(f"  {lbl:<10} {_fmt(s['mean']):>13} {_fmt(s['win_rate'], pct=False):>11}")

    # ── 2d. Close entry by event type (intraday only) ──
    print(f"\n--- 2d. Close-entry returns by event type (intraday only) ---")
    by_type: dict[str, list] = {}
    for r in intraday:
        et = r.get("event_type", "unknown")
        by_type.setdefault(et, []).append(r)

    header = f"{'EventType':<25} {'N':>5} {'Gap_mean':>9}"
    for lbl in HORIZON_LABELS:
        header += f"  {lbl+'_CE':>8} {lbl+'_NO':>8}"
    print(header)
    print("-" * len(header))

    for et in sorted(by_type, key=lambda k: -len(by_type[k])):
        items = by_type[et]
        # Gap
        type_gaps = []
        for r in items:
            ep = r.get("entry_price")
            ec = r.get("event_close")
            if ep and ec and ec > 0:
                type_gaps.append(ep / ec - 1.0)

        gap_mean = statistics.mean(type_gaps) if type_gaps else None
        line = f"{et:<25} {len(items):>5} {_fmt(gap_mean):>9}"

        for h in FWD_HORIZONS:
            ce_vals = []
            no_vals = []
            for r in items:
                fwd = r.get(h)
                ep = r.get("entry_price")
                ec = r.get("event_close")
                if fwd is not None and ep and ec and ec > 0:
                    fwd_f = float(fwd)
                    no_vals.append(fwd_f)
                    ce_vals.append((ep / ec) * (1.0 + fwd_f) - 1.0)
            ce_s = _stats(ce_vals)
            no_s = _stats(no_vals)
            line += f"  {_fmt(ce_s['mean']):>8} {_fmt(no_s['mean']):>8}"
        print(line)

    # ── 2e. Score-filtered close entry (score >= 0.5, intraday) ──
    filtered = [r for r in intraday if r.get("_score", 0) >= 0.5]
    if filtered:
        print(f"\n--- 2e. Score-filtered (>= 0.5) close entry (intraday, N={len(filtered)}) ---")

        header = f"{'Horizon':<10} {'CloseEntry_mean':>15} {'CE_wr':>8} {'NextOpen_mean':>14} {'NO_wr':>8}"
        print(header)
        print("-" * len(header))

        for h, lbl in zip(FWD_HORIZONS, HORIZON_LABELS):
            ce_vals = []
            no_vals = []
            for r in filtered:
                fwd = r.get(h)
                ep = r.get("entry_price")
                ec = r.get("event_close")
                if fwd is not None and ep and ec and ec > 0:
                    fwd_f = float(fwd)
                    no_vals.append(fwd_f)
                    ce_vals.append((ep / ec) * (1.0 + fwd_f) - 1.0)
            ce_s = _stats(ce_vals)
            no_s = _stats(no_vals)
            print(f"{lbl:<10} {_fmt(ce_s['mean']):>15} {_fmt(ce_s['win_rate'], pct=False):>8}"
                  f" {_fmt(no_s['mean']):>14} {_fmt(no_s['win_rate'], pct=False):>8}")


def _print_horizon_table(label: str, items: list[dict]) -> None:
    header = f"{'Horizon':<10} {'Mean':>10} {'Median':>10} {'WinRate':>8} {'N':>6} {'StdDev':>10}"
    print(header)
    print("-" * len(header))
    for h, lbl in zip(FWD_HORIZONS, HORIZON_LABELS):
        vals = [float(r[h]) for r in items if r.get(h) is not None]
        s = _stats(vals)
        std = statistics.stdev(vals) if len(vals) > 1 else 0
        print(f"{lbl:<10} {_fmt(s['mean']):>10} {_fmt(s['median']):>10}"
              f" {_fmt(s['win_rate'], pct=False):>8} {s['n']:>6} {std:>10.2%}")


def main() -> None:
    parser = argparse.ArgumentParser(description="Entry Timing Experiments")
    parser.add_argument("--snapshot-dir", required=True)
    parser.add_argument("--split", default="train")
    args = parser.parse_args()

    rows = _load_rows(Path(args.snapshot_dir), args.split)
    if not rows:
        print("No data found.")
        return

    print(f"Loaded {len(rows)} events from {args.split} split")

    experiment_1(rows)
    experiment_2(rows)

    # ── Summary ──
    print(f"\n{'='*100}")
    print("KEY DIAGNOSTIC QUESTIONS")
    print(f"{'='*100}")
    print("""
  Exp 1 — Time-based alpha:
    • Are any fixed-horizon returns consistently positive?
    • Does score monotonicity hold at any horizon?
    • Which event types carry actual alpha (mean > 0, win rate > 50%)?

  Exp 2 — Entry timing:
    • Is the overnight gap systematically positive (eating the edge)?
    • Does close-entry improve returns vs next-open?
    • Is the gap effect uniform across event types, or concentrated?
""")


if __name__ == "__main__":
    main()
