#!/usr/bin/env python3
"""Batch ultra-optimized training for all leagues, target 80%."""

from __future__ import annotations

import json
import sys
import subprocess
from pathlib import Path
from typing import List, Dict

import pandas as pd
import numpy as np
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier, ExtraTreesClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, f1_score
from sklearn.neural_network import MLPClassifier
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler
from sklearn.impute import SimpleImputer
from collections import defaultdict

REPO_ROOT = Path(__file__).resolve().parents[1]
HISTORICAL_DIR = REPO_ROOT / 'historical_data'
OUTPUT_DIR = REPO_ROOT / 'experiments' / 'goal_number_final'

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


def build_ultra_features(df: pd.DataFrame) -> pd.DataFrame:
    """Build ultra-comprehensive features."""
    team_hist = defaultdict(lambda: {
        "gf": [], "ga": [], "tot": [], "over": [],
        "home_gf": [], "home_ga": [], "home_over": [],
        "away_gf": [], "away_ga": [], "away_over": [],
        "gf_last3": [], "ga_last3": [], "over_last3": [],
        "gf_last5": [], "ga_last5": [], "over_last5": [],
        "gf_last10": [], "ga_last10": [], "over_last10": [],
        "gf_last15": [], "ga_last15": [], "over_last15": [],
        "gf_last20": [], "ga_last20": [], "over_last20": [],
        "momentum": [],
        "recent_form": [],
        "goal_trend": [],
    })
    
    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 = {}
        
        # Comprehensive window-based features
        for window in [3, 5, 10, 15, 20]:
            row[f"home_gf_last{window}"] = np.mean(home_hist[f"gf_last{window}"]) if home_hist[f"gf_last{window}"] else 1.4
            row[f"home_ga_last{window}"] = np.mean(home_hist[f"ga_last{window}"]) if home_hist[f"ga_last{window}"] else 1.4
            row[f"away_gf_last{window}"] = np.mean(away_hist[f"gf_last{window}"]) if away_hist[f"gf_last{window}"] else 1.2
            row[f"away_ga_last{window}"] = np.mean(away_hist[f"ga_last{window}"]) if away_hist[f"ga_last{window}"] else 1.2
            row[f"home_over_last{window}"] = np.mean(home_hist[f"over_last{window}"]) if home_hist[f"over_last{window}"] else 0.5
            row[f"away_over_last{window}"] = np.mean(away_hist[f"over_last{window}"]) if away_hist[f"over_last{window}"] else 0.5
            
            # Home/Away specific for larger windows
            if window in [5, 10, 15, 20]:
                row[f"home_home_gf_last{window}"] = np.mean(home_hist["home_gf"][-window:]) if len(home_hist["home_gf"]) >= window else 1.4
                row[f"home_home_ga_last{window}"] = np.mean(home_hist["home_ga"][-window:]) if len(home_hist["home_ga"]) >= window else 1.4
                row[f"away_away_gf_last{window}"] = np.mean(away_hist["away_gf"][-window:]) if len(away_hist["away_gf"]) >= window else 1.2
                row[f"away_away_ga_last{window}"] = np.mean(away_hist["away_ga"][-window:]) if len(away_hist["away_ga"]) >= window else 1.2
                row[f"home_home_over_last{window}"] = np.mean(home_hist["home_over"][-window:]) if len(home_hist["home_over"]) >= window else 0.5
                row[f"away_away_over_last{window}"] = np.mean(away_hist["away_over"][-window:]) if len(away_hist["away_over"]) >= window else 0.5
        
        # Overall averages
        row["home_gf_avg"] = np.mean(home_hist["gf"]) if home_hist["gf"] else 1.4
        row["home_ga_avg"] = np.mean(home_hist["ga"]) if home_hist["ga"] else 1.4
        row["away_gf_avg"] = np.mean(away_hist["gf"]) if away_hist["gf"] else 1.2
        row["away_ga_avg"] = np.mean(away_hist["ga"]) if away_hist["ga"] else 1.2
        row["home_tot_avg"] = np.mean(home_hist["tot"]) if home_hist["tot"] else 2.6
        row["away_tot_avg"] = np.mean(away_hist["tot"]) if away_hist["tot"] else 2.6
        row["home_over_rate"] = np.mean(home_hist["over"]) if home_hist["over"] else 0.5
        row["away_over_rate"] = np.mean(away_hist["over"]) if away_hist["over"] else 0.5
        
        # Home/Away specific
        row["home_home_gf_avg"] = np.mean(home_hist["home_gf"]) if home_hist["home_gf"] else np.mean(home_hist["gf"]) if home_hist["gf"] else 1.4
        row["home_home_ga_avg"] = np.mean(home_hist["home_ga"]) if home_hist["home_ga"] else np.mean(home_hist["ga"]) if home_hist["ga"] else 1.4
        row["away_away_gf_avg"] = np.mean(away_hist["away_gf"]) if away_hist["away_gf"] else np.mean(away_hist["gf"]) if away_hist["gf"] else 1.2
        row["away_away_ga_avg"] = np.mean(away_hist["away_ga"]) if away_hist["away_ga"] else np.mean(away_hist["ga"]) if away_hist["ga"] else 1.2
        row["home_home_over_rate"] = np.mean(home_hist["home_over"]) if home_hist["home_over"] else np.mean(home_hist["over"]) if home_hist["over"] else 0.5
        row["away_away_over_rate"] = np.mean(away_hist["away_over"]) if away_hist["away_over"] else np.mean(away_hist["over"]) if away_hist["over"] else 0.5
        
        # Momentum and form
        if len(home_hist["momentum"]) >= 10:
            row["home_momentum"] = np.mean(home_hist["momentum"][-10:])
        else:
            row["home_momentum"] = 0.5
        
        if len(away_hist["momentum"]) >= 10:
            row["away_momentum"] = np.mean(away_hist["momentum"][-10:])
        else:
            row["away_momentum"] = 0.5
        
        row["home_recent_form"] = np.mean(home_hist["recent_form"][-5:]) if len(home_hist["recent_form"]) >= 5 else 0.5
        row["away_recent_form"] = np.mean(away_hist["recent_form"][-5:]) if len(away_hist["recent_form"]) >= 5 else 0.5
        
        # Goal trend
        if len(home_hist["goal_trend"]) >= 5:
            row["home_goal_trend"] = home_hist["goal_trend"][-1]
        else:
            row["home_goal_trend"] = 0
        
        if len(away_hist["goal_trend"]) >= 5:
            row["away_goal_trend"] = away_hist["goal_trend"][-1]
        else:
            row["away_goal_trend"] = 0
        
        # Variance and consistency
        if len(home_hist["gf_last20"]) >= 2:
            row["home_gf_var"] = np.var(home_hist["gf_last20"])
            row["home_ga_var"] = np.var(home_hist["ga_last20"])
        else:
            row["home_gf_var"] = 0.5
            row["home_ga_var"] = 0.5
        
        if len(away_hist["gf_last20"]) >= 2:
            row["away_gf_var"] = np.var(away_hist["gf_last20"])
            row["away_ga_var"] = np.var(away_hist["ga_last20"])
        else:
            row["away_gf_var"] = 0.5
            row["away_ga_var"] = 0.5
        
        # Expected goals
        row["expected_goals_home"] = row["home_gf_avg"] + row["away_ga_avg"]
        row["expected_goals_away"] = row["away_gf_avg"] + row["home_ga_avg"]
        row["expected_goals_proxy"] = (row["expected_goals_home"] + row["expected_goals_away"]) / 2
        row["combined_over_rate"] = (row["home_over_rate"] + row["away_over_rate"]) / 2
        
        # Difference features
        row["home_away_gf_diff"] = row["home_gf_avg"] - row["away_gf_avg"]
        row["home_away_ga_diff"] = row["home_ga_avg"] - row["away_ga_avg"]
        row["home_away_tot_diff"] = row["home_tot_avg"] - row["away_tot_avg"]
        row["home_away_over_diff"] = row["home_over_rate"] - row["away_over_rate"]
        
        # Form ratio
        row["form_ratio"] = row["home_momentum"] / (row["away_momentum"] + 0.01)
        
        # Consistency features
        if len(home_hist["gf_last20"]) >= 2:
            row["home_gf_std"] = np.std(home_hist["gf_last20"])
            row["home_ga_std"] = np.std(home_hist["ga_last20"])
        else:
            row["home_gf_std"] = 0.5
            row["home_ga_std"] = 0.5
        
        if len(away_hist["gf_last20"]) >= 2:
            row["away_gf_std"] = np.std(away_hist["gf_last20"])
            row["away_ga_std"] = np.std(away_hist["ga_last20"])
        else:
            row["away_gf_std"] = 0.5
            row["away_ga_std"] = 0.5
        
        # Momentum advantage
        row["momentum_advantage"] = row["home_momentum"] - row["away_momentum"]
        
        # Odds features (if available)
        for odds_prefix in ['B365', 'Avg', 'PS', 'Max']:
            over_col = f'{odds_prefix}>2.5'
            under_col = f'{odds_prefix}<2.5'
            
            if over_col in df.columns and under_col in df.columns:
                over_odds = float(match.get(over_col, np.nan))
                under_odds = float(match.get(under_col, np.nan))
                
                if pd.notna(over_odds) and pd.notna(under_odds):
                    row[f"{odds_prefix.lower()}_over_odds"] = over_odds
                    row[f"{odds_prefix.lower()}_under_odds"] = under_odds
                    
                    over_prob = 1.0 / over_odds
                    under_prob = 1.0 / under_odds
                    row[f"{odds_prefix.lower()}_over_prob"] = over_prob
                    row[f"{odds_prefix.lower()}_under_prob"] = under_prob
                    
                    fair_over = over_prob / (over_prob + under_prob)
                    row[f"{odds_prefix.lower()}_fair_over"] = fair_over
                    
                    row[f"{odds_prefix.lower()}_margin"] = (over_prob + under_prob - 1.0)
                    
                    row["market_over_prob"] = fair_over
                    row[f"{odds_prefix.lower()}_over_under_ratio"] = over_odds / (under_odds + 0.01)
                    
                    # Use first available odds source
                    break
        
        # 1X2 odds
        for odds_prefix in ['B365', 'Avg', 'PS', 'Max']:
            home_col = f'{odds_prefix}H'
            draw_col = f'{odds_prefix}D'
            away_col = f'{odds_prefix}A'
            
            if home_col in df.columns and draw_col in df.columns and away_col in df.columns:
                home_odds = float(match.get(home_col, np.nan))
                draw_odds = float(match.get(draw_col, np.nan))
                away_odds = float(match.get(away_col, np.nan))
                
                if pd.notna(home_odds) and pd.notna(draw_odds) and pd.notna(away_odds):
                    row[f"{odds_prefix.lower()}_home_odds"] = home_odds
                    row[f"{odds_prefix.lower()}_draw_odds"] = draw_odds
                    row[f"{odds_prefix.lower()}_away_odds"] = away_odds
                    
                    home_prob = 1.0 / home_odds
                    draw_prob = 1.0 / draw_odds
                    away_prob = 1.0 / away_odds
                    prob_sum = home_prob + draw_prob + away_prob
                    
                    if prob_sum > 0:
                        row[f"{odds_prefix.lower()}_home_prob"] = home_prob / prob_sum
                        row[f"{odds_prefix.lower()}_draw_prob"] = draw_prob / prob_sum
                        row[f"{odds_prefix.lower()}_away_prob"] = away_prob / prob_sum
                        row[f"{odds_prefix.lower()}_home_away_gap"] = (home_prob - away_prob)
                        row[f"{odds_prefix.lower()}_home_draw_gap"] = (home_prob - draw_prob)
                        row[f"{odds_prefix.lower()}_draw_away_gap"] = (draw_prob - away_prob)
                    else:
                        row[f"{odds_prefix.lower()}_home_prob"] = np.nan
                        row[f"{odds_prefix.lower()}_draw_prob"] = np.nan
                        row[f"{odds_prefix.lower()}_away_prob"] = np.nan
                        row[f"{odds_prefix.lower()}_home_away_gap"] = np.nan
                        row[f"{odds_prefix.lower()}_home_draw_gap"] = np.nan
                        row[f"{odds_prefix.lower()}_draw_away_gap"] = np.nan
                    
                    # Use first available odds source
                    break
        
        row["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)
        
        for window in [3, 5, 10, 15, 20]:
            home_hist[f"gf_last{window}"].append(total_goals)
            home_hist[f"ga_last{window}"].append(total_goals)
            home_hist[f"over_last{window}"].append(is_over)
            
            if window in [5, 10, 15, 20]:
                home_hist["home_gf"].append(float(match['FTHG']) if pd.notna(match.get('FTHG')) else np.nan)
                home_hist["home_ga"].append(float(match['FTAG']) if pd.notna(match.get('FTAG')) else np.nan)
                home_hist["home_over"].append(is_over)
        
        home_result = 1 if total_goals > (match.get('FTAG', 0) or 0) else 0.5
        home_hist["momentum"].append(home_result)
        home_hist["recent_form"].append(home_result)
        
        if len(home_hist["gf_last10"]) >= 2:
            recent = home_hist["gf_last10"][-5:]
            old = home_hist["gf_last10"][:-5]
            home_hist["goal_trend"].append(np.mean(recent) - np.mean(old))
        else:
            home_hist["goal_trend"].append(0)
        
        away_hist["gf"].append(total_goals)
        away_hist["ga"].append(total_goals)
        away_hist["tot"].append(total_goals)
        away_hist["over"].append(is_over)
        
        for window in [3, 5, 10, 15, 20]:
            away_hist[f"gf_last{window}"].append(total_goals)
            away_hist[f"ga_last{window}"].append(total_goals)
            away_hist[f"over_last{window}"].append(is_over)
            
            if window in [5, 10, 15, 20]:
                away_hist["away_gf"].append(float(match['FTHG']) if pd.notna(match.get('FTHG')) else np.nan)
                away_hist["away_ga"].append(float(match['FTAG']) if pd.notna(match.get('FTAG')) else np.nan)
                away_hist["away_over"].append(is_over)
        
        away_result = 1 if total_goals > (match.get('FTHG', 0) or 0) else 0.5
        away_hist["momentum"].append(away_result)
        away_hist["recent_form"].append(away_result)
        
        if len(away_hist["gf_last10"]) >= 2:
            recent = away_hist["gf_last10"][-5:]
            old = away_hist["gf_last10"][:-5]
            away_hist["goal_trend"].append(np.mean(recent) - np.mean(old))
        else:
            away_hist["goal_trend"].append(0)
    
    return pd.DataFrame(rows)


def train_ultra_final(league_code: str, league_config: dict) -> dict | None:
    """Ultra-final training for a league."""
    print(f"\n{'='*70}")
    print(f"Ultra-final training for {league_code} ({league_config['name']})")
    print(f"Target: {league_config['target']*100:.0f}%")
    print(f"{'='*70}")
    
    # 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 ultra features...")
    features_df = build_ultra_features(df)
    print(f"  Built {len(features_df)} features")
    
    # Prepare data
    target_col = 'is_over'
    feature_cols = [c for c in features_df.columns if c != target_col]
    
    # Remove high-NaN features
    nan_ratio = features_df[feature_cols].isna().mean()
    valid_features = [c for c, ratio in zip(feature_cols, nan_ratio) if ratio < 0.6]
    print(f"  Features after filtering: {len(valid_features)}")
    
    # 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)
        
        features_sorted = features_df.iloc[df_sorted.index].reset_index(drop=True)
        X = features_sorted[valid_features].fillna(0)
        y = features_sorted[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:
        split_idx = int(len(features_df) * 0.8)
        X = features_df[valid_features].fillna(0)
        y = features_df[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:]
    
    print(f"  Train: {len(X_train)}, Test: {len(X_test)}")
    print(f"  Over rate: {y.mean():.2%}")
    
    # Test all models
    models = []
    
    # 1. RandomForest (ultra)
    models.append(('RF_ultra_1', Pipeline([
        ('imputer', SimpleImputer(strategy='median')),
        ('scaler', RobustScaler()),
        ('clf', RandomForestClassifier(
            n_estimators=600,
            max_depth=35,
            min_samples_leaf=1,
            max_features='sqrt',
            random_state=42,
            n_jobs=-1,
            class_weight='balanced'
        ))
    ])))
    
    models.append(('RF_ultra_2', Pipeline([
        ('imputer', SimpleImputer(strategy='median')),
        ('scaler', RobustScaler()),
        ('clf', RandomForestClassifier(
            n_estimators=800,
            max_depth=40,
            min_samples_leaf=1,
            max_features=0.7,
            random_state=42,
            n_jobs=-1,
            class_weight='balanced'
        ))
    ])))
    
    # 2. ExtraTrees (ultra)
    models.append(('ET_ultra', Pipeline([
        ('imputer', SimpleImputer(strategy='median')),
        ('scaler', RobustScaler()),
        ('clf', ExtraTreesClassifier(
            n_estimators=600,
            max_depth=30,
            min_samples_leaf=1,
            max_features='sqrt',
            random_state=42,
            n_jobs=-1,
            class_weight='balanced'
        ))
    ])))
    
    # 3. GradientBoosting (ultra)
    models.append(('GB_ultra_1', Pipeline([
        ('imputer', SimpleImputer(strategy='median')),
        ('scaler', RobustScaler()),
        ('clf', GradientBoostingClassifier(
            n_estimators=500,
            max_depth=10,
            learning_rate=0.05,
            subsample=0.85,
            random_state=42
        ))
    ])))
    
    models.append(('GB_ultra_2', Pipeline([
        ('imputer', SimpleImputer(strategy='median')),
        ('scaler', RobustScaler()),
        ('clf', GradientBoostingClassifier(
            n_estimators=800,
            max_depth=8,
            learning_rate=0.03,
            subsample=0.9,
            random_state=42
        ))
    ])))
    
    # 4. MLP (ultra)
    models.append(('MLP_ultra_1', Pipeline([
        ('imputer', SimpleImputer(strategy='mean')),
        ('scaler', MinMaxScaler()),
        ('clf', MLPClassifier(
            hidden_layer_sizes=(256, 128, 64, 32),
            max_iter=800,
            alpha=0.005,
            learning_rate='adaptive',
            random_state=42,
            batch_size=64
        ))
    ])))
    
    models.append(('MLP_ultra_2', Pipeline([
        ('imputer', SimpleImputer(strategy='mean')),
        ('scaler', MinMaxScaler()),
        ('clf', MLPClassifier(
            hidden_layer_sizes=(512, 256, 128, 64),
            max_iter=1000,
            alpha=0.001,
            learning_rate='adaptive',
            random_state=42,
            batch_size=128
        ))
    ])))
    
    # 5. LogisticRegression
    models.append(('LR_ultra', Pipeline([
        ('imputer', SimpleImputer(strategy='mean')),
        ('scaler', StandardScaler()),
        ('clf', LogisticRegression(
            C=100.0,
            penalty='l2',
            max_iter=3000,
            random_state=42,
            class_weight='balanced'
        ))
    ])))
    
    print(f"\n  Training {len(models)} ultra 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.35, 0.91, 0.01)
                
                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),
                            'f1_score': float(f1_score(y_test[mask], preds[mask])),
                            'rows': int(len(features_sorted)),
                            'train_rows': int(len(X_train)),
                            'test_rows': int(len(X_test)),
                            'features': valid_features,
                            'feature_count': len(valid_features),
                        }
                        
                        print(f"    New best! Threshold={threshold:.2f}, Bet Acc={bet_acc:.2%}, Bet Count={bet_count}")
                
                # Early stop if target met
                if best_config['bet_acc'] >= league_config['target']:
                    print(f"    [SUCCESS] Target {league_config['target']*100:.0f}% met!")
                    break
            else:
                # For models without predict_proba
                print(f"    Overall Acc: {overall_acc:.2%} (no predict_proba)")
                if overall_acc > best_test_acc:
                    best_test_acc = overall_acc
                    best_model = pipeline
                    best_config = {
                        'league': league_code,
                        'league_name': league_config['name'],
                        'model': model_name,
                        'threshold': 0.50,
                        'bet_count': len(y_test),
                        'bet_acc': float(overall_acc),
                        'overall_acc': float(overall_acc),
                        'rows': int(len(features_sorted)),
                        'train_rows': int(len(X_train)),
                        'test_rows': int(len(X_test)),
                        'features': valid_features,
                        'feature_count': len(valid_features),
                    }
        
        except Exception as e:
            print(f"  {model_name}: Error - {e}")
            continue
        
        # Early stop if target met overall
        if best_config and best_config['bet_acc'] >= league_config['target']:
            print(f"  [SUCCESS] Target reached! Accuracy: {best_config['bet_acc']:.2%}")
            break
    
    if not best_config:
        print(f"  [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"    F1 score: {best_config['f1_score']:.2%}")
    print(f"    Features: {best_config['feature_count']}")
    print(f"  {'='*60}")
    
    if best_config['bet_acc'] >= league_config['target']:
        print(f"  [SUCCESS] Target met! Accuracy: {best_config['bet_acc']:.2%}")
    else:
        print(f"  [WARNING] Accuracy {best_config['bet_acc']:.2%} is below target {league_config['target']*100:.0f}%")
    
    # 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)
    
    import joblib
    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("="*70)
    print("Ultra-final batch training for ALL leagues")
    print("Target: 80%+ bet accuracy")
    print("="*70)
    
    results = []
    
    for league_code, league_config in LEAGUES.items():
        result = train_ultra_final(league_code, league_config)
        if result:
            results.append(result)
    
    # Summary
    print(f"\n{'='*70}")
    print("FINAL SUMMARY")
    print(f"{'='*70}")
    
    if results:
        results_sorted = sorted(results, key=lambda x: x['bet_acc'], reverse=True)
        
        print(f"\nLeagues reaching 80%+ target:")
        high_acc_leagues = [r for r in results_sorted if r['bet_acc'] >= 0.80]
        
        for idx, result in enumerate(high_acc_leagues, 1):
            print(f"  {idx}. {result['league']} ({result['league_name']}): ✓")
            print(f"     Model: {result['model']}")
            print(f"     Threshold: {result['threshold']:.2f}")
            print(f"     Bet count: {result['bet_count']}")
            print(f"     Bet accuracy: {result['bet_acc']:.2%}")
            print()
        
        print(f"\nLeagues below 80%:")
        low_acc_leagues = [r for r in results_sorted if r['bet_acc'] < 0.80]
        
        for idx, result in enumerate(low_acc_leagues, 1):
            print(f"  {idx}. {result['league']} ({result['league_name']}):")
            print(f"     Model: {result['model']}")
            print(f"     Threshold: {result['threshold']:.2f}")
            print(f"     Bet count: {result['bet_count']}")
            print(f"     Bet accuracy: {result['bet_acc']:.2%}")
            print()
        
        # Save summary
        summary_path = OUTPUT_DIR / 'final_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 saved to: {summary_path}")
        
        print(f"\nOverall statistics:")
        print(f"  Total leagues: {len(results)}")
        print(f"  Leagues meeting 80%+ target: {len(high_acc_leagues)}/{len(results)}")
        
        if high_acc_leagues:
            avg_acc = sum(r['bet_acc'] for r in high_acc_leagues) / len(high_acc_leagues)
            print(f"  Average accuracy of 80%+ leagues: {avg_acc:.2%}")
        
        best_result = max(results, key=lambda x: x['bet_acc'])
        print(f"\nBest model: {best_result['league']} ({best_result['league_name']}) - {best_result['bet_acc']:.2%}")
    else:
        print("\nNo models trained!")


if __name__ == '__main__':
    main()
