"""Management Change Alpha Analysis — Phase 1 validation.

Empirically tests whether management_change events produce tradeable
PEAD-like signals (initial under-reaction followed by drift).

Analyses:
  1. Directional follow-through decomposition by initial reaction sign
  2. Forward return profiles (mean, median, win rate) by holding period
  3. MAE/MFE profiles
  4. Filter effect simulation (document_quality, volume_ratio, market_cap)
  5. Temporal distribution (monthly, quarterly, earnings-season overlap)

Usage:
  python -m apps.tools.management_change_analysis \
    --snapshot-dir data/datasets/snapshots/midlarge-liquid-long-v1
"""
from __future__ import annotations

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

import pyarrow.parquet as pq


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


def _safe_float(raw: Any) -> float | None:
    try:
        return float(raw)
    except (TypeError, ValueError):
        return None


def _win_rate(values: list[float]) -> float:
    if not values:
        return 0.0
    return sum(1 for v in values if v > 0) / len(values)


def _pct(value: float) -> str:
    return f"{value * 100:+.2f}%"


def _fmt_pct(value: float) -> str:
    return f"{value:.1%}"


# ───────────────────────────────────────────────────────────────────
# Analysis 1: Directional follow-through
# ───────────────────────────────────────────────────────────────────

def analyze_follow_through(mc_rows: list[dict]) -> None:
    """Decompose follow-through by initial reaction direction."""
    print("\n" + "=" * 70)
    print("1. DIRECTIONAL FOLLOW-THROUGH DECOMPOSITION")
    print("=" * 70)

    horizons = [
        ("fwd_return_1d", "1d"),
        ("fwd_return_3d", "3d"),
        ("fwd_return_5d", "5d"),
        ("fwd_return_10d", "10d"),
        ("fwd_return_20d", "20d"),
    ]

    groups = {
        "bullish_reaction (ret > 0)": [r for r in mc_rows if (_safe_float(r.get("reaction_day_return")) or 0) > 0],
        "bearish_reaction (ret < 0)": [r for r in mc_rows if (_safe_float(r.get("reaction_day_return")) or 0) < 0],
        "flat_reaction (ret == 0)": [r for r in mc_rows if (_safe_float(r.get("reaction_day_return")) or 0) == 0],
    }

    for group_name, rows in groups.items():
        if not rows:
            continue
        print(f"\n  {group_name}: N={len(rows)}")
        print(f"  {'Horizon':<10} {'Mean':>10} {'Median':>10} {'WinRate':>10} {'StdDev':>10}")
        print(f"  {'-'*50}")
        for col, label in horizons:
            vals = [float(r[col]) for r in rows if r.get(col) is not None]
            if not vals:
                continue
            mean = statistics.mean(vals)
            median = statistics.median(vals)
            wr = _win_rate(vals)
            sd = statistics.stdev(vals) if len(vals) > 1 else 0.0
            print(f"  {label:<10} {_pct(mean):>10} {_pct(median):>10} {_fmt_pct(wr):>10} {_pct(sd):>10}")


# ───────────────────────────────────────────────────────────────────
# Analysis 2: Return profile
# ───────────────────────────────────────────────────────────────────

