#!/usr/bin/env python3
"""Round 3 - Targeted single-pass optimization per league."""
from __future__ import annotations

import json
import sys
import warnings
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import List, Tuple

import numpy as np
import pandas as pd
from sklearn.base import clone
from sklearn.ensemble import (
    ExtraTreesClassifier, GradientBoostingClassifier, HistGradientBoostingClassifier,
    RandomForestClassifier, AdaBoostClassifier
)
from sklearn.feature_selection import mutual_info_classif
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import FunctionTransformer, StandardScaler

SCRIPTS_DIR = Path(__file__).resolve().parent
REPO_ROOT = SCRIPTS_DIR.parent
sys.path.insert(0, str(SCRIPTS_DIR))
sys.path.insert(0, str(REPO_ROOT))

from optimize_priority_models import create_enhanced_features
from train_models import LEAGUES, create_features

OUTPUT_DIR = REPO_ROOT / 'models' / '1year_V1_optimized'
CUTOFF_DATE = '2025-09-03'

LEAGUES_TO_OPTIMIZE = {
    'e2': ('英冠', 0.7143, 'RF_d6_n500', 0.59),
    'tr1': ('土超', 0.7059, 'ADA_d3', 0.56),
    'e3': ('英格兰第五级别', 0.6923, 'SVM_1', 0.52),
    'e1': ('英甲', 0.6667, 'RF_d6_n500', 0.58),
    'p1': ('葡超', 0.6667, 'RF_d6_n500', 0.69),
    'i1': ('意甲', 0.6667, 'LR_C05', 0.87),
    'br1': ('巴甲', 0.6571, 'ET_full_n900', 0.25),
    'a1': ('澳超', 0.6000, 'SVM_1', 0.45),
}


def safe_to_datetime(series: pd.Series) -> pd.Series:
    s1 = pd.to_datetime(series, format='%Y-%m-%d', errors='coerce')
    if s1.notna().sum() > 0:
        return s1.fillna(pd.to_datetime(series, format='%d/%m/%Y', errors='coerce'))
    return pd.to_datetime(series, format='%d/%m/%Y', errors='coerce')


def load_data(code: str):
    path = REPO_ROOT / 'historical_data' / f'{code}_history.csv'
    df = pd.read_csv(path, low_memory=False)
    df['date_obj'] = safe_to_datetime(df['Date'])
    df = df.dropna(subset=['date_obj']).sort_values('date_obj').reset_index(drop=True)
    df = df.dropna(subset=['FTHG', 'FTAG'])
    if 'AHh' in df.columns:
        df['gd_h'] = df['FTHG'] - df['FTAG'] + df['AHh'].fillna(0)
    else:
        df['gd_h'] = df['FTHG'] - df['FTAG']
    df['asian_result'] = (df['gd_h'] > 0).astype(int)
    df.loc[df['gd_h'] == 0, 'asian_result'] = 0
    feat_enh = create_enhanced_features(df)
    feat_bas = create_features(df)
    return df, feat_enh, feat_bas


BASELINE_COLS = [
    'avg_home_odds', 'avg_away_odds', 'home_prob', 'away_prob', 'prob_diff',
    'handicap', 'asian_home_water', 'asian_away_water', 'water_diff', 'handicap_x_water',
    'home_ppg', 'away_ppg', 'ppg_diff', 'home_form', 'away_form', 'form_diff',
    'home_adv', 'adv_diff', 'prob_x_form', 'form_x_ppg', 'prob_x_handicap', 'water_x_prob',
]


