#!/usr/bin/env python3
"""
分联赛全面特征大小球预测模型
- 每个联赛单独训练
- 获取完整的predictions和injuries数据
- 使用所有可用特征
"""

import os
import re
import json
import math
import time
import requests
from collections import defaultdict
from datetime import datetime, timedelta, timezone
from pathlib import Path

import numpy as np
import pandas as pd
import joblib
from sklearn.ensemble import RandomForestClassifier
from sklearn.impute import SimpleImputer
from sklearn.metrics import accuracy_score
from sklearn.pipeline import Pipeline

REPO_ROOT = Path(__file__).resolve().parents[1]
CACHE_DIR = REPO_ROOT / 'artifacts' / 'goal_number_api_dynamic_cache'
ARTIFACT_DIR = REPO_ROOT / 'experiments' / 'goal_number_per_league'
ARTIFACT_DIR.mkdir(parents=True, exist_ok=True)

BEIJING_TZ = timezone(timedelta(hours=8))

# ==================== API客户端 ====================

base_url = os.environ.get('API_FOOTBALL_BASE_URL', '').rstrip('/')
api_key = os.environ.get('API_FOOTBALL_KEY', '')
headers = {'x-apisports-key': api_key}

def fetch_json(url, params, max_retries=3):
    """API请求"""
    for attempt in range(max_retries):
        try:
            resp = requests.get(url, params=params, headers=headers, timeout=30)
            if resp.status_code == 200:
                return resp.json()
            elif resp.status_code == 429:
                print(f"    Rate limited, waiting 15s...")
                time.sleep(15)
            else:
                return None
        except Exception as e:
            if attempt < max_retries - 1:
                time.sleep(3)
            else:
                return None
    return None

def fetch_predictions(fixture_id):
    """获取预测数据"""
    cache_path = CACHE_DIR / f'predictions_{fixture_id}.json'
    if cache_path.exists():
        try:
            return json.loads(cache_path.read_text())
        except:
            pass
    
    data = fetch_json(f'{base_url}/predictions', {'fixture': fixture_id})
    if data:
        cache_path.write_text(json.dumps(data))
    return data

def fetch_injuries(fixture_id):
    """获取伤病数据"""
    cache_path = CACHE_DIR / f'injuries_{fixture_id}.json'
    if cache_path.exists():
        try:
            return json.loads(cache_path.read_text())
        except:
            pass
    
    data = fetch_json(f'{base_url}/injuries', {'fixture': fixture_id})
    if data:
        cache_path.write_text(json.dumps(data))
    return data

# ==================== 特征提取 ====================

def mean_or_default(values, default):
    return float(np.mean(values)) if values else float(default)

def parse_percent(s):
    if s is None:
        return None
    s = str(s).replace('%', '').strip()
    return float(s) if s else None

def parse_form_wdl(s):
    if not s:
        return 0, 0, 0
    s = str(s).upper()
    return sum(1 for c in s if c == 'W'), sum(1 for c in s if c == 'D'), sum(1 for c in s if c == 'L')

