#!/usr/bin/env python3
"""
全面特征大小球预测模型
- O/U赔率特征
- 球队形态/状态特征
- 历史交锋特征
- 伤病情况
- 博彩公司预测对比
"""

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, GradientBoostingClassifier
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_comprehensive'
ARTIFACT_DIR.mkdir(parents=True, exist_ok=True)

BEIJING_TZ = timezone(timedelta(hours=8))

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

def fetch_json(url, params, headers, cache_path=None, max_retries=3):
    """带缓存的API请求"""
    if cache_path and cache_path.exists():
        return json.loads(cache_path.read_text())
    
    for attempt in range(max_retries):
        try:
            resp = requests.get(url, params=params, headers=headers, timeout=30)
            if resp.status_code == 200:
                data = resp.json()
                if cache_path:
                    cache_path.write_text(json.dumps(data))
                return data
            elif resp.status_code == 429:
                time.sleep(10)
            else:
                break
        except Exception as e:
            if attempt < max_retries - 1:
                time.sleep(2)
            else:
                return None
    return None

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

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).strip()
    if not s:
        return None
    s = s.replace('%', '').strip()
    try:
        return float(s)
    except:
        return None

def parse_form_string(s):
    """解析WDL形态字符串，返回最近5场的胜负平"""
    if not s:
        return 0, 0, 0
    wins = sum(1 for c in str(s).upper() if c == 'W')
    draws = sum(1 for c in str(s).upper() if c == 'D')
    losses = sum(1 for c in str(s).upper() if c == 'L')
    return wins, draws, losses

def extract_prediction_data(fixture_id, base_url, headers, cache_dir):
    """从API获取预测数据"""
    cache_path = cache_dir / f'predictions_{fixture_id}.json'
    data = fetch_json(f'{base_url}/predictions', {'fixture': fixture_id}, headers, cache_path)
    
    if not data or not data.get('response'):
        return None
    
    pred = data['response'][0]
    result = {
        # Teams data
        '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,
        # Goals stats
        'home_goals_for_avg': None,
        'home_goals_against_avg': None,
        'away_goals_for_avg': None,
        'away_goals_against_avg': None,
        'home_under_25_rate': None,
        'home_over_25_rate': None,
        'away_under_25_rate': None,
        'away_over_25_rate': None,
        # H2H
        'h2h_home_wins': 0,
        'h2h_draws': 0,
        'h2h_away_wins': 0,
        # Comparison
        'comparison_form_home': None,
        'comparison_att_home': None,
        'comparison_def_home': None,
        'comparison_poisson_home': None,
        'comparison_h2h_home': None,
        'comparison_goals_home': None,
        'comparison_total_home': None,
    }
    
    # Parse teams data
    teams = pred.get('teams', {})
    home_team = teams.get('home', {})
    away_team = teams.get('away', {})
    
    # Last 5 form
    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'))
    
    # League stats (form string)
    home_league = home_team.get('league', {})
    away_league = away_team.get('league', {})
    
    w, d, l = parse_form_string(home_league.get('form'))
    result['home_last5_wins'] = w
    result['home_last5_draws'] = d
    result['home_last5_losses'] = l
    
    w, d, l = parse_form_string(away_league.get('form'))
    result['away_last5_wins'] = w
    result['away_last5_draws'] = d
    result['away_last5_losses'] = l
    
    # Goals for/against
    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['home_goals_against_avg'] = parse_percent(home_goals.get('average', {}).get('against', {}).get('total'))
    result['away_goals_for_avg'] = parse_percent(away_goals.get('average', {}).get('total'))
    result['away_goals_against_avg'] = parse_percent(away_goals.get('average', {}).get('against', {}).get('total'))
    
    # Under/over 2.5 from goals stats
    home_uo = home_goals.get('under_over', {})
    away_uo = away_goals.get('under_over', {})
    
    for key in ['2.5', '3.5']:
        if key in home_uo:
            over = home_uo[key].get('over', 0)
            under = home_uo[key].get('under', 0)
            total = over + under
            if total > 0:
                if key == '2.5':
                    result['home_over_25_rate'] = over / total
                    result['home_under_25_rate'] = under / total
        if key in away_uo:
            over = away_uo[key].get('over', 0)
            under = away_uo[key].get('under', 0)
            total = over + under
            if total > 0:
                if key == '2.5':
                    result['away_over_25_rate'] = over / total
                    result['away_under_25_rate'] = under / total
    
    # H2H
    h2h = pred.get('h2h', [])
    if h2h:
        h2h_home_wins = 0
        h2h_draws = 0
        h2h_away_wins = 0
        for match in h2h[:10]:  # 最近10场
            winner = match.get('teams', {}).get('home', {}).get('winner')
            if winner == True:
                h2h_home_wins += 1
            elif winner == False:
                h2h_away_wins += 1
            else:
                h2h_draws += 1
        result['h2h_home_wins'] = h2h_home_wins
        result['h2h_draws'] = h2h_draws
        result['h2h_away_wins'] = h2h_away_wins
    
    # Comparison
    comparison = pred.get('comparison', {})
    if comparison:
        for key in ['form', 'att', 'def', 'poisson', 'h2h', 'goals', 'total']:
            if key in comparison:
                val = comparison[key]
                home_val = parse_percent(val.get('home'))
                if home_val is not None:
                    result[f'comparison_{key}_home'] = home_val
    
    return result

