#!/usr/bin/env python3
"""Smart training - select by BET_ACC (threshold-optimized), not raw test accuracy."""
from __future__ import annotations

import argparse
import json
from pathlib import Path
from typing import Dict, List, Tuple
import warnings
warnings.filterwarnings('ignore')

import joblib
import numpy as np
import pandas as pd
from sklearn.ensemble import GradientBoostingClassifier, RandomForestClassifier, ExtraTreesClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
from sklearn.preprocessing import StandardScaler

FEATURE_COLUMNS = [
    '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',
]

LEAGUES = {
    'i1': '意甲', 'e0': '英超', 'd1': '德甲', 'd2': '德乙',
    'sp1': '西甲', 'f1': '法甲', 'e2': '英冠', 'e1': '英甲', 'e3': '英乙',
    'p1': '葡超', 'tr1': '土超', 'br1': '巴甲', 'uz1': '乌兹别克',
    'e2n': '荷乙', 'sc1': '苏超', 'cz1': '捷克甲级', 'r1': '俄超', 'm1': '美职联',
    'j1': '日职联', 'j2': '日职乙', 'k2': '韩国K2', 'a1': '澳超', 'f2': '法乙', 'g1': '希腊超',
}


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 create_features(df: pd.DataFrame) -> pd.DataFrame:
    features = pd.DataFrame(index=df.index)

    if 'AvgH' in df.columns:
        features['avg_home_odds'] = df['AvgH'].fillna(2.0)
        features['avg_away_odds'] = df['AvgA'].fillna(3.0)
    elif 'B365H' in df.columns:
        features['avg_home_odds'] = df['B365H'].fillna(2.0)
        features['avg_away_odds'] = df['B365A'].fillna(3.0)
    else:
        features['avg_home_odds'] = 2.0
        features['avg_away_odds'] = 3.0

    features['home_prob'] = 1 / features['avg_home_odds'].replace(0, np.nan)
    features['away_prob'] = 1 / features['avg_away_odds'].replace(0, np.nan)
    features['prob_diff'] = features['home_prob'] - features['away_prob']
    features['prob_ratio'] = features['home_prob'] / features['away_prob'].replace(0, 1)

    if 'AHh' in df.columns:
        features['handicap'] = df['AHh'].fillna(0)
        features['asian_home_water'] = df.get('B365AHH', pd.Series(0.9, index=df.index)).fillna(0.9)
        features['asian_away_water'] = df.get('B365AHA', pd.Series(0.9, index=df.index)).fillna(0.9)
    else:
        features['handicap'] = 0.0
        features['asian_home_water'] = 0.9
        features['asian_away_water'] = 0.9

    features['water_diff'] = features['asian_home_water'] - features['asian_away_water']
    features['handicap_x_water'] = features['handicap'] * features['asian_home_water']
    features['water_sum'] = features['asian_home_water'] + features['asian_away_water']

    if 'HomeTeam' in df.columns and 'AwayTeam' in df.columns and 'FTR' in df.columns:
        teams = list(df['HomeTeam'].dropna().unique())

        def calc_ppg(team: str, idx: int) -> float:
            past = df.iloc[:idx][(df.iloc[:idx]['HomeTeam'] == team) | (df.iloc[:idx]['AwayTeam'] == team)]
            if len(past) == 0:
                return 1.5
            pts = sum(3 if (r['FTR'] == ('H' if r['HomeTeam'] == team else 'A')) else (1 if r['FTR'] == 'D' else 0) for _, r in past.iterrows())
            return pts / len(past)

        def calc_form(team: str, idx: int, w: int = 5) -> float:
            past = df.iloc[:idx][(df.iloc[:idx]['HomeTeam'] == team) | (df.iloc[:idx]['AwayTeam'] == team)].tail(w)
            if len(past) == 0:
                return 1.5
            pts = sum(3 if (r['FTR'] == ('H' if r['HomeTeam'] == team else 'A')) else (1 if r['FTR'] == 'D' else 0) for _, r in past.iterrows())
            return pts / len(past)

        def calc_recent_wins(team: str, idx: int, w: int = 3) -> float:
            past = df.iloc[:idx][(df.iloc[:idx]['HomeTeam'] == team) | (df.iloc[:idx]['AwayTeam'] == team)].tail(w)
            if len(past) == 0:
                return 0.5
            wins = sum(1 if (r['FTR'] == 'H' and r['HomeTeam'] == team) or (r['FTR'] == 'A' and r['AwayTeam'] == team) else 0 for _, r in past.iterrows())
            return wins / len(past)

        features['home_ppg'] = [calc_ppg(row['HomeTeam'], i) for i, row in df.iterrows()]
        features['away_ppg'] = [calc_ppg(row['AwayTeam'], i) for i, row in df.iterrows()]
        features['ppg_diff'] = features['home_ppg'] - features['away_ppg']
        features['home_form'] = [calc_form(row['HomeTeam'], i) for i, row in df.iterrows()]
        features['away_form'] = [calc_form(row['AwayTeam'], i) for i, row in df.iterrows()]
        features['form_diff'] = features['home_form'] - features['away_form']
        features['home_recent_wins'] = [calc_recent_wins(row['HomeTeam'], i) for i, row in df.iterrows()]
        features['away_recent_wins'] = [calc_recent_wins(row['AwayTeam'], i) for i, row in df.iterrows()]
        features['recent_wins_diff'] = features['home_recent_wins'] - features['away_recent_wins']
        home_adv = {t: (df[df['HomeTeam'] == t]['FTR'] == 'H').mean() for t in teams}
        features['home_adv'] = df['HomeTeam'].map(home_adv).fillna(0.45)
    else:
        features['home_ppg'] = 1.5
        features['away_ppg'] = 1.5
        features['ppg_diff'] = 0.0
        features['home_form'] = 1.5
        features['away_form'] = 1.5
        features['form_diff'] = 0.0
        features['home_adv'] = 0.45
        features['home_recent_wins'] = 0.5
        features['away_recent_wins'] = 0.5
        features['recent_wins_diff'] = 0.0

    features['adv_diff'] = features['home_adv'] - 0.45
    features['prob_x_form'] = features['home_prob'] * features['home_form']
    features['form_x_ppg'] = features['home_form'] * features['home_ppg']
    features['prob_x_handicap'] = features['home_prob'] * features['handicap']
    features['water_x_prob'] = features['asian_home_water'] * features['home_prob']
    features['form_x_handicap'] = features['home_form'] * features['handicap']
    features['ppg_x_handicap'] = features['home_ppg'] * features['handicap']

    extended = FEATURE_COLUMNS + ['prob_ratio', 'water_sum', 'recent_wins_diff', 'form_x_handicap', 'ppg_x_handicap']
    return features[extended].fillna(0).replace([np.inf, -np.inf], 0)