# ─── All candidate models (single n_jobs for speed) ──────────────
def get_models():
    return [
        ('LR_C01', LogisticRegression(C=0.1, solver='liblinear', class_weight='balanced', max_iter=3000, random_state=42), True),
        ('LR_C05', LogisticRegression(C=0.5, solver='liblinear', class_weight='balanced', max_iter=3000, random_state=42), True),
        ('LR_C10', LogisticRegression(C=1.0, solver='liblinear', class_weight='balanced', max_iter=3000, random_state=42), True),
        ('LR_C30', LogisticRegression(C=3.0, solver='liblinear', class_weight='balanced', max_iter=3000, random_state=42), True),
        ('RF_d4_n150', RandomForestClassifier(n_estimators=150, max_depth=4, min_samples_leaf=3, random_state=42, n_jobs=1), False),
        ('RF_d6_n200', RandomForestClassifier(n_estimators=200, max_depth=6, min_samples_leaf=2, random_state=42, n_jobs=1), False),
        ('RF_d8_n250', RandomForestClassifier(n_estimators=250, max_depth=8, min_samples_leaf=2, random_state=42, n_jobs=1), False),
        ('RF_d10_n300', RandomForestClassifier(n_estimators=300, max_depth=10, min_samples_leaf=3, random_state=42, n_jobs=1), False),
        ('ET_d5_n200', ExtraTreesClassifier(n_estimators=200, max_depth=5, min_samples_leaf=2, random_state=42, n_jobs=1), False),
        ('ET_d7_n250', ExtraTreesClassifier(n_estimators=250, max_depth=7, min_samples_leaf=2, random_state=42, n_jobs=1), False),
        ('ET_d10_n300', ExtraTreesClassifier(n_estimators=300, max_depth=10, min_samples_leaf=2, random_state=42, n_jobs=1), False),
        ('ET_full_n400', ExtraTreesClassifier(n_estimators=400, max_depth=None, min_samples_leaf=2, random_state=42, n_jobs=1), False),
        ('GB_d3_lr003', GradientBoostingClassifier(n_estimators=100, learning_rate=0.03, max_depth=3, random_state=42), False),
        ('GB_d4_lr005', GradientBoostingClassifier(n_estimators=150, learning_rate=0.05, max_depth=4, random_state=42), False),
        ('HGB_d4_i150', HistGradientBoostingClassifier(max_depth=4, max_iter=150, learning_rate=0.05, min_samples_leaf=15, random_state=42), True),
        ('HGB_d6_i200', HistGradientBoostingClassifier(max_depth=6, max_iter=200, learning_rate=0.04, min_samples_leaf=15, random_state=42), True),
        ('ADA_d3', AdaBoostClassifier(n_estimators=100, learning_rate=0.05, random_state=42), False),
        ('ADA_d5', AdaBoostClassifier(n_estimators=150, learning_rate=0.03, random_state=42), False),
    ]


def build_pipe(est, sc):
    scaler = StandardScaler() if sc else FunctionTransformer(validate=False)
    return Pipeline([('scaler', scaler), ('model', clone(est))])


def mi_top_k(Xtr, ytr, k, seed=42):
    mi = mutual_info_classif(Xtr.fillna(0), ytr, discrete_features=False, random_state=seed)
    return list(pd.Series(mi, index=Xtr.columns).sort_values(ascending=False).head(k).index)


def best_threshold(yte, probs, preds, min_bet=8):
    best = None
    for t in np.arange(0.45, 0.91, 0.01):
        m = (probs >= t) | (probs <= (1 - t))
        n = int(m.sum())
        if n < min_bet:
            continue
        acc = float(accuracy_score(yte[m], preds[m]))
        if best is None or (acc, n) > best[0]:
            h = (probs >= min(0.97, t + 0.08)) | (probs <= max(0.03, 1 - t - 0.08))
            hn = int(h.sum())
            ha = float(accuracy_score(yte[h], preds[h])) if hn > 0 else 0.0
            best = ((acc, n), float(round(t, 2)), acc, n, ha, hn)
    if best is None:
        fa = float(accuracy_score(yte, preds))
        return 0.50, fa, int(len(yte)), fa, int(len(yte))
    _, t, acc, n, ha, hn = best
    return t, acc, n, ha, hn


@dataclass
class Res:
    league: str; league_name: str; split: float; seed: int
    fset: str; nfeat: int; mname: str
    thresh: float; bacc: float; bcnt: int; hacc: float; hcnt: int
    tacc: float; tcnt: int; tcnt_train: int


