#!/usr/bin/env python3
"""
快速retrain：ah_1year_v3
- 只用本地已有赔率数据（odds_api_history/）
- 不调任何API
- 输出到 models/ah_1year_v3/
"""
import 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, ExtraTreesClassifier, GradientBoostingClassifier
from sklearn.linear_model import LogisticRegression, RidgeClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.calibration import CalibratedClassifierCV
from sklearn.metrics import accuracy_score
warnings.filterwarnings('ignore')
np.random.seed(42)

REPO = Path('/root/.openclaw/workspace-flj_bot1/football-predictor-repro')
ODDS_DIR = REPO / 'artifacts' / 'odds_api_history'
HIST_DIR = REPO / 'historical_data'
OUT_DIR = REPO / 'models' / 'ah_1year_v3'

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

ODDS_COLS = ['B365AHH','B365AHA','AvgAHH','AvgAHA','PAHH','PAHA','AHh']

def _build_team_hist():
    return {'gf':[],'ga':[],'win':[],'und':[],'tot':[],'hgf':[],'hga':[],'hgc':[],
            'agf':[],'aga':[],'agc':[],'mom':[],'rf':[]}

def _update_cache(cache, home, away, fthg, ftag):
    ht, at = home.strip(), away.strip()
    for team, gf, ga, win_ in [(ht,fthg,ftag,1 if fthg>ftag else 0),(at,ftag,fthg,1 if ftag>fthg else 0)]:
        if team not in cache: cache[team] = _build_team_hist()
        tg = cache[team]
        tg['gf'].append(float(gf)); tg['ga'].append(float(ga))
        tg['win'].append(float(win_)); tg['und'].append(1.0-float(win_))
        tg['tot'].append(float(gf)+float(ga))
        tg['hgf'].append(float(gf)); tg['hga'].append(float(ga)); tg['hgc'].append(float(win_))
        tg['agf'].append(float(ga)); tg['aga'].append(float(fthg)); tg['agc'].append(1.0-float(win_))
        tg['mom'].append(float(win_)); tg['rf'].append(float(gf)-float(ga))

