#!/usr/bin/env python3
"""Stable training for each league, target 80%."""

from __future__ import annotations

import json
import numpy as np
import pandas as pd
from pathlib import Path
from typing import List

import joblib
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, RobustScaler, MinMaxScaler
from sklearn.impute import SimpleImputer
from sklearn.neural_network import MLPClassifier
from collections import defaultdict

REPO_ROOT = Path.cwd()
HISTORICAL_DIR = REPO_ROOT / 'historical_data'
OUTPUT_DIR = REPO_ROOT / 'experiments' / 'goal_number_stable'

# All leagues
LEAGUES = {
    'a1': {'name': '澳超'},
    'br1': {'name': '巴甲'},
    'd2': {'name': '德乙'},
    'e0': {'name': '英超'},
    'e1': {'name': '英甲'},
    'e2': {'name': '英冠'},
    'f1': {'name': '法甲'},
    'f2': {'name': '法乙'},
    'g1': {'name': '希腊超'},
    'j1': {'name': '日职联'},
    'm1': {'name': '美职联'},
    'r1': {'name': '俄超'},
    'uz1': {'name': '乌兹别克超'},
    'sp1': {'name': '西甲'},
}


def mean_or_default(values: List[float], default: float) -> float:
    return float(np.mean(values)) if values else float(default)