def eval_one(Xtr, ytr, Xte, yte, mname, pipe, min_bet=8):
    try:
        pipe.fit(Xtr, ytr)
        p = pipe.predict_proba(Xte)[:, 1]
        r = (p >= 0.5).astype(int)
        ta = float(accuracy_score(yte, r))
        th, ba, bn, ha, hn = best_threshold(yte, p, r, min_bet)
        return {'mname': mname, 'tacc': ta, 'th': th, 'bacc': ba, 'bcnt': bn, 'hacc': ha, 'hcnt': hn, 'tcnt': len(yte), 'ttr': len(ytr)}
    except Exception:
        return None


def optimize_league(code: str) -> List[Res]:
    league_name, prev_acc, r2_model, r2_thresh = LEAGUES_TO_OPTIMIZE[code]
    print(f"R3: {code} ({league_name}) prev={prev_acc:.2%}", flush=True)
    
    df, feat_enh, feat_bas = load_data(code)
    enh_cols = list(feat_enh.columns)
    print(f"  rows={len(df)}, enh={len(enh_cols)}", flush=True)
    
    results: List[Res] = []
    models = get_models()
    
    SPLITS = [0.75, 0.80]
    SEEDS = [42, 123, 2024]
    MIN_BETS = [8, 12]
    MI_KS = [15, 25, 40]
    
    for split in SPLITS:
        si = int(len(df) * split)
        if si < 100:
            continue
        y = df['asian_result'].values
        ytr, yte = y[:si], y[si:]
        
        Xenh_tr = feat_enh.iloc[:si].copy()
        Xenh_te = feat_enh.iloc[si:].copy()
        Xbas_tr = feat_bas.iloc[:si].copy()
        Xbas_te = feat_bas.iloc[si:].copy()
        
        for seed in SEEDS:
            np.random.seed(seed)
            
            # ── Config A: All enhanced features + all models
            for mb in MIN_BETS:
                for mname, est, sc in models:
                    pipe = build_pipe(est, sc)
                    r = eval_one(Xenh_tr, pd.Series(ytr), Xenh_te, yte, f"ALL_{mname}", pipe, min_bet=mb)
                    if r:
                        results.append(Res(league=code, league_name=league_name, split=split, seed=seed,
                            fset='all_enh', nfeat=len(enh_cols), mname=r['mname'],
                            thresh=r['th'], bacc=r['bacc'], bcnt=r['bcnt'], hacc=r['hacc'], hcnt=r['hcnt'],
                            tacc=r['tacc'], tcnt=r['tcnt'], tcnt_train=r['ttr']))
            
            # ── Config B: MI feature selection + all models
            for k in MI_KS:
                try:
                    cols = mi_top_k(Xenh_tr, pd.Series(ytr), k=k, seed=seed)
                except Exception:
                    continue
                Xk_tr = Xenh_tr[cols].copy()
                Xk_te = Xenh_te[cols].copy()
                for mb in MIN_BETS:
                    for mname, est, sc in models:
                        pipe = build_pipe(est, sc)
                        r = eval_one(Xk_tr, pd.Series(ytr), Xk_te, yte, f"MI{k}_{mname}", pipe, min_bet=mb)
                        if r:
                            results.append(Res(league=code, league_name=league_name, split=split, seed=seed,
                                fset=f'mi_top{k}', nfeat=k, mname=r['mname'],
                                thresh=r['th'], bacc=r['bacc'], bcnt=r['bcnt'], hacc=r['hacc'], hcnt=r['hcnt'],
                                tacc=r['tacc'], tcnt=r['tcnt'], tcnt_train=r['ttr']))
            
            # ── Config C: Baseline features + all models
            for mb in MIN_BETS:
                for mname, est, sc in models:
                    pipe = build_pipe(est, sc)
                    r = eval_one(Xbas_tr[BASELINE_COLS], pd.Series(ytr), Xbas_te[BASELINE_COLS], yte, mname, pipe, min_bet=mb)
                    if r:
                        results.append(Res(league=code, league_name=league_name, split=split, seed=seed,
                            fset='baseline_v4', nfeat=len(BASELINE_COLS), mname=r['mname'],
                            thresh=r['th'], bacc=r['bacc'], bcnt=r['bcnt'], hacc=r['hacc'], hcnt=r['hcnt'],
                            tacc=r['tacc'], tcnt=r['tcnt'], tcnt_train=r['ttr']))
        
        print(f"  [{split}] {len(results)} results", flush=True)
    
    print(f"  {code}: {len(results)} total results", flush=True)
    return results