def extract_prediction_data(pred_data):
    """从predictions API响应提取特征"""
    if not pred_data or not pred_data.get('response'):
        return None
    
    pred = pred_data['response'][0]
    result = {
        'home_form_pct': None, 'away_form_pct': None,
        'home_att': None, 'away_att': None,
        'home_def': None, 'away_def': None,
        'home_last5_wins': 0, 'home_last5_draws': 0, 'home_last5_losses': 0,
        'away_last5_wins': 0, 'away_last5_draws': 0, 'away_last5_losses': 0,
        'home_goals_for_avg': None, 'home_goals_against_avg': None,
        'away_goals_for_avg': None, 'away_goals_against_avg': None,
        'home_over_25_rate': None, 'away_over_25_rate': None,
        'h2h_home_wins': 0, 'h2h_draws': 0, 'h2h_away_wins': 0,
        'comparison_form_home': None, 'comparison_att_home': None,
        'comparison_poisson_home': None, 'comparison_total_home': None,
    }
    
    teams = pred.get('teams', {})
    home_team = teams.get('home', {})
    away_team = teams.get('away', {})
    
    # Last 5
    home_last5 = home_team.get('last_5', {})
    away_last5 = away_team.get('last_5', {})
    result['home_form_pct'] = parse_percent(home_last5.get('form'))
    result['away_form_pct'] = parse_percent(away_last5.get('form'))
    result['home_att'] = parse_percent(home_last5.get('att'))
    result['away_att'] = parse_percent(away_last5.get('att'))
    result['home_def'] = parse_percent(home_last5.get('def'))
    result['away_def'] = parse_percent(away_last5.get('def'))
    
    # Form string
    home_league = home_team.get('league', {})
    away_league = away_team.get('league', {})
    w, d, l = parse_form_wdl(home_league.get('form'))
    result['home_last5_wins'] = w; result['home_last5_draws'] = d; result['home_last5_losses'] = l
    w, d, l = parse_form_wdl(away_league.get('form'))
    result['away_last5_wins'] = w; result['away_last5_draws'] = d; result['away_last5_losses'] = l
    
    # Goals stats
    home_goals = home_team.get('goals', {})
    away_goals = away_team.get('goals', {})
    result['home_goals_for_avg'] = parse_percent(home_goals.get('average', {}).get('total'))
    result['away_goals_for_avg'] = parse_percent(away_goals.get('average', {}).get('total'))
    
    # Under/over 2.5
    for team_data, key in [(home_goals, 'home'), (away_goals, 'away')]:
        uo = team_data.get('under_over', {})
        if '2.5' in uo:
            over = uo['2.5'].get('over', 0)
            under = uo['2.5'].get('under', 0)
            total = over + under
            if total > 0:
                result[f'{key}_over_25_rate'] = over / total
    
    # H2H
    h2h = pred.get('h2h', [])
    for match in h2h[:10]:
        winner = match.get('teams', {}).get('home', {}).get('winner')
        if winner == True:
            result['h2h_home_wins'] += 1
        elif winner == False:
            result['h2h_away_wins'] += 1
        else:
            result['h2h_draws'] += 1
    
    # Comparison
    comparison = pred.get('comparison', {})
    for key in ['form', 'att', 'poisson', 'total']:
        if key in comparison:
            result[f'comparison_{key}_home'] = parse_percent(comparison[key].get('home'))
    
    return result

def extract_injuries(inj_data):
    """从injuries API响应提取伤病数"""
    if not inj_data or not inj_data.get('response'):
        return 0, 0
    return len(inj_data['response']) // 2, len(inj_data['response']) // 2  # 简化处理

def choose_main_total(bookmakers):
    """选择主盘口"""
    per_line = defaultdict(lambda: {'quotes': 0, 'over': [], 'under': []})
    
    for bookmaker in bookmakers:
        for bet in bookmaker.get('bets', []):
            if bet.get('name') != 'Goals Over/Under':
                continue
            local = defaultdict(dict)
            for item in bet.get('values', []):
                val = item.get('value', '')
                match = re.match(r'^(Over|Under)\s+([0-9.]+)$', val, re.I)
                if not match:
                    continue
                try:
                    odd = float(item.get('odd'))
                    if odd <= 1:
                        continue
                except:
                    continue
                local[float(match.group(2))][match.group(1).lower()] = odd
            for line, sides in local.items():
                if 'over' not in sides or 'under' not in sides:
                    continue
                rec = per_line[line]
                rec['quotes'] += 1
                rec['over'].append(float(sides['over']))
                rec['under'].append(float(sides['under']))
    
    if not per_line:
        return None
    
    candidates = []
    for line, rec in per_line.items():
        over_odds = float(np.mean(rec['over']))
        under_odds = float(np.mean(rec['under']))
        over_prob = 1/over_odds if over_odds > 1 else 0.5
        under_prob = 1/under_odds if under_odds > 1 else 0.5
        margin = (over_prob + under_prob) - 1.0
        candidates.append({'line': line, 'over_odds': over_odds, 'under_odds': under_odds, 'margin': margin, 'quotes': rec['quotes']})
    
    candidates.sort(key=lambda x: (x['margin'], -x['quotes']))
    best = candidates[0]
    
    return {
        'ou_line': best['line'],
        'over_odds': best['over_odds'],
        'under_odds': best['under_odds'],
    }

def extract_prob(odds):
    if odds is None or odds <= 0:
        return np.nan
    return 1.0 / float(odds)

# ==================== 特征构建 ====================