def analyze_return_profile(mc_rows: list[dict]) -> None:
    """Forward return statistics at each horizon."""
    print("\n" + "=" * 70)
    print("2. RETURN PROFILE (all management_change)")
    print("=" * 70)
    print(f"  Total events: {len(mc_rows)}")

    horizons = [
        ("fwd_return_1d", "1d"),
        ("fwd_return_3d", "3d"),
        ("fwd_return_5d", "5d"),
        ("fwd_return_10d", "10d"),
        ("fwd_return_20d", "20d"),
    ]

    print(f"\n  {'Horizon':<10} {'Mean':>10} {'Median':>10} {'WinRate':>10} {'P25':>10} {'P75':>10}")
    print(f"  {'-'*60}")
    for col, label in horizons:
        vals = sorted([float(r[col]) for r in mc_rows if r.get(col) is not None])
        if not vals:
            continue
        mean = statistics.mean(vals)
        median = statistics.median(vals)
        wr = _win_rate(vals)
        p25 = vals[len(vals) // 4]
        p75 = vals[3 * len(vals) // 4]
        print(f"  {label:<10} {_pct(mean):>10} {_pct(median):>10} {_fmt_pct(wr):>10} {_pct(p25):>10} {_pct(p75):>10}")


# ───────────────────────────────────────────────────────────────────
# Analysis 3: MAE/MFE profile
# ───────────────────────────────────────────────────────────────────

def analyze_mae_mfe(mc_rows: list[dict]) -> None:
    """Maximum Adverse/Favorable Excursion by horizon."""
    print("\n" + "=" * 70)
    print("3. MAE / MFE PROFILE")
    print("=" * 70)

    windows = [("3d", "mae_3d", "mfe_3d"), ("5d", "mae_5d", "mfe_5d"),
               ("10d", "mae_10d", "mfe_10d"), ("20d", "mae_20d", "mfe_20d")]

    print(f"\n  {'Window':<8} {'MAE_mean':>10} {'MAE_med':>10} {'MFE_mean':>10} {'MFE_med':>10} {'MFE/MAE':>10}")
    print(f"  {'-'*58}")
    for label, mae_col, mfe_col in windows:
        mae_vals = [abs(float(r[mae_col])) for r in mc_rows if r.get(mae_col) is not None]
        mfe_vals = [float(r[mfe_col]) for r in mc_rows if r.get(mfe_col) is not None]
        if not mae_vals or not mfe_vals:
            continue
        mae_mean = statistics.mean(mae_vals)
        mae_med = statistics.median(mae_vals)
        mfe_mean = statistics.mean(mfe_vals)
        mfe_med = statistics.median(mfe_vals)
        ratio = mfe_mean / mae_mean if mae_mean > 0 else 0
        print(f"  {label:<8} {_pct(mae_mean):>10} {_pct(mae_med):>10} {_pct(mfe_mean):>10} {_pct(mfe_med):>10} {ratio:>10.2f}")


# ───────────────────────────────────────────────────────────────────
# Analysis 4: Filter effect simulation
# ───────────────────────────────────────────────────────────────────

def analyze_filter_effects(mc_rows: list[dict]) -> None:
    """Test how filters affect count and follow-through quality."""
    print("\n" + "=" * 70)
    print("4. FILTER EFFECT SIMULATION")
    print("=" * 70)

    filters = [
        ("No filter (baseline)", lambda r: True),
        ("reaction_day_return > 0 (bullish)", lambda r: (_safe_float(r.get("reaction_day_return")) or 0) > 0),
        ("reaction_day_return > 0.02", lambda r: (_safe_float(r.get("reaction_day_return")) or 0) > 0.02),
        ("reaction_day_return > 0.03", lambda r: (_safe_float(r.get("reaction_day_return")) or 0) > 0.03),
        ("reaction_day_return > 0.05", lambda r: (_safe_float(r.get("reaction_day_return")) or 0) > 0.05),
        ("document_quality >= 0.5", lambda r: (_safe_float(r.get("document_quality_score")) or 0) >= 0.5),
        ("volume_ratio >= 1.5x", lambda r: (_safe_float(r.get("volume_ratio_20d")) or 0) >= 1.5),
        ("volume_ratio >= 1.2x", lambda r: (_safe_float(r.get("volume_ratio_20d")) or 0) >= 1.2),
        ("market_cap >= $2B", lambda r: (_safe_float(r.get("market_cap_proxy")) or 0) >= 2e9),
        ("market_cap >= $5B", lambda r: (_safe_float(r.get("market_cap_proxy")) or 0) >= 5e9),
        ("close_location >= 0.5", lambda r: (_safe_float(r.get("close_location")) or 0) >= 0.5),
        ("close_location >= 0.6", lambda r: (_safe_float(r.get("close_location")) or 0) >= 0.6),
        # Combined filters
        ("bullish + doc>=0.5", lambda r: (
            (_safe_float(r.get("reaction_day_return")) or 0) > 0
            and (_safe_float(r.get("document_quality_score")) or 0) >= 0.5
        )),
        ("bullish + vol>=1.2", lambda r: (
            (_safe_float(r.get("reaction_day_return")) or 0) > 0
            and (_safe_float(r.get("volume_ratio_20d")) or 0) >= 1.2
        )),
        ("bullish + close>=0.5", lambda r: (
            (_safe_float(r.get("reaction_day_return")) or 0) > 0
            and (_safe_float(r.get("close_location")) or 0) >= 0.5
        )),
        ("bullish + doc>=0.5 + close>=0.5", lambda r: (
            (_safe_float(r.get("reaction_day_return")) or 0) > 0
            and (_safe_float(r.get("document_quality_score")) or 0) >= 0.5
            and (_safe_float(r.get("close_location")) or 0) >= 0.5
        )),
        ("bullish + doc>=0.5 + vol>=1.2", lambda r: (
            (_safe_float(r.get("reaction_day_return")) or 0) > 0
            and (_safe_float(r.get("document_quality_score")) or 0) >= 0.5
            and (_safe_float(r.get("volume_ratio_20d")) or 0) >= 1.2
        )),
        ("ret>0.03 + doc>=0.5", lambda r: (
            (_safe_float(r.get("reaction_day_return")) or 0) > 0.03
            and (_safe_float(r.get("document_quality_score")) or 0) >= 0.5
        )),
        ("ret>0.03 + close>=0.5", lambda r: (
            (_safe_float(r.get("reaction_day_return")) or 0) > 0.03
            and (_safe_float(r.get("close_location")) or 0) >= 0.5
        )),
    ]

    horizons = ["fwd_return_5d", "fwd_return_10d", "fwd_return_20d"]
    h_labels = ["5d", "10d", "20d"]

    header = f"  {'Filter':<42} {'N':>5}"
    for h in h_labels:
        header += f" {'WR_' + h:>8} {'Mean_' + h:>10}"
    header += f" {'AnnFreq':>8}"
    print(f"\n{header}")
    print(f"  {'-' * (len(header) - 2)}")

    # Compute date range for annualization
    dates = sorted(set(r.get("event_date", "") for r in mc_rows if r.get("event_date")))
    if len(dates) >= 2:
        from datetime import datetime
        d0 = datetime.strptime(str(dates[0])[:10], "%Y-%m-%d")
        d1 = datetime.strptime(str(dates[-1])[:10], "%Y-%m-%d")
        years = max((d1 - d0).days / 365.25, 0.5)
    else:
        years = 1.0

    for name, fn in filters:
        filtered = [r for r in mc_rows if fn(r)]
        n = len(filtered)
        ann_freq = n / years

        row_str = f"  {name:<42} {n:>5}"
        for h_col in horizons:
            vals = [float(r[h_col]) for r in filtered if r.get(h_col) is not None]
            if vals:
                wr = _win_rate(vals)
                mean = statistics.mean(vals)
                row_str += f" {_fmt_pct(wr):>8} {_pct(mean):>10}"
            else:
                row_str += f" {'N/A':>8} {'N/A':>10}"
        row_str += f" {ann_freq:>7.1f}"
        print(row_str)


# ───────────────────────────────────────────────────────────────────
# Analysis 5: Temporal distribution
# ───────────────────────────────────────────────────────────────────

def analyze_temporal_distribution(mc_rows: list[dict]) -> None:
    """Monthly and quarterly frequency, earnings season overlap."""
    print("\n" + "=" * 70)
    print("5. TEMPORAL DISTRIBUTION")
    print("=" * 70)

    from datetime import datetime

    months: dict[str, int] = {}
    quarters: dict[str, int] = {}
    earnings_months = {1, 2, 4, 5, 7, 8, 10, 11}  # typical earnings season months

    in_season = 0
    out_season = 0

    for r in mc_rows:
        ed = str(r.get("event_date", ""))[:10]
        if len(ed) < 7:
            continue
        try:
            dt = datetime.strptime(ed, "%Y-%m-%d")
        except ValueError:
            continue

        ym = f"{dt.year}-{dt.month:02d}"
        yq = f"{dt.year}-Q{(dt.month - 1) // 3 + 1}"

        months[ym] = months.get(ym, 0) + 1
        quarters[yq] = quarters.get(yq, 0) + 1

        if dt.month in earnings_months:
            in_season += 1
        else:
            out_season += 1

    total = in_season + out_season

    print(f"\n  Earnings season months (Jan/Feb/Apr/May/Jul/Aug/Oct/Nov): {in_season} ({in_season/total:.0%})")
    print(f"  Non-earnings months (Mar/Jun/Sep/Dec): {out_season} ({out_season/total:.0%})")
    print(f"  --> Temporal diversification: {'GOOD' if out_season / total >= 0.20 else 'POOR'}")

    print(f"\n  Quarterly distribution:")
    for q in sorted(quarters.keys()):
        bar = "#" * quarters[q]
        print(f"    {q}: {quarters[q]:>3}  {bar}")

    # Monthly avg
    if months:
        avg_per_month = statistics.mean(months.values())
        print(f"\n  Average events per month: {avg_per_month:.1f}")
        print(f"  Total months with events: {len(months)}/{len(months)}")
        print(f"  Min/Max per month: {min(months.values())}/{max(months.values())}")


# ───────────────────────────────────────────────────────────────────
# Analysis 6: Comparison vs earnings_release baseline
# ───────────────────────────────────────────────────────────────────

def analyze_vs_earnings(mc_rows: list[dict], all_rows: list[dict]) -> None:
    """Compare MC follow-through to earnings_release baseline."""
    print("\n" + "=" * 70)
    print("6. MANAGEMENT_CHANGE vs EARNINGS_RELEASE COMPARISON")
    print("=" * 70)

    er_rows = [r for r in all_rows if r.get("event_type") == "earnings_release"]

    # Bullish subsets
    mc_bull = [r for r in mc_rows if (_safe_float(r.get("reaction_day_return")) or 0) > 0]
    er_bull = [r for r in er_rows if (_safe_float(r.get("reaction_day_return")) or 0) > 0]

    horizons = [
        ("fwd_return_5d", "5d"),
        ("fwd_return_10d", "10d"),
        ("fwd_return_20d", "20d"),
    ]

    print(f"\n  ALL events:")
    print(f"  {'Metric':<20} {'MC (N={len(mc_rows)})':>20} {'ER (N={len(er_rows)})':>20}")
    print(f"  {'-'*60}")
    for col, label in horizons:
        mc_vals = [float(r[col]) for r in mc_rows if r.get(col) is not None]
        er_vals = [float(r[col]) for r in er_rows if r.get(col) is not None]
        mc_wr = _win_rate(mc_vals) if mc_vals else 0
        er_wr = _win_rate(er_vals) if er_vals else 0
        mc_mean = statistics.mean(mc_vals) if mc_vals else 0
        er_mean = statistics.mean(er_vals) if er_vals else 0
        print(f"  {label + ' WR':<20} {_fmt_pct(mc_wr):>20} {_fmt_pct(er_wr):>20}")
        print(f"  {label + ' Mean':<20} {_pct(mc_mean):>20} {_pct(er_mean):>20}")

    print(f"\n  BULLISH reaction only (ret > 0):")
    print(f"  {'Metric':<20} {'MC (N={len(mc_bull)})':>20} {'ER (N={len(er_bull)})':>20}")
    print(f"  {'-'*60}")
    for col, label in horizons:
        mc_vals = [float(r[col]) for r in mc_bull if r.get(col) is not None]
        er_vals = [float(r[col]) for r in er_bull if r.get(col) is not None]
        mc_wr = _win_rate(mc_vals) if mc_vals else 0
        er_wr = _win_rate(er_vals) if er_vals else 0
        mc_mean = statistics.mean(mc_vals) if mc_vals else 0
        er_mean = statistics.mean(er_vals) if er_vals else 0
        print(f"  {label + ' WR':<20} {_fmt_pct(mc_wr):>20} {_fmt_pct(er_wr):>20}")
        print(f"  {label + ' Mean':<20} {_pct(mc_mean):>20} {_pct(er_mean):>20}")


# ───────────────────────────────────────────────────────────────────
# Analysis 7: Reaction magnitude buckets
# ───────────────────────────────────────────────────────────────────

def analyze_reaction_buckets(mc_rows: list[dict]) -> None:
    """Break down follow-through by reaction magnitude bucket."""
    print("\n" + "=" * 70)
    print("7. FOLLOW-THROUGH BY REACTION MAGNITUDE BUCKET")
    print("=" * 70)

    buckets = [
        ("ret < -5%", lambda r: (_safe_float(r.get("reaction_day_return")) or 0) < -0.05),
        ("-5% <= ret < -2%", lambda r: -0.05 <= (_safe_float(r.get("reaction_day_return")) or 0) < -0.02),
        ("-2% <= ret < 0%", lambda r: -0.02 <= (_safe_float(r.get("reaction_day_return")) or 0) < 0),
        ("0% <= ret < 2%", lambda r: 0 <= (_safe_float(r.get("reaction_day_return")) or 0) < 0.02),
        ("2% <= ret < 5%", lambda r: 0.02 <= (_safe_float(r.get("reaction_day_return")) or 0) < 0.05),
        ("5% <= ret < 10%", lambda r: 0.05 <= (_safe_float(r.get("reaction_day_return")) or 0) < 0.10),
        ("ret >= 10%", lambda r: (_safe_float(r.get("reaction_day_return")) or 0) >= 0.10),
    ]

    print(f"\n  {'Bucket':<22} {'N':>5} {'5d_WR':>8} {'5d_Mean':>10} {'10d_WR':>8} {'10d_Mean':>10} {'20d_WR':>8} {'20d_Mean':>10}")
    print(f"  {'-'*90}")

    for name, fn in buckets:
        filtered = [r for r in mc_rows if fn(r)]
        n = len(filtered)
        if n == 0:
            continue

        parts = f"  {name:<22} {n:>5}"
        for col in ["fwd_return_5d", "fwd_return_10d", "fwd_return_20d"]:
            vals = [float(r[col]) for r in filtered if r.get(col) is not None]
            if vals:
                wr = _win_rate(vals)
                mean = statistics.mean(vals)
                parts += f" {_fmt_pct(wr):>8} {_pct(mean):>10}"
            else:
                parts += f" {'N/A':>8} {'N/A':>10}"
        print(parts)


# ───────────────────────────────────────────────────────────────────
# Judgment summary
# ───────────────────────────────────────────────────────────────────

def print_judgment(mc_rows: list[dict], years: float) -> None:
    """Evaluate against Phase 1 pass/fail criteria."""
    print("\n" + "=" * 70)
    print("PHASE 1 JUDGMENT CRITERIA")
    print("=" * 70)

    # Criterion 1: Bullish reaction -> 5d forward return positive >= 55%
    bullish = [r for r in mc_rows if (_safe_float(r.get("reaction_day_return")) or 0) > 0]
    bull_5d = [float(r["fwd_return_5d"]) for r in bullish if r.get("fwd_return_5d") is not None]
    bull_wr = _win_rate(bull_5d) if bull_5d else 0

    # Criterion 2: Filtered events >= 30/year
    # Use "bullish + doc>=0.5" as a reasonable filter combo
    filtered = [r for r in mc_rows
                if (_safe_float(r.get("reaction_day_return")) or 0) > 0
                and (_safe_float(r.get("document_quality_score")) or 0) >= 0.5]
    ann_freq = len(filtered) / years

    # Criterion 3: Temporal diversification (non-earnings-season >= 20%)
    from datetime import datetime
    earnings_months = {1, 2, 4, 5, 7, 8, 10, 11}
    out_season = 0
    total = 0
    for r in mc_rows:
        ed = str(r.get("event_date", ""))[:10]
        try:
            dt = datetime.strptime(ed, "%Y-%m-%d")
            total += 1
            if dt.month not in earnings_months:
                out_season += 1
        except ValueError:
            pass
    temporal_div = out_season / total if total > 0 else 0

    # Criterion 4: Win rate >= 52% (all bullish MC at 5d)
    overall_wr = bull_wr  # same as criterion 1

    # Print results
    c1_pass = bull_wr >= 0.55
    c2_pass = ann_freq >= 30
    c3_pass = temporal_div >= 0.20
    c4_pass = overall_wr >= 0.52
    all_pass = c1_pass and c2_pass and c3_pass and c4_pass

    def _status(ok: bool) -> str:
        return "PASS" if ok else "FAIL"

    print(f"\n  1. Bullish reaction -> 5d WR >= 55%:   {_fmt_pct(bull_wr):>8}  [{_status(c1_pass)}]")
    print(f"  2. Filtered events >= 30/year:          {ann_freq:>7.1f}  [{_status(c2_pass)}]")
    print(f"  3. Temporal diversification >= 20%:     {_fmt_pct(temporal_div):>8}  [{_status(c3_pass)}]")
    print(f"  4. Win rate >= 52%:                     {_fmt_pct(overall_wr):>8}  [{_status(c4_pass)}]")
    print(f"\n  OVERALL: {'>>> PROCEED TO PHASE 2 <<<' if all_pass else '>>> DOES NOT PASS — review filter combos above <<<'}")

    # Also check best filter combo that might pass
    if not all_pass:
        print(f"\n  NOTE: Check Section 4 filter combos for alternative thresholds that may pass.")


# ───────────────────────────────────────────────────────────────────
# Main
# ───────────────────────────────────────────────────────────────────

def main() -> None:
    parser = argparse.ArgumentParser(description="Management Change Alpha Analysis")
    parser.add_argument("--snapshot-dir", type=str,
                        default="data/datasets/snapshots/midlarge-liquid-long-v1")
    parser.add_argument("--split", type=str, default="train",
                        help="Which split to analyze (default: train)")
    args = parser.parse_args()

    snapshot_dir = Path(args.snapshot_dir)
    print(f"Loading {args.split} from {snapshot_dir} ...")

    all_rows = _load_rows(snapshot_dir, args.split)
    mc_rows = [r for r in all_rows if r.get("event_type") == "management_change"]

    print(f"Total events: {len(all_rows)}")
    print(f"management_change events: {len(mc_rows)}")

    if not mc_rows:
        print("No management_change events found. Exiting.")
        return

    # Compute years for annualization
    from datetime import datetime
    dates = sorted(set(str(r.get("event_date", ""))[:10] for r in mc_rows))
    dates = [d for d in dates if len(d) >= 10]
    if len(dates) >= 2:
        d0 = datetime.strptime(dates[0], "%Y-%m-%d")
        d1 = datetime.strptime(dates[-1], "%Y-%m-%d")
        years = max((d1 - d0).days / 365.25, 0.5)
    else:
        years = 1.0
    print(f"Date range: {dates[0]} to {dates[-1]} ({years:.1f} years)")

    analyze_follow_through(mc_rows)
    analyze_return_profile(mc_rows)
    analyze_mae_mfe(mc_rows)
    analyze_filter_effects(mc_rows)
    analyze_temporal_distribution(mc_rows)
    analyze_vs_earnings(mc_rows, all_rows)
    analyze_reaction_buckets(mc_rows)
    print_judgment(mc_rows, years)


if __name__ == "__main__":
    main()