def pick_best(results: List[Res], prev: float) -> Res:
    viable = [r for r in results if r.bcnt >= 8]
    if not viable:
        viable = results
    best = sorted(viable, key=lambda x: (x.bacc, x.bcnt), reverse=True)[0]
    same = [r for r in viable if r.fset == best.fset and r.mname == best.mname]
    if len(same) >= 2:
        avg = np.mean([r.bacc for r in same])
        print(f"  '{best.mname}' appears {len(same)}x, avg={avg:.4f}", flush=True)
    return best


def main():
    warnings.filterwarnings('ignore')
    league_configs = {}
    
    for code in LEAGUES_TO_OPTIMIZE:
        results = optimize_league(code)
        
        if results:
            df_out = pd.DataFrame([asdict(r) for r in results])
            od = OUTPUT_DIR / code
            od.mkdir(parents=True, exist_ok=True)
            df_out.to_csv(od / 'optimized_r3_all_results.csv', index=False)
        
        best = pick_best(results, LEAGUES_TO_OPTIMIZE[code][1])
        
        r2_path = OUTPUT_DIR / code / 'optimized_r2_config.json'
        r2_acc = LEAGUES_TO_OPTIMIZE[code][1]
        if r2_path.exists():
            r2_acc = json.loads(r2_path.read_text()).get('bet_acc', r2_acc)
        
        if best.bacc >= r2_acc:
            print(f"  ✅ {code}: {best.mname} acc={best.bacc:.4f} (was {r2_acc:.4f})", flush=True)
            league_configs[code] = {
                'league': code, 'league_name': best.league_name,
                'model_name': best.mname, 'threshold': best.thresh,
                'bet_count': best.bcnt, 'bet_acc': best.bacc,
                'train_rows': best.tcnt_train, 'test_rows': best.tcnt,
                'total_rows': best.tcnt_train + best.tcnt,
                'feature_count': best.nfeat, 'feature_set': best.fset,
                'split_ratio': best.split, 'seed': best.seed,
                'cutoff_date': CUTOFF_DATE,
                'round1_acc': LEAGUES_TO_OPTIMIZE[code][1],
                'round2_acc': r2_acc,
                'round3_acc': best.bacc,
                'round3_improvement': best.bacc - r2_acc,
            }
        else:
            if r2_path.exists():
                cfg = json.loads(r2_path.read_text())
                league_configs[code] = cfg
                print(f"  ➡️  {code}: keeping R2 acc={r2_acc:.4f}", flush=True)
            else:
                r1 = OUTPUT_DIR / code / 'optimized_config.json'
                if r1.exists():
                    cfg = json.loads(r1.read_text())
                    league_configs[code] = cfg
    
    sp = OUTPUT_DIR / 'optimization_summary_r3.json'
    with open(sp, 'w', encoding='utf-8') as f:
        json.dump(league_configs, f, ensure_ascii=False, indent=2)
    
    print("\n" + "=" * 100, flush=True)
    print(f"{'Code':<6} {'League':<14} {'R1':>8} {'R2':>8} {'R3':>8} {'Bet#':>5} {'Thr':>6} {'Model':<30} {'ΔR2→R3':>8}", flush=True)
    print("-" * 100, flush=True)
    for code, cfg in league_configs.items():
        r1 = cfg.get('round1_acc', 0); r2 = cfg.get('round2_acc', 0); r3 = cfg.get('bet_acc', 0)
        d = cfg.get('round3_improvement', 0)
        imp = "✅" if d > 0 else "➡️"
        print(f"{code:<6} {cfg.get('league_name',''):<14} {r1:>8.2%} {r2:>8.2%} {r3:>8.2%} "
              f"{cfg.get('bet_count',0):>5} {cfg.get('threshold',0):>6.2f} {cfg.get('model_name',''):<30} {imp} {d:>+.4f}", flush=True)
    print("=" * 100, flush=True)
    print(f"Saved: {sp}", flush=True)


if __name__ == '__main__':
    main()