FEATURE_COLS = [
    # O/U
    'ou_line', 'over_odds', 'under_odds', 'over_implied_prob', 'under_implied_prob', 'odds_margin',
    # Team form
    'home_form_pct', 'away_form_pct', 'home_att', 'away_att', 'home_def', 'away_def',
    # WDL
    'home_last5_wins', 'home_last5_draws', 'home_last5_losses',
    'away_last5_wins', 'away_last5_draws', 'away_last5_losses',
    # Goals
    'home_goals_for_avg', 'away_goals_for_avg',
    'home_over_25_rate', 'away_over_25_rate',
    # H2H
    'h2h_home_wins', 'h2h_draws', 'h2h_away_wins',
    # Comparison
    'comparison_form_home', 'comparison_att_home', 'comparison_poisson_home', 'comparison_total_home',
    # Injuries
    'home_injuries', 'away_injuries',
    # Historical
    'home_gf_avg', 'home_ga_avg', 'away_gf_avg', 'away_ga_avg',
    'home_tot_avg', 'away_tot_avg',
    'home_over_rate', 'away_over_rate',
    'home_home_gf_avg', 'home_home_ga_avg',
    'away_away_gf_avg', 'away_away_ga_avg',
    'home_home_over_rate', 'away_away_over_rate',
    'home_gf_last5', 'home_ga_last5',
    'away_gf_last5', 'away_ga_last5',
    'home_over_last5', 'away_over_last5',
    # Derived
    'expected_goals_proxy', 'combined_over_rate',
    'form_diff', 'att_diff', 'def_diff',
    'h2h_home_win_rate',
]

