#!/usr/bin/env python3
"""
pro_v5 训练脚本
- 基于22个联赛的完整B365让球盘数据
- 特征：盘口概率 + 让球数 + 水钱分析 + 球队近期状态
- 模型：RF + GBT 集成
- 输出: artifacts/v4_models_serving_current/
"""
import os, sys, json, warnings, joblib
from pathlib import Path
from collections import defaultdict
import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.metrics import accuracy_score
from sklearn.calibration import CalibratedClassifierCV
warnings.filterwarnings('ignore')
np.random.seed(42)

REPO = Path('/root/.openclaw/workspace-flj_bot1/football-predictor-repro')
ODDS_DIR = REPO / 'artifacts' / 'odds_api_history'
OUT_DIR = REPO / 'artifacts' / 'v4_models_serving_current_v5'
OUT_DIR.mkdir(parents=True, exist_ok=True)

LEAGUE_NAMES = {
    'e0':'英超','e1':'英甲','e2':'英冠','e2n':'英乙','e3':'英格兰第五级',
    'd1':'德甲','d2':'德乙','f1':'法甲','f2':'法乙',
    'i1':'意甲','i2':'意乙','sp1':'西甲','sp2':'西乙',
    'tr1':'土超','p1':'葡超','b1':'比甲','g1':'希腊超',
    'n1':'荷兰甲','n2':'荷兰乙','sc1':'苏超','cz1':'捷甲',
    'a1':'澳超','r1':'俄超','br1':'巴甲','m1':'美职联','j1':'日职联',
}

# ═══════════════════════════════════════════════════════════════════
# 特征工程
# ═══════════════════════════════════════════════════════════════════

def build_team_form(hist_df):
    """从历史数据构建球队近期状态"""
    cache = defaultdict(lambda: {'gf':[],'ga':[],'win':[],'und':[],'draw':[],'mom':[],'rf':[]})
    for _, r in hist_df.iterrows():
        ht, at = str(r.get('HomeTeam','')).strip(), str(r.get('AwayTeam','')).strip()
        fthg, ftag = r.get('FTHG'), r.get('FTAG')
        if fthg is None or ftag is None: continue
        try:
            fthg, ftag = float(fthg), float(ftag)
        except: continue
        win_h = 1 if fthg > ftag else 0
        win_a = 1 if ftag > fthg else 0
        drw = 1 if fthg == ftag else 0
        for team, gf, ga, w in [(ht,fthg,ftag,win_h),(at,ftag,fthg,win_a)]:
            c = cache[team]
            c['gf'].append(float(gf)); c['ga'].append(float(ga))
            c['win'].append(float(w)); c['und'].append(1.0-float(w))
            c['draw'].append(float(drw)); c['mom'].append(float(w)); c['rf'].append(float(gf)-float(ga))
    return cache

def _mn(v, n=10):
    vals = v[-n:]; return sum(vals)/len(vals) if vals else 0.5

def _arr(v, n=10):
    return ([0.5]*n + list(v))[-n:]

def _std(v, n=10):
    vals = v[-n:]
    if len(vals) < 2: return 0.0
    m = sum(vals)/len(vals)
    return (sum((x-m)**2 for x in vals)/len(vals))**0.5

def _recent_form(v, n=10):
    vals = v[-n:]
    if len(vals) < 3: return 0.0
    return sum(vals[-3:])/3 - sum(vals)/len(vals)

def build_features(ahh, b365hh, b365ha, home, away, team_cache):
    c = team_cache.get(home, {'win':[],'und':[],'draw':[],'mom':[],'rf':[],'gf':[],'ga':[]})
    ca = team_cache.get(away, {'win':[],'und':[],'draw':[],'mom':[],'rf':[],'gf':[],'ga':[]})

    # 盘口概率
    try:
        p_home = 1.0 / float(b365hh) if float(b365hh) > 1.0 else 0.5
        p_away = 1.0 / float(b365ha) if float(b365ha) > 1.0 else 0.5
    except:
        p_home = p_away = 0.5

    prob_sum = p_home + p_away
    prob_diff = p_home - p_away
    water_home = float(b365hh) - (prob_sum / p_home) if p_home > 0 else 0
    water_away = float(b365ha) - (prob_sum / p_away) if p_away > 0 else 0

    # 球队状态
    h_wr10 = _mn(c['win'], 10); a_wr10 = _mn(ca['win'], 10)
    h_wr3 = _mn(c['win'], 3); a_wr3 = _mn(ca['win'], 3)
    h_mom = _mn(c['mom'], 5); a_mom = _mn(ca['mom'], 5)
    h_recent = _recent_form(c['win'], 10); a_recent = _recent_form(ca['win'], 10)
    h_var = _std(c['win'], 10); a_var = _std(ca['win'], 10)
    h_gd10 = _mn(c['rf'], 10); a_gd10 = _mn(ca['rf'], 10)
    h_gf10 = _mn(c['gf'], 10); a_gf10 = _mn(ca['gf'], 10)
    h_ga10 = _mn(c['ga'], 10); a_ga10 = _mn(ca['ga'], 10)
    rec = _arr(c['win'], 10) + _arr(ca['win'], 10)

    return [
        float(ahh),
        p_home, p_away,
        prob_diff,
        water_home, water_away,
        float(ahh) * prob_diff,
        h_wr10 - a_wr10,
        h_wr3 - a_wr3,
        h_mom - a_mom,
        h_recent - a_recent,
        h_var, a_var,
        h_gd10, a_gd10,
        h_gf10 - a_gf10,
        h_ga10 - a_ga10,
    ] + rec