def extract_injuries(fixture_id, base_url, headers, cache_dir):
    """获取伤病情况"""
    cache_path = cache_dir / f'injuries_{fixture_id}.json'
    data = fetch_json(f'{base_url}/injuries', {'fixture': fixture_id}, headers, cache_path)
    
    if not data or not data.get('response'):
        return 0, 0
    
    home_injuries = 0
    away_injuries = 0
    
    for item in data['response']:
        team = item.get('team', {})
        if team.get('home') == True:
            home_injuries += 1
        elif team.get('home') == False:
            away_injuries += 1
        else:
            # Try to match by team name
            home_injuries += 1
    
    return home_injuries, away_injuries

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

def choose_main_total(bookmakers):
    """选择主盘口"""
    from collections import defaultdict
    import re
    
    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
                side = match.group(1).lower()
                line = float(match.group(2))
                try:
                    odd = float(item.get('odd'))
                except:
                    continue
                if odd <= 1:
                    continue
                local[line][side] = 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 = extract_prob(over_odds)
        under_prob = extract_prob(under_odds)
        margin = (over_prob + under_prob) - 1.0
        candidates.append({
            'line': line, 'over_odds': over_odds, 'under_odds': under_odds,
            'over_prob': over_prob, 'under_prob': under_prob, 'margin': margin
        })
    
    if not candidates:
        return None
    
    # 选择margin最小且quotes最多的
    candidates.sort(key=lambda x: (x['margin'], -x.get('quotes', 0)))
    best = candidates[0]
    
    return {
        'ou_line': best['line'],
        'over_odds': best['over_odds'],
        'under_odds': best['under_odds'],
        'over_prob': best['over_prob'],
        'under_prob': best['under_prob'],
    }

# ==================== 特征定义 ====================

