#!/usr/bin/env python3
"""
训练动态盘口大小球模型
- 从API获取历史动态盘口数据
- 结合球队历史统计
- 预测主盘口（大/小）
"""

import os
import re
import json
import time
import math
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_dynamic_ou'
ARTIFACT_DIR.mkdir(parents=True, exist_ok=True)

BEIJING_TZ = timezone(timedelta(hours=8))

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

def extract_ou_values(odds_data):
    """从赔率数据中提取O/U值和赔率"""
    ou_values = []
    for item in odds_data:
        for bm in item.get('bookmakers', []):
            for bet in bm.get('bets', []):
                if 'Goals Over/Under' in bet.get('name', '') and 'Half' not in bet.get('name', ''):
                    for v in bet.get('values', []):
                        val = v.get('value', '')
                        match = re.match(r'^(Over|Under)\s+([0-9.]+)$', val, re.I)
                        if match:
                            direction = match.group(1).lower()
                            line = float(match.group(2))
                            odds = float(v.get('odd', 0))
                            if odds > 1:
                                ou_values.append({
                                    'bookmaker': bm.get('name'),
                                    'line': line,
                                    'direction': direction,
                                    'odds': odds
                                })
    return ou_values

def choose_main_total(odds_data):
    """选择主盘口：赔率最接近50%的那个"""
    ou_values = extract_ou_values(odds_data)
    if not ou_values:
        return None
    
    # 按盘口分组，计算每个盘口的平均赔率
    line_odds = defaultdict(list)
    for v in ou_values:
        line_odds[v['line']].append(v['odds'])
    
    # 找主盘口：over和under赔率都最接近2.0的那个
    best_line = None
    best_diff = float('inf')
    
    for line, odds_list in line_odds.items():
        if len(odds_list) >= 2:
            avg_odds = sum(odds_list) / len(odds_list)
            diff = abs(avg_odds - 2.0)
            if diff < best_diff:
                best_diff = diff
                best_line = line
    
    if best_line is None:
        return None
    
    # 获取该盘口的over/under赔率
    over_odds = None
    under_odds = None
    for v in ou_values:
        if v['line'] == best_line:
            if v['direction'] == 'over':
                over_odds = v['odds']
            else:
                under_odds = v['odds']
    
    if over_odds is None or under_odds is None:
        return None
    
    return {
        'ou_line': best_line,
        'over_odds': over_odds,
        'under_odds': under_odds,
        'fair_over': 1 / over_odds if over_odds > 1 else None,
        'fair_under': 1 / under_odds if under_odds > 1 else None,
    }

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

# 特征列表
FEATURE_COLS = [
    'ou_line', 'over_odds', 'under_odds', 'fair_over', 'fair_under',
    'over_implied_prob', 'under_implied_prob', 'odds_margin',
    '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',
    'expected_goals_proxy', 'combined_over_rate',
    'league_avg_tot', 'league_avg_over_rate',
    'line_minus_expected', 'line_minus_league_avg',
]

def build_features(home_hist, away_hist, league_hist, ou_info):
    league_tot_avg = mean_or_default(league_hist['tot'], 2.6)
    league_over_rate = mean_or_default(league_hist['over'], 0.5)
    
    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, away_gf_avg, home_ga_avg])
    combined_over_rate = np.nanmean([home_over_rate, away_over_rate, home_over_last5, away_over_last5])
    
    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
    fair_over = ou_info.get('fair_over', 0.5) if ou_info else 0.5
    fair_under = ou_info.get('fair_under', 0.5) if ou_info else 0.5
    
    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 + under_implied) else 0
    
    return {
        'ou_line': ou_line,
        'over_odds': over_odds,
        'under_odds': under_odds,
        'fair_over': fair_over,
        'fair_under': fair_under,
        'over_implied_prob': over_implied,
        'under_implied_prob': under_implied,
        'odds_margin': odds_margin,
        '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,
        'league_avg_tot': league_tot_avg,
        'league_avg_over_rate': league_over_rate,
        'line_minus_expected': ou_line - expected_goals_proxy,
        'line_minus_league_avg': ou_line - league_tot_avg,
    }

def is_over_result(total_goals, ou_line):
    """判断是否大于盘口"""
    return 1 if total_goals > ou_line else 0

# ==================== 训练流程 ====================

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

if not base_url or not api_key:
    print("缺少 API 配置")
    exit(1)

print("="*70)
print("动态盘口大小球模型训练")
print("="*70)

# 收集历史数据（从缓存和API）
from goal_number_api_dynamic import (
    APIFootballClient, LEAGUE_CODE_MAP, LEAGUE_ZH_MAP, 
    load_serving_codes, SUPPORTED_FINAL_STATUSES
)

client = APIFootballClient(base_url, api_key, cache_dir=CACHE_DIR)
allowed_codes = load_serving_codes()

# 从缓存获取历史数据
print("\n[1/4] 从缓存收集历史数据...")
all_rows = []