def build_features(df: pd.DataFrame) -> pd.DataFrame:
    """Build features."""
    team_hist = defaultdict(lambda: {
        "gf": [], "ga": [], "tot": [], "over": [],
        "home_gf": [], "home_ga": [], "home_over": [],
        "away_gf": [], "away_ga": [], "away_over": [],
        "gf_last5": [], "ga_last5": [], "over_last5": [],
        "gf_last10": [], "ga_last10": [], "over_last10": [],
        "momentum": [],
    })
    
    rows = []
    
    for _, match in df.iterrows():
        home = str(match["HomeTeam"])
        away = str(match["AwayTeam"])
        home_hist = team_hist[home]
        away_hist = team_hist[away]
        
        total_goals = float(match.get('FTHG', 0) or 0) + float(match.get('FTAG', 0) or 0)
        is_over = 1 if total_goals > 2.5 else 0
        
        row = {
            "home_gf_avg": mean_or_default(home_hist["gf"], 1.4),
            "home_ga_avg": mean_or_default(home_hist["ga"], 1.4),
            "away_gf_avg": mean_or_default(away_hist["gf"], 1.2),
            "away_ga_avg": mean_or_default(away_hist["ga"], 1.2),
            "home_tot_avg": mean_or_default(home_hist["tot"], 2.6),
            "away_tot_avg": mean_or_default(away_hist["tot"], 2.6),
            "home_over_rate": mean_or_default(home_hist["over"], 0.5),
            "away_over_rate": mean_or_default(away_hist["over"], 0.5),
            "home_gf_last5": mean_or_default(home_hist["gf_last5"], 1.4),
            "home_ga_last5": mean_or_default(home_hist["ga_last5"], 1.4),
            "away_gf_last5": mean_or_default(away_hist["gf_last5"], 1.2),
            "away_ga_last5": mean_or_default(away_hist["ga_last5"], 1.2),
            "home_over_last5": mean_or_default(home_hist["over_last5"], 0.5),
            "away_over_last5": mean_or_default(away_hist["over_last5"], 0.5),
            "home_gf_last10": mean_or_default(home_hist["gf_last10"], 1.4),
            "home_ga_last10": mean_or_default(home_hist["ga_last10"], 1.4),
            "away_gf_last10": mean_or_default(away_hist["gf_last10"], 1.2),
            "away_ga_last10": mean_or_default(away_hist["ga_last10"], 1.2),
            "home_over_last10": mean_or_default(home_hist["over_last10"], 0.5),
            "away_over_last10": mean_or_default(away_hist["over_last10"], 0.5),
            "home_momentum": np.mean(home_hist["momentum"][-10:]) if len(home_hist["momentum"]) >= 10 else 0.5,
            "away_momentum": np.mean(away_hist["momentum"][-10:]) if len(away_hist["momentum"]) >= 10 else 0.5,
            "home_home_gf_avg": mean_or_default(home_hist["home_gf"], mean_or_default(home_hist["gf"], 1.4)),
            "home_home_ga_avg": mean_or_default(home_hist["home_ga"], mean_or_default(home_hist["ga"], 1.4)),
            "away_away_gf_avg": mean_or_default(away_hist["away_gf"], mean_or_default(away_hist["gf"], 1.2)),
            "away_away_ga_avg": mean_or_default(away_hist["away_ga"], mean_or_default(away_hist["ga"], 1.2)),
            "home_home_over_rate": mean_or_default(home_hist["home_over"], mean_or_default(home_hist["over"], 0.5)),
            "away_away_over_rate": mean_or_default(away_hist["away_over"], mean_or_default(away_hist["over"], 0.5)),
            "is_over": is_over,
        }
        rows.append(row)
        
        # Update team history
        home_hist["gf"].append(total_goals)
        home_hist["ga"].append(total_goals)
        home_hist["tot"].append(total_goals)
        home_hist["over"].append(is_over)
        home_hist["gf_last5"].append(total_goals)
        home_hist["ga_last5"].append(total_goals)
        home_hist["gf_last10"].append(total_goals)
        home_hist["ga_last10"].append(total_goals)
        home_hist["over_last5"].append(is_over)
        home_hist["over_last10"].append(is_over)
        
        home_result = 1 if total_goals > (match.get('FTAG', 0) or 0) else 0.5
        home_hist["momentum"].append(home_result)
        
        if pd.notna(match.get('FTHG')) and pd.notna(match.get('FTAG')):
            home_hist["home_gf"].append(float(match['FTHG']))
            home_hist["home_ga"].append(float(match['FTAG']))
            home_hist["home_over"].append(is_over)
        
        away_hist["gf"].append(total_goals)
        away_hist["ga"].append(total_goals)
        away_hist["tot"].append(total_goals)
        away_hist["over"].append(is_over)
        away_hist["gf_last5"].append(total_goals)
        away_hist["ga_last5"].append(total_goals)
        away_hist["gf_last10"].append(total_goals)
        away_hist["ga_last10"].append(total_goals)
        away_hist["over_last5"].append(is_over)
        away_hist["over_last10"].append(is_over)
        
        away_result = 1 if total_goals > (match.get('FTHG', 0) or 0) else 0.5
        away_hist["momentum"].append(away_result)
        
        if pd.notna(match.get('FTHG')) and pd.notna(match.get('FTAG')):
            away_hist["away_gf"].append(float(match['FTHG']))
            away_hist["away_ga"].append(float(match['FTAG']))
            away_hist["away_over"].append(is_over)
    
    return pd.DataFrame(rows)