def optimize_threshold(y_test: pd.Series, probs: np.ndarray, preds: np.ndarray, fallback_bet_acc: float) -> Tuple[float, float]:
    best_threshold = 0.55
    best_bet_acc = 0.0
    for threshold in np.arange(0.45, 0.75, 0.01):
        mask = (probs > threshold) | (probs < (1 - threshold))
        if mask.sum() >= 10:
            bet_acc = accuracy_score(y_test[mask], preds[mask])
            if bet_acc > best_bet_acc:
                best_bet_acc = float(bet_acc)
                best_threshold = float(threshold)
    if best_bet_acc < 0.5:
        best_bet_acc = float(fallback_bet_acc)
    return best_threshold, best_bet_acc


# Focused model pool - fewer configs, more likely to succeed
MODEL_POOL = [
    ('LR_C0.1', LogisticRegression(max_iter=2000, C=0.1, class_weight='balanced')),
    ('LR_C0.5', LogisticRegression(max_iter=2000, C=0.5, class_weight='balanced')),
    ('LR_C1.0', LogisticRegression(max_iter=2000, C=1.0, class_weight='balanced')),
    ('LR_C2.0', LogisticRegression(max_iter=2000, C=2.0, class_weight='balanced')),
    ('RF_300_7', RandomForestClassifier(n_estimators=300, max_depth=7, random_state=42, n_jobs=-1)),
    ('RF_300_9', RandomForestClassifier(n_estimators=300, max_depth=9, random_state=42, n_jobs=-1)),
    ('RF_500_7', RandomForestClassifier(n_estimators=500, max_depth=7, random_state=42, n_jobs=-1)),
    ('RF_500_9', RandomForestClassifier(n_estimators=500, max_depth=9, random_state=42, n_jobs=-1)),
    ('GB_0.05_5', GradientBoostingClassifier(n_estimators=200, max_depth=5, learning_rate=0.05, random_state=42)),
    ('GB_0.1_4', GradientBoostingClassifier(n_estimators=200, max_depth=4, learning_rate=0.1, random_state=42)),
    ('GB_0.1_5', GradientBoostingClassifier(n_estimators=200, max_depth=5, learning_rate=0.1, random_state=42)),
    ('ET_300_9', ExtraTreesClassifier(n_estimators=300, max_depth=9, random_state=42, n_jobs=-1)),
    ('ET_500_7', ExtraTreesClassifier(n_estimators=500, max_depth=7, random_state=42, n_jobs=-1)),
]