FEATURE_COLS = [
    # O/U Odds features
    'ou_line', 'over_odds', 'under_odds', 'over_implied_prob', 'under_implied_prob', 'odds_margin',
    
    # Team form features
    'home_form_pct', 'away_form_pct', 'home_att', 'away_att', 'home_def', 'away_def',
    
    # Recent WDL
    'home_last5_wins', 'home_last5_draws', 'home_last5_losses',
    'away_last5_wins', 'away_last5_draws', 'away_last5_losses',
    
    # Goals stats
    'home_goals_for_avg', 'home_goals_against_avg',
    'away_goals_for_avg', 'away_goals_against_avg',
    'home_over_25_rate', 'home_under_25_rate',
    'away_over_25_rate', 'away_under_25_rate',
    
    # H2H
    'h2h_home_wins', 'h2h_draws', 'h2h_away_wins',
    
    # Comparison from API
    'comparison_form_home', 'comparison_att_home', 'comparison_def_home',
    'comparison_poisson_home', 'comparison_h2h_home', 'comparison_goals_home', 'comparison_total_home',
    
    # Injuries
    'home_injuries', 'away_injuries',
    
    # Historical team stats
    '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):
    """构建所有特征"""
    
    # Historical features
    league_tot_avg = mean_or_default(league_hist['tot'], 2.6)
    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 features
    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 features (with defaults)
    f = {}
    if pred_info:
        f = {**pred_info}
    else:
        f = {k: None for k in [
            'home_form_pct', 'away_form_pct', 'home_att', 'away_att', 'home_def', 'away_def',
            'home_last5_wins', 'home_last5_draws', 'home_last5_losses',
            'away_last5_wins', 'away_last5_draws', 'away_last5_losses',
            'home_goals_for_avg', 'home_goals_against_avg',
            'away_goals_for_avg', 'away_goals_against_avg',
            'home_over_25_rate', 'home_under_25_rate',
            'away_over_25_rate', 'away_under_25_rate',
            'h2h_home_wins', 'h2h_draws', 'h2h_away_wins',
            'comparison_form_home', 'comparison_att_home', 'comparison_def_home',
            'comparison_poisson_home', 'comparison_h2h_home', 'comparison_goals_home', 'comparison_total_home',
        ]}
    
    # Injuries
    home_inj = injuries[0] if injuries else 0
    away_inj = injuries[1] if injuries else 0
    
    # Derived features
    form_diff = (f.get('home_form_pct') or 50) - (f.get('away_form_pct') or 50)
    att_diff = (f.get('home_att') or 50) - (f.get('away_att') or 50)
    def_diff = (f.get('home_def') or 50) - (f.get('away_def') or 50)
    
    total_h2h = (f.get('h2h_home_wins') or 0) + (f.get('h2h_draws') or 0) + (f.get('h2h_away_wins') or 0)
    h2h_home_win_rate = (f.get('h2h_home_wins') or 0) / total_h2h if total_h2h > 0 else 0.5
    
    return {
        # O/U
        '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,
        # Form
        'home_form_pct': f.get('home_form_pct'), 'away_form_pct': f.get('away_form_pct'),
        'home_att': f.get('home_att'), 'away_att': f.get('away_att'),
        'home_def': f.get('home_def'), 'away_def': f.get('away_def'),
        # WDL
        'home_last5_wins': f.get('home_last5_wins', 0), 'home_last5_draws': f.get('home_last5_draws', 0), 'home_last5_losses': f.get('home_last5_losses', 0),
        'away_last5_wins': f.get('away_last5_wins', 0), 'away_last5_draws': f.get('away_last5_draws', 0), 'away_last5_losses': f.get('away_last5_losses', 0),
        # Goals
        'home_goals_for_avg': f.get('home_goals_for_avg'), 'home_goals_against_avg': f.get('home_goals_against_avg'),
        'away_goals_for_avg': f.get('away_goals_for_avg'), 'away_goals_against_avg': f.get('away_goals_against_avg'),
        'home_over_25_rate': f.get('home_over_25_rate'), 'home_under_25_rate': f.get('home_under_25_rate'),
        'away_over_25_rate': f.get('away_over_25_rate'), 'away_under_25_rate': f.get('away_under_25_rate'),
        # H2H
        'h2h_home_wins': f.get('h2h_home_wins', 0), 'h2h_draws': f.get('h2h_draws', 0), 'h2h_away_wins': f.get('h2h_away_wins', 0),
        # Comparison
        'comparison_form_home': f.get('comparison_form_home'), 'comparison_att_home': f.get('comparison_att_home'),
        'comparison_def_home': f.get('comparison_def_home'), 'comparison_poisson_home': f.get('comparison_poisson_home'),
        'comparison_h2h_home': f.get('comparison_h2h_home'), 'comparison_goals_home': f.get('comparison_goals_home'),
        'comparison_total_home': f.get('comparison_total_home'),
        # Injuries
        'home_injuries': home_inj, 'away_injuries': away_inj,
        # Historical
        '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,
        # Derived
        '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
    
    base_url = os.environ.get('API_FOOTBALL_BASE_URL', '').rstrip('/')
    api_key = os.environ.get('API_FOOTBALL_KEY', '')
    headers = {'x-apisports-key': api_key}
    
    cache_dir = CACHE_DIR
    cache_dir.mkdir(parents=True, exist_ok=True)
    
    allowed_codes = load_serving_codes()
    
    print("="*70)
    print("全面特征大小球预测模型训练")
    print("="*70)
    
    # 收集数据
    print("\n[1/4] 收集数据...")
    
    # 获取近期的数据
    all_rows = []
    for i in range(45, 0, -1):
        date_str = (datetime.now() - timedelta(days=i)).strftime('%Y-%m-%d')
        
        # 获取fixture
        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
            
            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
            
            # 获取prediction数据（从缓存）
            pred_path = cache_dir / f'predictions_{fixture_id}.json'
            pred_info = None
            if pred_path.exists():
                try:
                    pred_data = json.loads(pred_path.read_text())
                    if pred_data and pred_data.get('response'):
                        # 已经提取过，但这里需要重新处理
                        pass
                except:
                    pass
            
            all_rows.append({
                'date': date_str,
                'fixture_id': fixture_id,
                'league_code': league_code,
                'league': LEAGUE_ZH_MAP.get(league_code, league_code),
                '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(all_rows) >= 500:
            break
        
        if i % 10 == 0:
            print(f"  已收集 {len(all_rows)} 场比赛...")
    
    print(f"总数据: {len(all_rows)} 场")
    
    # 提取额外特征（prediction和injuries）
    print("\n[2/4] 提取额外特征 (predictions & injuries)...")
    
    for row in all_rows[:100]:  # 先处理前100个
        fixture_id = row['fixture_id']
        
        # Predictions
        pred_path = cache_dir / f'predictions_{fixture_id}.json'
        pred_info = None
        if pred_path.exists():
            try:
                pred_data = json.loads(pred_path.read_text())
                if pred_data and pred_data.get('response'):
                    pred_info = extract_prediction_data(fixture_id, base_url, headers, cache_dir)
            except:
                pass
        
        # Injuries
        inj_path = cache_dir / f'injuries_{fixture_id}.json'
        injuries = (0, 0)
        if inj_path.exists():
            try:
                inj_data = json.loads(inj_path.read_text())
                if inj_data and inj_data.get('response'):
                    injuries = extract_injuries(fixture_id, base_url, headers, cache_dir)
            except:
                pass
        
        row['pred_info'] = pred_info
        row['injuries'] = injuries
    
    # 构建特征
    print("\n[3/4] 构建特征...")
    
    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': []})
    
    features_list = []
    labels_list = []
    
    all_rows.sort(key=lambda x: x['date'])
    
    for row in all_rows:
        league_code = row['league_code']
        home_key = (league_code, row['home'])
        away_key = (league_code, row['away'])
        hh = team_hist[home_key]
        aa = team_hist[away_key]
        ll = league_hist[league_code]
        
        ou_info = {'ou_line': row['ou_line'], 'over_odds': row['over_odds'], 'under_odds': row['under_odds']}
        pred_info = row.get('pred_info')
        injuries = row.get('injuries', (0, 0))
        
        features = build_features(hh, aa, ll, ou_info, pred_info, injuries)
        features_list.append(features)
        labels_list.append(row['is_over'])
        
        # Update history
        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 = pd.DataFrame(features_list)
    # 确保列顺序一致
    available_cols = [c for c in FEATURE_COLS if c in X.columns]
    X = X[available_cols]
    y = np.array(labels_list)
    
    print(f"特征数: {X.shape[1]}, 样本: {len(X)}")
    print(f"缺失值比例: {(X.isnull().sum().sum() / X.size):.1%}")
    
    # 训练
    print("\n[4/4] 训练模型...")
    
    model = Pipeline([
        ('imp', SimpleImputer(strategy='median')),
        ('clf', RandomForestClassifier(
            n_estimators=500, max_depth=12, min_samples_leaf=3,
            random_state=42, class_weight='balanced', n_jobs=-1
        ))
    ])
    model.fit(X, y)
    
    model_path = ARTIFACT_DIR / 'comprehensive_model.pkl'
    joblib.dump(model, model_path)
    print(f"模型已保存: {model_path}")
    
    # 特征重要性
    print("\n特征重要性 (Top 20):")
    importances = model.named_steps['clf'].feature_importances_
    indices = np.argsort(importances)[::-1][:20]
    for i, idx in enumerate(indices):
        print(f"  {i+1}. {available_cols[idx]}: {importances[idx]:.4f}")
    
    print("\n训练完成!")

if __name__ == '__main__':
    main()