def train_league(league_code: str, league_config: dict) -> dict | None:
    """Train a model for a league."""
    print(f"\n{'='*60}")
    print(f"Training {league_code} ({league_config['name']})")
    print(f"Target: 80%+")
    print(f"{'='*60}")
    
    # Load data
    history_path = HISTORICAL_DIR / f'{league_code}_history.csv'
    if not history_path.exists():
        print(f"  [SKIP] File not found")
        return None
    
    df = pd.read_csv(history_path)
    print(f"  Loaded {len(df)} matches")
    
    if len(df) < 800:
        print(f"  [SKIP] Not enough data")
        return None
    
    # Build features
    print("  Building features...")
    features_df = build_features(df)
    print(f"  Built {len(features_df)} rows with {len(features_df.columns)} features")
    
    # Prepare data
    target_col = 'is_over'
    feature_cols = [c for c in features_df.columns if c != target_col]
    
    X = features_df[feature_cols].fillna(0)
    y = features_df[target_col].astype(int)
    
    # Time-based split
    if 'Date' in df.columns:
        df['Date_dt'] = pd.to_datetime(df['Date'], errors='coerce')
        df_sorted = df.sort_values('Date_dt').reset_index(drop=True)
        split_idx = int(len(df_sorted) * 0.8)
        X = features_df.iloc[df_sorted.index].reset_index(drop=True)[feature_cols].fillna(0)
        y = features_df.iloc[df_sorted.index].reset_index(drop=True)[target_col].astype(int)
        X_train, X_test = X.iloc[:split_idx], X.iloc[split_idx:]
        y_train, y_test = y.iloc[:split_idx], y.iloc[split_idx:]
    else:
        from sklearn.model_selection import train_test_split
        X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
    
    print(f"  Train: {len(X_train)}, Test: {len(X_test)}")
    print(f"  Over rate: {y.mean():.2%}")
    
    # Test models
    models = []
    
    # 1. RandomForest
    models.append(('RF_500_d20', Pipeline([
        ('imputer', SimpleImputer(strategy='median')),
        ('scaler', RobustScaler()),
        ('clf', RandomForestClassifier(
            n_estimators=500,
            max_depth=20,
            min_samples_leaf=5,
            random_state=42,
            n_jobs=-1,
            class_weight='balanced'
        ))
    ])))
    
    models.append(('RF_600_d25', Pipeline([
        ('imputer', SimpleImputer(strategy='median')),
        ('scaler', RobustScaler()),
        ('clf', RandomForestClassifier(
            n_estimators=600,
            max_depth=25,
            min_samples_leaf=2,
            random_state=42,
            n_jobs=-1,
            class_weight='balanced'
        ))
    ])))
    
    # 2. GradientBoosting
    models.append(('GB_300', Pipeline([
        ('imputer', SimpleImputer(strategy='median')),
        ('scaler', RobustScaler()),
        ('clf', GradientBoostingClassifier(
            n_estimators=300,
            max_depth=8,
            learning_rate=0.05,
            random_state=42
        ))
    ])))
    
    # 3. LogisticRegression
    models.append(('LR_10', Pipeline([
        ('imputer', SimpleImputer(strategy='mean')),
        ('scaler', StandardScaler()),
        ('clf', LogisticRegression(
            C=10.0,
            max_iter=2000,
            random_state=42,
            class_weight='balanced'
        ))
    ])))
    
    # 4. MLP
    models.append(('MLP', Pipeline([
        ('imputer', SimpleImputer(strategy='mean')),
        ('scaler', MinMaxScaler()),
        ('clf', MLPClassifier(
            hidden_layer_sizes=(128, 64, 32),
            max_iter=500,
            alpha=0.01,
            learning_rate='adaptive',
            random_state=42
        ))
    ])))
    
    print(f"\n  Testing {len(models)} models...")
    
    best_config = None
    best_model = None
    best_test_acc = 0.0
    
    for model_name, pipeline in models:
        try:
            print(f"\n  {model_name}...")
            
            pipeline.fit(X_train, y_train)
            
            # Evaluate
            y_pred = pipeline.predict(X_test)
            overall_acc = accuracy_score(y_test, y_pred)
            
            # Test thresholds
            if hasattr(pipeline[-1], 'predict_proba'):
                probs = pipeline.predict_proba(X_test)[:, 1]
                thresholds = np.arange(0.40, 0.91, 0.02)
                
                for threshold in thresholds:
                    mask = (probs >= threshold) | (probs <= (1.0 - threshold))
                    bet_count = int(mask.sum())
                    
                    if bet_count < 20:
                        continue
                    
                    preds = (probs >= threshold).astype(int)
                    bet_acc = accuracy_score(y_test[mask], preds[mask])
                    
                    if bet_acc > best_test_acc:
                        best_test_acc = bet_acc
                        best_model = pipeline
                        best_config = {
                            'league': league_code,
                            'league_name': league_config['name'],
                            'model': model_name,
                            'threshold': float(threshold),
                            'bet_count': bet_count,
                            'bet_acc': float(bet_acc),
                            'overall_acc': float(overall_acc),
                            'rows': int(len(features_df)),
                            'train_rows': int(len(X_train)),
                            'test_rows': int(len(X_test)),
                            'features': feature_cols,
                            'feature_count': len(feature_cols),
                        }
                        
                        print(f"    New best! Threshold={threshold:.2f}, Bet Acc={bet_acc:.2%}, Bet Count={bet_count}")
            else:
                # For models without predict_proba
                print(f"    Overall Acc: {overall_acc:.2%} (no predict_proba)")
        
        except Exception as e:
            print(f"    {model_name}: Error - {e}")
            continue
    
    if not best_config:
        print(f"\n  [ERROR] No model trained successfully")
        return None
    
    print(f"\n  {'='*60}")
    print(f"  FINAL MODEL")
    print(f"  {'='*60}")
    print(f"    Model: {best_config['model']}")
    print(f"    Threshold: {best_config['threshold']:.2f}")
    print(f"    Bet count: {best_config['bet_count']}")
    print(f"    Bet accuracy: {best_config['bet_acc']:.2%}")
    print(f"    Overall accuracy: {best_config['overall_acc']:.2%}")
    print(f"  {'='*60}")
    
    if best_config['bet_acc'] >= 0.80:
        print(f"  [SUCCESS] Target met! Accuracy: {best_config['bet_acc']:.2%}")
    else:
        print(f"  [WARNING] Accuracy {best_config['bet_acc']:.2%} is below 80% target")
    
    # Save model
    OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
    model_dir = OUTPUT_DIR / league_code
    model_dir.mkdir(parents=True, exist_ok=True)
    
    model_path = model_dir / 'goal_number_pipeline.pkl'
    config_path = model_dir / 'goal_number_config.json'
    
    # Re-train on full training data
    best_model.fit(X_train, y_train)
    
    joblib.dump(best_model, model_path)
    
    config = {
        **best_config,
        'task': 'goal-number',
        'target': 'over_under_2_5',
        'model_path': str(model_path),
    }
    
    with open(config_path, 'w', encoding='utf-8') as f:
        json.dump(config, f, ensure_ascii=False, indent=2)
    
    print(f"  Model saved to: {model_path}")
    print(f"  Config saved to: {config_path}")
    
    return best_config