def train_league(code):
    """训练单个联赛的 pro_v5 模型"""
    # 读取数据
    full_file = ODDS_DIR / f'{code}_full_odds.csv'
    if not full_file.exists():
        return None, f'无文件'

    df = pd.read_csv(full_file, low_memory=False)
    df = df[df['PinnAHH'].notna() & (df['PinnAHH'] > 1.0)]
    df = df[df['PinnAHA'].notna() & (df['PinnAHA'] > 1.0)]
    df = df[df['FTHG'].notna() & df['FTAG'].notna()]
    df = df[df['PinnAHh'].notna()]

    if len(df) < 80:
        return None, f'数据太少({len(df)})'

    # 标签：让球后主队赢=1（胜）
    df['label'] = (df['FTHG'] + df['PinnAHh'] > df['FTAG']).astype(int)

    # 构建球队状态缓存
    hist_files = list(ODDS_DIR.glob(f'{code}_S*_fixtures.csv'))
    all_hist = pd.concat([pd.read_csv(f, usecols=['HomeTeam','AwayTeam','FTHG','FTAG'], low_memory=False) for f in hist_files], ignore_index=True)
    team_cache = build_team_form(all_hist)

    # 构建特征
    X_rows, y_rows = [], []
    for _, r in df.iterrows():
        try:
            feat = build_features(float(r['PinnAHh']), float(r['PinnAHH']), float(r['PinnAHA']),
                                 str(r['HomeTeam']), str(r['AwayTeam']), team_cache)
            X_rows.append(feat)
            y_rows.append(int(r['label']))
        except:
            continue

    X = np.array(X_rows, dtype=float)
    y = np.array(y_rows)
    n = len(X)
    if n < 80:
        return None, f'特征构建后数据不足({n})'

    split = max(50, int(n * 0.8))
    X_tr, X_te = X[:split], X[split:]
    y_tr, y_te = y[:split], y[split:]

    # 训练多个模型选最佳
    best_acc, best_model = 0, None
    for depth in [3, 5, 7, 9]:
        for est in [100, 200]:
            for min_s in [5, 10, 20]:
                m = RandomForestClassifier(n_estimators=est, max_depth=depth, min_samples_leaf=min_s, random_state=42, n_jobs=-1)
                m.fit(X_tr, y_tr)
                acc = accuracy_score(y_te, m.predict(X_te))
                if acc > best_acc:
                    best_acc, best_model = acc, m

    # 校准
    try:
        cal = CalibratedClassifierCV(best_model, cv=3, method='isotonic')
        cal.fit(X_tr, y_tr)
        cal_acc = accuracy_score(y_te, cal.predict(X_te))
    except:
        cal = best_model
        cal_acc = best_acc

    # 保存
    out_file = OUT_DIR / f'{code}_pro_v5_model.pkl'
    scaler = StandardScaler()
    scaler.fit(X_tr)
    joblib.dump({'model': cal, 'scaler': scaler, 'acc': cal_acc, 'n_samples': n, 'league': code}, out_file)

    return cal_acc, n


# ═══════════════════════════════════════════════════════════════════
# 主流程
# ═══════════════════════════════════════════════════════════════════

print("=" * 60)
print("pro_v5 训练开始")
print("=" * 60)

results = {}
for code in sorted(LEAGUE_NAMES.keys()):
    name = LEAGUE_NAMES[code]
    print(f"\n{'='*50}")
    print(f"训练 {code} ({name})")
    print(f"{'='*50}")
    try:
        acc, n = train_league(code)
        if acc is not None:
            results[code] = {'acc': acc, 'n': n, 'status': '✅成功'}
            print(f"  → acc={acc:.3f}, n={n}")
        else:
            results[code] = {'status': f'⚠️{n}'}
            print(f"  → {n}")
    except Exception as e:
        results[code] = {'status': f'❌{e}'}
        print(f"  → ❌ {e}")

# 汇总
print("\n" + "=" * 60)
print("pro_v5 训练结果汇总")
print("=" * 60)
print(f"{'联赛':<8} {'准确率':<10} {'样本数':<8} {'状态'}")
for code, r in sorted(results.items()):
    acc_str = f"{r['acc']:.1%}" if 'acc' in r else '-'
    n_str = str(r.get('n', '-'))
    print(f"{code:<8} {acc_str:<10} {n_str:<8} {r['status']}")

# 保存汇总
summary = {code: {'league_name': LEAGUE_NAMES.get(code, code), **r}
            for code, r in results.items()}
with open(OUT_DIR / 'pro_v5_summary.json', 'w') as f:
    json.dump(summary, f, indent=2)

print(f"\n模型保存目录: {OUT_DIR}")