def _load_team_hist():
    cache = defaultdict(_build_team_hist)
    for hf in sorted(HIST_DIR.glob('*_history.csv')):
        try:
            df = pd.read_csv(hf, usecols=['HomeTeam','AwayTeam','FTHG','FTAG'], low_memory=False)
            df = df[df['FTHG'].notna() & df['FTAG'].notna()]
            for _, r in df.iterrows():
                _update_cache(cache, str(r['HomeTeam']), str(r['AwayTeam']), float(r['FTHG']), float(r['FTAG']))
        except: pass
    for mf in sorted(ODDS_DIR.glob('*_merged*.csv')):
        try:
            mdf = pd.read_csv(mf, usecols=['HomeTeam','AwayTeam','FTHG','FTAG'], low_memory=False)
            mdf = mdf[mdf['FTHG'].notna() & mdf['FTAG'].notna()]
            for _, r in mdf.iterrows():
                _update_cache(cache, str(r['HomeTeam']), str(r['AwayTeam']), float(r['FTHG']), float(r['FTAG']))
        except: pass
    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 _stdv(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 _build_feat(handicap, ahh, aha, home, away, cache):
    c = cache.get(home.strip(), _build_team_hist())
    ca = cache.get(away.strip(), _build_team_hist())
    hg = _mn(c['gf'],10); ag = _mn(ca['gf'],10)
    hga_ = _mn(c['ga'],10); aga_ = _mn(ca['ga'],10)
    hw = _mn(c['win'],10); aw = _mn(ca['win'],10)
    hd = _mn(c['draw'],10) if 'draw' in c else 0.0
    hv = _stdv(c['win'],10)
    rec = _arr(c['win'],10) + _arr(ca['win'],10)
    recent3_diff = _mn(c['win'],3)-_mn(c['win'],10)
    arecent3_diff = _mn(ca['win'],3)-_mn(ca['win'],10)
    return [handicap, ahh, aha,
            float(handicap)*(hw-aw), hw-aw, hv, hd, hw, aw,
            recent3_diff, arecent3_diff] + rec

# ─── 读取赔率数据 ────────────────────────────────────────────────
ODDS_FILES = sorted(ODDS_DIR.glob('*_odds_api_merged.csv')) + sorted(ODDS_DIR.glob('*_merged_v2.csv'))
print(f"找到 {len(ODDS_FILES)} 个赔率文件")

results = []
for odds_f in ODDS_FILES:
    code = odds_f.stem.replace('_odds_api_merged','').replace('_merged_v2','')
    name = LEAGUE_NAMES.get(code, code)
    print(f"\n{'='*50}")
    print(f"处理 {code} ({name})")
    print(f"{'='*50}")

    dfs = []
    for pattern in [f'{code}_odds_api_merged.csv', f'{code}_merged_v2.csv']:
        f = ODDS_DIR / pattern
        if f.exists():
            try:
                df = pd.read_csv(f, low_memory=False)
                if 'B365AHH' in df.columns:
                    dfs.append(df)
            except: pass

    if not dfs:
        print(f"  跳过：无有效赔率数据")
        results.append((code, name, '❌ 无数据'))
        continue

    data = pd.concat(dfs, ignore_index=True).drop_duplicates(subset=['Date','HomeTeam','AwayTeam'])
    data = data[data['B365AHH'].notna() & (data['B365AHH'] > 1.0)]
    data = data[data['B365AHA'].notna() & (data['B365AHA'] > 1.0)]
    data = data[data['FTHG'].notna() & data['FTAG'].notna()]
    data = data[data['AHh'].notna()]
    print(f"  有效数据: {len(data)} 条")

    if len(data) < 30:
        print(f"  跳过：数据太少 ({len(data)} 条)")
        results.append((code, name, f'⚠️ 数据太少({len(data)})'))
        continue

    data['label'] = (data['FTHG'] + data['AHh'] > data['FTAG']).astype(int)

    team_hist = _load_team_hist()
    X_rows, y_rows = [], []
    for _, r in data.iterrows():
        try:
            feat = _build_feat(float(r['AHh']), float(r['B365AHH']), float(r['B365AHA']),
                              str(r['HomeTeam']), str(r['AwayTeam']), team_hist)
            X_rows.append(feat); y_rows.append(int(r['label']))
        except: continue

    X = np.array(X_rows, dtype=float)
    y = np.array(y_rows)
    print(f"  特征: {X.shape}, 正例比例: {y.mean():.3f}")

    n = len(X)
    split = max(30, int(n * 0.75))
    X_tr, X_te = X[:split], X[split:]
    y_tr, y_te = y[:split], y[split:]

    best_acc, best_model, best_name = 0, None, ''
    for model_cls, name_m in [
        (lambda: RandomForestClassifier(n_estimators=200, max_depth=5, min_samples_leaf=10, random_state=42, n_jobs=-1), 'RF'),
        (lambda: ExtraTreesClassifier(n_estimators=200, max_depth=5, min_samples_leaf=10, random_state=42, n_jobs=-1), 'ET'),
        (lambda: LogisticRegression(C=0.1, max_iter=1000, random_state=42), 'LR'),
    ]:
        for _ in range(3):
            m = model_cls()
            m.fit(X_tr, y_tr)
            acc = accuracy_score(y_te, m.predict(X_te))
            if acc > best_acc:
                best_acc, best_model, best_name = acc, m, name_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

    print(f"  最佳: {best_name} acc={best_acc:.3f} 校准后={cal_acc:.3f}")

    out_dir = OUT_DIR / code
    out_dir.mkdir(parents=True, exist_ok=True)
    cfg = {'league': code, 'league_name': name, 'accuracy': float(cal_acc),
           'n_samples': int(n), 'threshold': 0.5, 'direction': 'over',
           'model_type': f'{best_name}_calibrated'}
    with open(out_dir / 'optimized_config.json', 'w') as f:
        json.dump(cfg, f, indent=2)
    joblib.dump(cal, out_dir / 'optimized_pipeline.pkl')
    print(f"  ✅ 保存到 {out_dir}")
    results.append((code, name, f'✅ acc={cal_acc:.1%}'))

print("\n" + "="*60)
print("结果汇总:")
for code, name, status in results:
    print(f"  {code} ({name}): {status}")