def build_features(home_hist, away_hist, league_hist, ou_info, pred_info, injuries):
    league_tot_avg = mean_or_default(league_hist['tot'], 2.6)
    
    # Historical
    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'], league_tot_avg)
    away_tot_avg = mean_or_default(away_hist['tot'], league_tot_avg)
    home_over_rate = mean_or_default(home_hist['over'], 0.5)
    away_over_rate = mean_or_default(away_hist['over'], 0.5)
    home_home_gf_avg = mean_or_default(home_hist['home_gf'], home_gf_avg)
    home_home_ga_avg = mean_or_default(home_hist['home_ga'], home_ga_avg)
    away_away_gf_avg = mean_or_default(away_hist['away_gf'], away_gf_avg)
    away_away_ga_avg = mean_or_default(away_hist['away_ga'], away_ga_avg)
    home_home_over_rate = mean_or_default(home_hist['home_over'], home_over_rate)
    away_away_over_rate = mean_or_default(away_hist['away_over'], away_over_rate)
    home_gf_last5 = mean_or_default(home_hist['gf'][-5:], home_gf_avg)
    home_ga_last5 = mean_or_default(home_hist['ga'][-5:], home_ga_avg)
    away_gf_last5 = mean_or_default(away_hist['gf'][-5:], away_gf_avg)
    away_ga_last5 = mean_or_default(away_hist['ga'][-5:], away_ga_avg)
    home_over_last5 = mean_or_default(home_hist['over'][-5:], home_over_rate)
    away_over_last5 = mean_or_default(away_hist['over'][-5:], away_over_rate)
    expected_goals_proxy = np.nanmean([home_gf_avg, away_ga_avg])
    combined_over_rate = np.nanmean([home_over_rate, away_over_rate])
    
    # O/U
    ou_line = ou_info.get('ou_line', 2.5) if ou_info else 2.5
    over_odds = ou_info.get('over_odds', 2.0) if ou_info else 2.0
    under_odds = ou_info.get('under_odds', 2.0) if ou_info else 2.0
    over_implied = extract_prob(over_odds)
    under_implied = extract_prob(under_odds)
    odds_margin = (over_implied + under_implied) - 1.0 if not (math.isnan(over_implied) or math.isnan(under_implied)) else 0
    
    # Prediction
    p = pred_info or {}
    
    # Injuries
    home_inj = injuries[0] if injuries else 0
    away_inj = injuries[1] if injuries else 0
    
    # Derived
    form_diff = (p.get('home_form_pct') or 50) - (p.get('away_form_pct') or 50)
    att_diff = (p.get('home_att') or 50) - (p.get('away_att') or 50)
    def_diff = (p.get('home_def') or 50) - (p.get('away_def') or 50)
    total_h2h = (p.get('h2h_home_wins') or 0) + (p.get('h2h_draws') or 0) + (p.get('h2h_away_wins') or 0)
    h2h_home_win_rate = (p.get('h2h_home_wins') or 0) / total_h2h if total_h2h > 0 else 0.5
    
    return {
        'ou_line': ou_line, 'over_odds': over_odds, 'under_odds': under_odds,
        'over_implied_prob': over_implied, 'under_implied_prob': under_implied, 'odds_margin': odds_margin,
        'home_form_pct': p.get('home_form_pct'), 'away_form_pct': p.get('away_form_pct'),
        'home_att': p.get('home_att'), 'away_att': p.get('away_att'),
        'home_def': p.get('home_def'), 'away_def': p.get('away_def'),
        'home_last5_wins': p.get('home_last5_wins', 0), 'home_last5_draws': p.get('home_last5_draws', 0), 'home_last5_losses': p.get('home_last5_losses', 0),
        'away_last5_wins': p.get('away_last5_wins', 0), 'away_last5_draws': p.get('away_last5_draws', 0), 'away_last5_losses': p.get('away_last5_losses', 0),
        'home_goals_for_avg': p.get('home_goals_for_avg'), 'away_goals_for_avg': p.get('away_goals_for_avg'),
        'home_over_25_rate': p.get('home_over_25_rate'), 'away_over_25_rate': p.get('away_over_25_rate'),
        'h2h_home_wins': p.get('h2h_home_wins', 0), 'h2h_draws': p.get('h2h_draws', 0), 'h2h_away_wins': p.get('h2h_away_wins', 0),
        'comparison_form_home': p.get('comparison_form_home'), 'comparison_att_home': p.get('comparison_att_home'),
        'comparison_poisson_home': p.get('comparison_poisson_home'), 'comparison_total_home': p.get('comparison_total_home'),
        'home_injuries': home_inj, 'away_injuries': away_inj,
        'home_gf_avg': home_gf_avg, 'home_ga_avg': home_ga_avg,
        'away_gf_avg': away_gf_avg, 'away_ga_avg': away_ga_avg,
        'home_tot_avg': home_tot_avg, 'away_tot_avg': away_tot_avg,
        'home_over_rate': home_over_rate, 'away_over_rate': away_over_rate,
        'home_home_gf_avg': home_home_gf_avg, 'home_home_ga_avg': home_home_ga_avg,
        'away_away_gf_avg': away_away_gf_avg, 'away_away_ga_avg': away_away_ga_avg,
        'home_home_over_rate': home_home_over_rate, 'away_away_over_rate': away_away_over_rate,
        'home_gf_last5': home_gf_last5, 'home_ga_last5': home_ga_last5,
        'away_gf_last5': away_gf_last5, 'away_ga_last5': away_ga_last5,
        'home_over_last5': home_over_last5, 'away_over_last5': away_over_last5,
        'expected_goals_proxy': expected_goals_proxy, 'combined_over_rate': combined_over_rate,
        'form_diff': form_diff, 'att_diff': att_diff, 'def_diff': def_diff,
        'h2h_home_win_rate': h2h_home_win_rate,
    }

# ==================== 主程序 ====================