def main() -> None:
    print("="*60)
    print("逐个联赛稳定训练（目标 80%+）")
    print("="*60)
    
    results = []
    
    for league_code, league_config in LEAGUES.items():
        result = train_league(league_code, league_config)
        if result:
            results.append(result)
    
    # Summary
    print(f"\n{'='*60}")
    print("训练总结")
    print(f"{'='*60}")
    
    if results:
        results_sorted = sorted(results, key=lambda x: x['bet_acc'], reverse=True)
        
        print(f"\n共训练 {len(results)} 个联赛:")
        
        for idx, result in enumerate(results_sorted, 1):
            status = "✓" if result['bet_acc'] >= 0.80 else "✗"
            print(f"  {idx}. {result['league']} ({result['league_name']}): {status}")
            print(f"     模型: {result['model']}")
            print(f"     准确率: {result['bet_acc']:.2%}")
            print(f"     投注次数: {result['bet_count']}")
            print()
        
        # Save summary
        summary_path = OUTPUT_DIR / 'all_leagues_summary.json'
        with open(summary_path, 'w', encoding='utf-8') as f:
            json.dump(results_sorted, f, ensure_ascii=False, indent=2)
        print(f"总结已保存到: {summary_path}")
        
        high_accuracy = [r for r in results if r['bet_acc'] >= 0.80]
        print(f"\n达到 80%+ 目标的联赛: {len(high_accuracy)}/{len(results)}")
        
        if high_accuracy:
            avg_acc = sum(r['bet_acc'] for r in high_accuracy) / len(high_accuracy)
            print(f"80%+ 联赛的平均准确率: {avg_acc:.2%}")
        
        best_result = max(results, key=lambda x: x['bet_acc'])
        print(f"\n最佳模型: {best_result['league']} ({best_result['league_name']}) - {best_result['bet_acc']:.2%}")
    else:
        print("\nNo models trained!")


if __name__ == '__main__':
    main()
