#!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
from pathlib import Path
from typing import Dict, List

import joblib
import numpy as np
import pandas as pd
from sklearn.ensemble import GradientBoostingClassifier, RandomForestClassifier
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']

    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']

    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)

        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']

        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['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']

    return features[FEATURE_COLUMNS].fillna(0).replace([np.inf, -np.inf], 0)


def build_models() -> Dict[str, object]:
    return {
        'LR': LogisticRegression(max_iter=1000, C=0.5, class_weight='balanced'),
        'GB': GradientBoostingClassifier(n_estimators=200, max_depth=5, learning_rate=0.05),
        'RF': RandomForestClassifier(n_estimators=300, max_depth=7, random_state=42),
    }


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.70, 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


def train_one(data_path: Path, output_dir: Path, league: str | None = None, code: str | None = None) -> dict:
    df = pd.read_csv(data_path)
    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)

    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_acc = 0.0

    for model_name, model in build_models().items():
        model.fit(X_train_scaled, y_train)
        preds = model.predict(X_test_scaled)
        probs = model.predict_proba(X_test_scaled)[:, 1]
        high_conf_mask = (probs > 0.55) | (probs < 0.45)
        if high_conf_mask.sum() > 0:
            bet_acc = accuracy_score(y_test[high_conf_mask], preds[high_conf_mask])
        else:
            bet_acc = accuracy_score(y_test, preds)

        if bet_acc > best_acc:
            best_acc = float(bet_acc)
            best_model = model
            best_name = model_name

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

    probs = best_model.predict_proba(X_test_scaled)[:, 1]
    preds = best_model.predict(X_test_scaled)
    best_threshold, best_bet_acc = optimize_threshold(y_test, probs, preds, best_acc)

    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_bet_acc),
        'bet_acc': float(best_bet_acc),
        'high_conf_acc': float(high_conf_acc),
        'high_conf_count': int((probs > 0.70).sum()),
        'features': FEATURE_COLUMNS,
        'model_name': best_name,
        'data_count': int(len(df)),
        'league': league_name,
    }

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


def main() -> None:
    parser = argparse.ArgumentParser(description='Train football Asian handicap models.')
    parser.add_argument('--data', type=Path, help='Single historical CSV file')
    parser.add_argument('--data-dir', type=Path, help='Directory of historical CSV files')
    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:
        configs.append(train_one(args.data, args.output_dir, args.league, args.code))
    else:
        for path in sorted(args.data_dir.glob('*_history.csv')):
            try:
                configs.append(train_one(path, args.output_dir))
                print(f'[OK] {path.name}')
            except Exception as exc:
                print(f'[FAIL] {path.name}: {exc}')

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


if __name__ == '__main__':
    main()