# 需要更早的日期来建立历史
hist_dates = []
for i in range(60, 0, -1):
    date = (datetime.now() - timedelta(days=i)).strftime('%Y-%m-%d')
    hist_dates.append(date)

for date_str in hist_dates[:30]:  # 先用30天
    fixtures = client.fixtures_by_date(date_str)
    odds_rows = client.odds_by_date(date_str)
    
    fixture_map = {int(x['fixture']['id']): x for x in fixtures}
    odds_map = {int(item['fixture']['id']): item for item in odds_rows}
    
    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_total = choose_main_total(odds_item.get('bookmakers', []))
        if not chosen_total:
            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_total['ou_line']
        is_over = is_over_result(total, ou_line)
        
        all_rows.append({
            'date': date_str,
            'league_code': league_code,
            'league': LEAGUE_ZH_MAP.get(league_code, league_code),
            'home_team': fx['teams']['home']['name'],
            'away_team': fx['teams']['away']['name'],
            'ou_line': ou_line,
            'over_odds': chosen_total['over_odds'],
            'under_odds': chosen_total['under_odds'],
            'home_goals': home_goals,
            'away_goals': away_goals,
            'is_over': is_over,
        })

print(f"收集到 {len(all_rows)} 条记录")

# 构建特征
print("\n[2/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_team'])
    away_key = (league_code, row['away_team'])
    
    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'],
        'fair_over': extract_prob(row['over_odds']),
        'fair_under': extract_prob(row['under_odds']),
    }
    
    features = build_features(hh, aa, ll, ou_info)
    features_list.append(features)
    labels_list.append(row['is_over'])
    
    # 更新历史
    total = row['home_goals'] + row['away_goals']
    is_over = row['is_over']
    ou_line = row['ou_line']
    
    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(features_list)
y_train = np.array(labels_list)

print(f"训练数据: {len(X_train)} 样本, {X_train.shape[1]} 特征")

# 训练模型
print("\n[3/4] 训练模型...")

model = Pipeline([
    ('imp', SimpleImputer(strategy='median')),
    ('clf', RandomForestClassifier(
        n_estimators=500,
        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 / 'dynamic_ou_model.pkl'
joblib.dump(model, model_path)
print(f"模型已保存: {model_path}")

# 测试阈值
print("\n[4/4] 回测上周数据...")

# 用最近的数据做测试
test_dates = []
for i in range(14, 7, -1):
    date = (datetime.now() - timedelta(days=i)).strftime('%Y-%m-%d')
    test_dates.append(date)

test_rows = []
for date_str in test_dates:
    fixtures = client.fixtures_by_date(date_str)
    odds_rows = client.odds_by_date(date_str)
    
    fixture_map = {int(x['fixture']['id']): x for x in fixtures}
    odds_map = {int(item['fixture']['id']): item for item in odds_rows}
    
    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_total = choose_main_total(odds_item.get('bookmakers', []))
        if not chosen_total:
            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_total['ou_line']
        is_over = is_over_result(total, ou_line)
        
        test_rows.append({
            'date': date_str,
            'league_code': league_code,
            'league': LEAGUE_ZH_MAP.get(league_code, league_code),
            'home_team': fx['teams']['home']['name'],
            'away_team': fx['teams']['away']['name'],
            'ou_line': ou_line,
            'over_odds': chosen_total['over_odds'],
            'under_odds': chosen_total['under_odds'],
            'home_goals': home_goals,
            'away_goals': away_goals,
            'is_over': is_over,
        })

print(f"测试数据: {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': []})

test_features = []
test_labels = []

for row in test_rows:
    league_code = row['league_code']
    home_key = (league_code, row['home_team'])
    away_key = (league_code, row['away_team'])
    
    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'],
        'fair_over': extract_prob(row['over_odds']),
        'fair_under': extract_prob(row['under_odds']),
    }
    
    features = build_features(hh, aa, ll, ou_info)
    test_features.append(features)
    test_labels.append(row['is_over'])
    
    # 更新历史
    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)[FEATURE_COLS]
y_test = np.array(test_labels)

# 预测
probs = model.predict_proba(X_test)[:, 1]

# 评估不同阈值
print("\n" + "="*70)
print("回测结果")
print("="*70)

for threshold in [0.50, 0.52, 0.55, 0.58, 0.60, 0.65]:
    preds = (probs >= threshold).astype(int)
    mask = (probs >= threshold) | (probs <= (1 - threshold))
    
    certain_mask = mask
    certain_preds = preds[certain_mask]
    certain_labels = y_test[certain_mask]
    
    if len(certain_preds) > 0:
        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
        
        print(f"\n阈值={threshold}:")
        print(f"  预测数: {len(certain_preds)} (大球{over_mask.sum()}, 小球{under_mask.sum()})")
        print(f"  准确率: {acc:.1%} (大球{over_acc:.1%}, 小球{under_acc:.1%})")

baseline = y_test.mean()
print(f"\n基准大球率: {baseline:.1%}")
print("="*70)
