#!/usr/bin/env python3
"""Train LightGBM ML scorer for PEAD long, save booster + feature manifest.

Outputs:
- data/ml_models/lgbm_v1_pead_long.txt           (LGBM booster, text format)
- data/ml_models/lgbm_v1_pead_long_manifest.json (feature schema + category vocab)
"""
from __future__ import annotations

import json
from pathlib import Path

import numpy as np
import pandas as pd
import pyarrow.parquet as pq
from lightgbm import LGBMClassifier

NUMERIC = [
    "reaction_day_return", "gap_size", "close_location", "volume_ratio_20d",
    "document_quality_score", "parse_confidence_overall", "oneoff_penalty",
    "market_cap_proxy", "avg_dollar_volume_20d",
    "pre_event_volatility_20d", "pre_event_rsi_14", "pre_event_bb_position",
    "pre_event_obv_slope_20d",
    "macro_vix", "macro_hy_spread", "macro_t10y2y", "prior_event_fwd5d",
    "lm_positive_pct", "lm_negative_pct", "lm_net_sentiment",
    "earnings_surprise_pct",
    "sue_lag_1_pct", "sue_lag_2_pct", "sue_lag_3_pct",
    "sue_hist_mean_4q", "sue_hist_mean_8q", "sue_hist_mean_12q",
    "sue_hist_pos_rate_4q", "sue_hist_pos_rate_12q",
    "sue_hist_latest_pct", "sue_hist_streak_pos",
    "peer_sector_event_count_365d", "peer_sector_surprise_median_365d",
    "peer_sector_surprise_mean_365d", "peer_sector_surprise_pos_rate_365d",
    "peer_relative_surprise_pct_365d",
    "peer_sector_sue_hist_mean_4q_median_365d",
    "peer_sector_sue_hist_mean_4q_mean_365d",
    "peer_relative_sue_hist_mean_4q_365d",
    "peer_sector_sue_hist_pos_rate_4q_mean_365d",
    "pre_event_short_ratio", "pre_event_sector_momentum_20d",
    "prior_catalyst_count_60d", "prior_catalyst_type_diversity_60d",
    "price_vs_sma20", "pre_event_momentum_20d",
]
CAT = ["event_type", "event_direction", "guidance_status", "sector"]
SNAPSHOT = Path("data/parquet/midlarge-liquid-long-v1_bucketfix_full_audit_canonical_ftb_fix_v2")
OUT_DIR = Path("data/ml_models")


def load(split: str) -> pd.DataFrame:
    schema = pq.read_schema(SNAPSHOT / f"{split}.parquet")
    cols = [c for c in NUMERIC + CAT + ["mfe_10d", "mae_10d", "fwd_return_10d"] if c in schema.names]
    df = pq.read_table(SNAPSHOT / f"{split}.parquet", columns=cols).to_pandas()
    for c in NUMERIC:
        if c not in df.columns:
            df[c] = np.nan
        df[c] = pd.to_numeric(df[c], errors="coerce")
    for c in CAT:
        if c not in df.columns:
            df[c] = "missing"
        df[c] = df[c].fillna("missing").astype(str)
    df["target"] = ((df["mfe_10d"] - df["mae_10d"]) > 0.08).astype(int)
    return df


def main() -> None:
    OUT_DIR.mkdir(parents=True, exist_ok=True)
    train = load("train")

    cat_vocab = {c: sorted(train[c].dropna().unique().tolist()) for c in CAT}
    # encode categories as integer codes for stable serialisation
    for c in CAT:
        codes = {v: i for i, v in enumerate(cat_vocab[c])}
        train[c] = train[c].map(codes).fillna(-1).astype("int32")
    feats = NUMERIC + CAT
    model = LGBMClassifier(
        n_estimators=400, learning_rate=0.03, num_leaves=31,
        min_child_samples=50, subsample=0.8, colsample_bytree=0.8,
        objective="binary", random_state=42, verbosity=-1,
    )
    model.fit(train[feats], train["target"], categorical_feature=CAT)

    booster = model.booster_
    model_path = OUT_DIR / "lgbm_v1_pead_long.txt"
    booster.save_model(str(model_path))

    manifest = {
        "numeric_features": NUMERIC,
        "categorical_features": CAT,
        "categorical_vocab": cat_vocab,
        "snapshot": str(SNAPSHOT),
        "n_estimators": 400,
        "lgbm_version": __import__("lightgbm").__version__,
    }
    manifest_path = OUT_DIR / "lgbm_v1_pead_long_manifest.json"
    manifest_path.write_text(json.dumps(manifest, indent=2))

    # quick eval: re-predict on train + valid + test
    for split in ["train", "valid", "test"]:
        df = load(split)
        for c in CAT:
            codes = {v: i for i, v in enumerate(cat_vocab[c])}
            df[c] = df[c].map(codes).fillna(-1).astype("int32")
        prob = model.predict_proba(df[feats])[:, 1]
        print(f"{split}: mean={prob.mean():.4f} std={prob.std():.4f} p10={np.percentile(prob,10):.3f} p90={np.percentile(prob,90):.3f}")

    print(f"\nSaved booster: {model_path}")
    print(f"Saved manifest: {manifest_path}")


if __name__ == "__main__":
    main()