def train_one(data_path: Path, output_dir: Path, league: str | None = None, code: str | None = None) -> dict:
    df = pd.read_csv(data_path, low_memory=False)
    if 'Date' not in df.columns:
        raise ValueError(f'{data_path} 缺少 Date 字段')
    for col in ['FTHG', 'FTAG', 'HomeTeam', 'AwayTeam']:
        if col not in df.columns:
            raise ValueError(f'{data_path} 缺少 {col} 字段')

    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 len(df) < 200:
        raise ValueError(f'{data_path} 数据量太少 ({len(df)}场)')

    features = create_features(df)
    feature_names = features.columns.tolist()

    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

    split_idx = int(len(df) * 0.8)
    if split_idx <= 20 or len(df) - split_idx <= 5:
        raise ValueError(f'{data_path} 数据量过小，无法稳定训练: {len(df)}')

    X_train = features.iloc[:split_idx]
    X_test = features.iloc[split_idx:]
    y_train = df['asian_result'].iloc[:split_idx]
    y_test = df['asian_result'].iloc[split_idx:]

    scaler = StandardScaler()
    X_train_scaled = scaler.fit_transform(X_train)
    X_test_scaled = scaler.transform(X_test)

    best_model = None
    best_name = ''
    best_bet_acc = 0.0
    best_threshold = 0.55
    best_probs = None
    best_preds = None
    best_test_acc = 0.0

    for name, model in MODEL_POOL:
        try:
            model.fit(X_train_scaled, y_train)
            preds = model.predict(X_test_scaled)
            probs = model.predict_proba(X_test_scaled)[:, 1]
            test_acc = accuracy_score(y_test, preds)
            threshold, bet_acc = optimize_threshold(y_test, probs, preds, test_acc)
            # KEY: select by BET_ACC, not test_acc
            if bet_acc > best_bet_acc:
                best_bet_acc = float(bet_acc)
                best_threshold = float(threshold)
                best_model = model
                best_name = name
                best_probs = probs
                best_preds = preds
                best_test_acc = float(test_acc)
        except Exception:
            continue

    if best_model is None:
        raise RuntimeError(f'{data_path} 训练失败')

    probs = best_probs
    preds = best_preds
    high_conf_mask = probs > 0.70
    if high_conf_mask.sum() > 0:
        high_conf_acc = float(accuracy_score(y_test[high_conf_mask], preds[high_conf_mask]))
    else:
        high_conf_acc = float(best_bet_acc)

    league_name = league or LEAGUES.get((code or data_path.stem.replace('_history', '')), data_path.stem.replace('_history', ''))
    league_code = code or data_path.stem.replace('_history', '')

    output_dir.mkdir(parents=True, exist_ok=True)
    scaler_path = output_dir / f'{league_code}_pro_v4_scaler.pkl'
    model_path = output_dir / f'{league_code}_pro_v4_model.pkl'
    config_path = output_dir / f'{league_code}_pro_v4_config.pkl'

    config = {
        'threshold': best_threshold,
        'test_acc': float(best_test_acc),
        'bet_acc': float(best_bet_acc),
        'high_conf_acc': float(high_conf_acc),
        'high_conf_count': int((probs > 0.70).sum()),
        'features': feature_names,
        'model_name': best_name,
        'data_count': int(len(df)),
        'league': league_name,
        'code': league_code,
    }

    joblib.dump(scaler, scaler_path)
    joblib.dump(best_model, model_path)
    joblib.dump(config, config_path)
    return config


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument('--data', type=Path)
    parser.add_argument('--data-dir', type=Path)
    parser.add_argument('--output-dir', type=Path, default=Path('models'))
    parser.add_argument('--league', type=str)
    parser.add_argument('--code', type=str)
    args = parser.parse_args()

    if not args.data and not args.data_dir:
        raise SystemExit('需要 --data 或 --data-dir')

    configs: List[dict] = []
    if args.data:
        cfg = train_one(args.data, args.output_dir, args.league, args.code)
        configs.append(cfg)
        print(f'[OK] {args.data.name} - BetAcc: {cfg["bet_acc"]*100:.2f}% TestAcc: {cfg["test_acc"]*100:.2f}% Model: {cfg["model_name"]}')
    else:
        for path in sorted(args.data_dir.glob('*_history.csv')):
            try:
                cfg = train_one(path, args.output_dir)
                configs.append(cfg)
                print(f'[OK] {path.name} - BetAcc: {cfg["bet_acc"]*100:.2f}% TestAcc: {cfg["test_acc"]*100:.2f}% Model: {cfg["model_name"]}')
            except Exception as exc:
                print(f'[FAIL] {path.name}: {exc}')

    print(json.dumps(configs, ensure_ascii=False, indent=2))


if __name__ == '__main__':
    main()