def main():
    import sys
    sys.path.insert(0, str(Path(__file__).parent))
    from goal_number_api_dynamic import LEAGUE_CODE_MAP, LEAGUE_ZH_MAP, load_serving_codes, SUPPORTED_FINAL_STATUSES
    
    allowed_codes = load_serving_codes()
    
    print("="*70)
    print("分联赛全面特征大小球预测模型")
    print("="*70)
    
    # 获取所有联赛的数据量
    league_counts = defaultdict(int)
    all_dates_data = {}
    
    for i in range(60, 0, -1):
        date_str = (datetime.now() - timedelta(days=i)).strftime('%Y-%m-%d')
        fixtures_path = CACHE_DIR / f'fixtures_date_{date_str}.json'
        odds_path = CACHE_DIR / f'odds_date_{date_str}.json'
        
        if not fixtures_path.exists():
            continue
        
        fixtures_data = json.loads(fixtures_path.read_text())
        odds_data = json.loads(odds_path.read_text()) if odds_path.exists() else []
        
        fixture_map = {int(x['fixture']['id']): x for x in fixtures_data}
        odds_map = {int(item['fixture']['id']): item for item in odds_data}
        
        for fixture_id, fx in fixture_map.items():
            if fx['fixture']['status']['short'] not in SUPPORTED_FINAL_STATUSES:
                continue
            
            odds_item = odds_map.get(fixture_id)
            if not odds_item:
                continue
            
            chosen = choose_main_total(odds_item.get('bookmakers', []))
            if not chosen:
                continue
            
            league = fx.get('league', {})
            league_key = (league.get('country'), league.get('name'))
            league_code = LEAGUE_CODE_MAP.get(league_key, f"api_{league.get('id')}")
            if league_code not in allowed_codes:
                continue
            
            league_counts[league_code] += 1
    
    print("\n各联赛数据量:")
    for code, count in sorted(league_counts.items(), key=lambda x: -x[1])[:15]:
        name = LEAGUE_ZH_MAP.get(code, code)
        print(f"  {name} ({code}): {count} 场")
    
    # 选择数据量足够的联赛进行训练
    target_leagues = [code for code, count in league_counts.items() if count >= 15]
    print(f"\n选择 {len(target_leagues)} 个联赛进行训练 (>=30场)")
    
    all_results = {}
    
    for league_code in target_leagues:
        print(f"\n{'='*70}")
        print(f"处理联赛: {LEAGUE_ZH_MAP.get(league_code, league_code)} ({league_code})")
        print("="*70)
        
        # 收集该联赛数据
        league_rows = []
        
        for i in range(60, 0, -1):
            date_str = (datetime.now() - timedelta(days=i)).strftime('%Y-%m-%d')
            fixtures_path = CACHE_DIR / f'fixtures_date_{date_str}.json'
            odds_path = CACHE_DIR / f'odds_date_{date_str}.json'
            
            if not fixtures_path.exists():
                continue
            
            fixtures_data = json.loads(fixtures_path.read_text())
            odds_data = json.loads(odds_path.read_text()) if odds_path.exists() else []
            
            fixture_map = {int(x['fixture']['id']): x for x in fixtures_data}
            odds_map = {int(item['fixture']['id']): item for item in odds_data}
            
            for fixture_id, fx in fixture_map.items():
                if fx['fixture']['status']['short'] not in SUPPORTED_FINAL_STATUSES:
                    continue
                
                odds_item = odds_map.get(fixture_id)
                if not odds_item:
                    continue
                
                chosen = choose_main_total(odds_item.get('bookmakers', []))
                if not chosen:
                    continue
                
                league = fx.get('league', {})
                lk = (league.get('country'), league.get('name'))
                lc = LEAGUE_CODE_MAP.get(lk, f"api_{league.get('id')}")
                if lc != league_code:
                    continue
                
                home_goals = int(fx['goals']['home'])
                away_goals = int(fx['goals']['away'])
                total = home_goals + away_goals
                ou_line = chosen['ou_line']
                is_over = 1 if total > ou_line else 0
                
                league_rows.append({
                    'date': date_str,
                    'fixture_id': fixture_id,
                    'league_code': lc,
                    'home': fx['teams']['home']['name'],
                    'away': fx['teams']['away']['name'],
                    'ou_line': ou_line,
                    'over_odds': chosen['over_odds'],
                    'under_odds': chosen['under_odds'],
                    'home_goals': home_goals,
                    'away_goals': away_goals,
                    'is_over': is_over,
                })
        
        if len(league_rows) < 30:
            print(f"  数据不足: {len(league_rows)} 场")
            continue
        
        print(f"  收集到 {len(league_rows)} 场比赛")
        
        # 获取predictions和injuries数据
        print("  获取predictions和injuries数据...")
        for row in league_rows:
            fixture_id = row['fixture_id']
            
            # Predictions
            pred_data = fetch_predictions(fixture_id)
            if pred_data:
                row['pred_info'] = extract_prediction_data(pred_data)
            else:
                row['pred_info'] = None
            
            # Injuries
            inj_data = fetch_injuries(fixture_id)
            if inj_data:
                row['injuries'] = extract_injuries(inj_data)
            else:
                row['injuries'] = (0, 0)
            
            time.sleep(0.3)  # 避免过快
        
        # 分割训练/测试 (70/30)
        league_rows.sort(key=lambda x: x['date'])
        split_idx = int(len(league_rows) * 0.7)
        train_rows = league_rows[:split_idx]
        test_rows = league_rows[split_idx:]
        
        print(f"  训练: {len(train_rows)} 场, 测试: {len(test_rows)} 场")
        
        # 构建特征
        team_hist = defaultdict(lambda: {'gf': [], 'ga': [], 'tot': [], 'over': [], 
                                          'home_gf': [], 'home_ga': [], 'home_over': [],
                                          'away_gf': [], 'away_ga': [], 'away_over': []})
        league_hist = defaultdict(lambda: {'tot': [], 'over': []})
        
        train_features = []
        train_labels = []
        
        for row in train_rows:
            hh = team_hist[(league_code, row['home'])]
            aa = team_hist[(league_code, row['away'])]
            ll = league_hist[league_code]
            
            ou_info = {'ou_line': row['ou_line'], 'over_odds': row['over_odds'], 'under_odds': row['under_odds']}
            features = build_features(hh, aa, ll, ou_info, row.get('pred_info'), row.get('injuries'))
            train_features.append(features)
            train_labels.append(row['is_over'])
            
            # Update
            total = row['home_goals'] + row['away_goals']
            is_over = row['is_over']
            hh['gf'].append(float(row['home_goals'])); hh['ga'].append(float(row['away_goals']))
            hh['tot'].append(total); hh['over'].append(float(is_over))
            hh['home_gf'].append(float(row['home_goals'])); hh['home_ga'].append(float(row['away_goals']))
            hh['home_over'].append(float(is_over))
            aa['gf'].append(float(row['away_goals'])); aa['ga'].append(float(row['home_goals']))
            aa['tot'].append(total); aa['over'].append(float(is_over))
            aa['away_gf'].append(float(row['away_goals'])); aa['away_ga'].append(float(row['away_goals']))
            aa['away_over'].append(float(is_over))
            ll['tot'].append(total); ll['over'].append(float(is_over))
        
        X_train = pd.DataFrame(train_features)
        available_cols = [c for c in FEATURE_COLS if c in X_train.columns]
        X_train = X_train[available_cols]
        y_train = np.array(train_labels)
        
        print(f"  特征数: {len(available_cols)}, 缺失率: {X_train.isnull().sum().sum() / X_train.size:.1%}")
        
        # 训练
        model = Pipeline([
            ('imp', SimpleImputer(strategy='median')),
            ('clf', RandomForestClassifier(
                n_estimators=300, max_depth=10, min_samples_leaf=3,
                random_state=42, class_weight='balanced', n_jobs=-1
            ))
        ])
        model.fit(X_train, y_train)
        
        # 保存模型
        model_path = ARTIFACT_DIR / f'{league_code}_model.pkl'
        joblib.dump((model, available_cols), model_path)
        
        # 测试
        team_hist = defaultdict(lambda: {'gf': [], 'ga': [], 'tot': [], 'over': [], 
                                          'home_gf': [], 'home_ga': [], 'home_over': [],
                                          'away_gf': [], 'away_ga': [], 'away_over': []})
        league_hist = defaultdict(lambda: {'tot': [], 'over': []})
        
        # 用训练数据建立历史
        for row in train_rows:
            hh = team_hist[(league_code, row['home'])]
            aa = team_hist[(league_code, row['away'])]
            ll = league_hist[league_code]
            total = row['home_goals'] + row['away_goals']
            is_over = row['is_over']
            hh['gf'].append(float(row['home_goals'])); hh['ga'].append(float(row['away_goals']))
            hh['tot'].append(total); hh['over'].append(float(is_over))
            hh['home_gf'].append(float(row['home_goals'])); hh['home_ga'].append(float(row['away_goals']))
            hh['home_over'].append(float(is_over))
            aa['gf'].append(float(row['away_goals'])); aa['ga'].append(float(row['home_goals']))
            aa['tot'].append(total); aa['over'].append(float(is_over))
            aa['away_gf'].append(float(row['away_goals'])); aa['away_ga'].append(float(row['away_goals']))
            aa['away_over'].append(float(is_over))
            ll['tot'].append(total); ll['over'].append(float(is_over))
        
        test_features = []
        test_labels = []
        
        for row in test_rows:
            hh = team_hist[(league_code, row['home'])]
            aa = team_hist[(league_code, row['away'])]
            ll = league_hist[league_code]
            
            ou_info = {'ou_line': row['ou_line'], 'over_odds': row['over_odds'], 'under_odds': row['under_odds']}
            features = build_features(hh, aa, ll, ou_info, row.get('pred_info'), row.get('injuries'))
            test_features.append(features)
            test_labels.append(row['is_over'])
            
            # Update
            total = row['home_goals'] + row['away_goals']
            is_over = row['is_over']
            hh['gf'].append(float(row['home_goals'])); hh['ga'].append(float(row['away_goals']))
            hh['tot'].append(total); hh['over'].append(float(is_over))
            hh['home_gf'].append(float(row['home_goals'])); hh['home_ga'].append(float(row['away_goals']))
            hh['home_over'].append(float(is_over))
            aa['gf'].append(float(row['away_goals'])); aa['ga'].append(float(row['home_goals']))
            aa['tot'].append(total); aa['over'].append(float(is_over))
            aa['away_gf'].append(float(row['away_goals'])); aa['away_ga'].append(float(row['away_goals']))
            aa['away_over'].append(float(is_over))
            ll['tot'].append(total); ll['over'].append(float(is_over))
        
        X_test = pd.DataFrame(test_features)[available_cols]
        y_test = np.array(test_labels)
        
        probs = model.predict_proba(X_test)[:, 1]
        
        # 评估
        results = {'league': LEAGUE_ZH_MAP.get(league_code, league_code), 'league_code': league_code}
        
        for threshold in [0.55, 0.58, 0.60, 0.65]:
            mask = (probs >= threshold) | (probs <= (1 - threshold))
            if mask.sum() >= 5:
                certain_probs = probs[mask]
                certain_labels = y_test[mask]
                certain_preds = (certain_probs >= threshold).astype(int)
                acc = accuracy_score(certain_labels, certain_preds)
                
                over_mask = certain_preds == 1
                under_mask = certain_preds == 0
                over_acc = accuracy_score(certain_labels[over_mask], certain_preds[over_mask]) if over_mask.sum() > 0 else 0
                under_acc = accuracy_score(certain_labels[under_mask], certain_preds[under_mask]) if under_mask.sum() > 0 else 0
                
                results[f'threshold_{threshold}'] = {
                    'count': int(mask.sum()),
                    'accuracy': float(acc),
                    'over_count': int(over_mask.sum()),
                    'over_accuracy': float(over_acc),
                    'under_count': int(under_mask.sum()),
                    'under_accuracy': float(under_acc),
                }
        
        results['baseline'] = float(y_test.mean())
        results['test_count'] = len(y_test)
        results['train_count'] = len(train_rows)
        
        all_results[league_code] = results
        
        # 打印结果
        print(f"\n  测试结果 ({len(test_rows)} 场, 基准大球率: {y_test.mean():.1%}):")
        for threshold in [0.55, 0.58, 0.60, 0.65]:
            if f'threshold_{threshold}' in results:
                r = results[f'threshold_{threshold}']
                if r['count'] >= 5:
                    print(f"    阈值={threshold}: {r['count']}场 准确率={r['accuracy']:.1%} (大球{r['over_count']}场{r['over_accuracy']:.1%}, 小球{r['under_count']}场{r['under_accuracy']:.1%})")
    
    # 保存所有结果
    output_path = ARTIFACT_DIR / 'all_results.json'
    with open(output_path, 'w') as f:
        json.dump(all_results, f, indent=2)
    
    print("\n" + "="*70)
    print("所有联赛汇总")
    print("="*70)
    
    print(f"\n{'联赛':<15} {'训练':<6} {'测试':<6} {'基准':<8} {'阈值0.58准确率':<15} {'大球数/准确率'}")
    print("-" * 80)
    
    for code, res in sorted(all_results.items(), key=lambda x: -x[1].get('threshold_0.58', {}).get('accuracy', 0)):
        league_name = res['league'][:12]
        train_n = res['train_count']
        test_n = res['test_count']
        baseline = f"{res['baseline']:.1%}"
        
        if 'threshold_0.58' in res and res['threshold_0.58']['count'] >= 5:
            r = res['threshold_0.58']
            acc = f"{r['accuracy']:.1%}"
            over_info = f"{r['over_count']}场/{r['over_accuracy']:.1%}"
        else:
            acc = "-"
            over_info = "-"
        
        print(f"{league_name:<15} {train_n:<6} {test_n:<6} {baseline:<8} {acc:<15} {over_info}")
    
    print(f"\n结果已保存: {output_path}")

if __name__ == '__main__':
    main()
