#!/usr/bin/env python3
"""
ah_1year_v6: per-league AutoML 独立最优
- 每个联赛独立搜索：最优特征组 × 最优模型
- 特征分组：odds基础、hist历史、goals进球、adj调整、interaction交互、recent近期、h2h交手
- 模型候选：XGBoost(多种深度)、RF、GBT、HistGB
- 输出: models/ah_1year_v6/
"""
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, HistGradientBoostingClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import accuracy_score
from sklearn.calibration import CalibratedClassifierCV
warnings.filterwarnings('ignore')
np.random.seed(42)

try:
    from xgboost import XGBClassifier
    USE_XGB = True
except ImportError:
    USE_XGB = False

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

LEAGUE_NAMES = {
    'e0':'英超','e1':'英甲','e2':'英冠','e2n':'英乙',
    'd1':'德甲','d2':'德乙','f1':'法甲','f2':'法乙',
    'i1':'意甲','i2':'意乙','sp1':'西甲','sp2':'西乙',
    'tr1':'土超','p1':'葡超','g1':'希腊超',
    'n1':'荷兰甲','j1':'日职联','m1':'美职联','br1':'巴甲',
}

# ═══════════════════════════════════════════════════════════════════
# 特征构建（分模块，可按需启用）
# ═══════════════════════════════════════════════════════════════════

def build_team_hist(hist_df):
    """球队历史统计（含主客场）"""
    cache = defaultdict(lambda: {
        'hw':[],'aw':[],'dr':[],'gf':[],'ga':[],'tot':[],
        'home_hw':[],'home_gf':[],'home_ga':[],
        'away_aw':[],'away_gf':[],'away_ga':[],
    })
    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
        drw = 1.0 if fthg == ftag else 0.0
        win_h = 1.0 if fthg > ftag else 0.0
        # 主队
        c = cache[ht]
        c['hw'].append(win_h); c['gf'].append(fthg); c['ga'].append(ftag)
        c['tot'].append(fthg+ftag); c['dr'].append(drw)
        c['home_hw'].append(win_h); c['home_gf'].append(fthg); c['home_ga'].append(ftag)
        # 客队
        ca = cache[at]
        ca['aw'].append(1.0-win_h); ca['gf'].append(ftag); ca['ga'].append(fthg)
        ca['tot'].append(fthg+ftag); ca['dr'].append(drw)
        ca['away_aw'].append(1.0-win_h); ca['away_gf'].append(ftag); ca['away_ga'].append(fthg)
    return cache

def build_h2h_cache(hist_df):
    """H2H历史交手"""
    h2h = defaultdict(list)
    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
        h2h[(ht,at)].append({'win_h': 1 if fthg > ftag else 0, 'gf': fthg, 'ga': ftag})
    return h2h

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

def _arr10(v):
    return ([0.5]*10 + list(v))[-10:]

def _h2h_wr(h2h, home, away, n=5):
    matches = h2h.get((home, away), [])
    if not matches: return 0.5
    recent = matches[-n:]
    return sum(m['win_h'] for m in recent) / len(recent)

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

def build_full_features(ahh, b365hh, b365ha, home, away, cache, h2h):
    """
    全量特征（分7组），后续按组选择
    """
    c = cache.get(home, {})
    ca = cache.get(away, {})

    try:
        total_odds = 1/float(b365hh) + 1/float(b365ha)
        norm_home = (1/float(b365hh)) / total_odds if total_odds > 0 else 0.5
        norm_away = (1/float(b365ha)) / total_odds if total_odds > 0 else 0.5
        odds_gap = abs(norm_home - norm_away)
    except:
        norm_home = norm_away = odds_gap = 0.5

    try:
        ahh_f = float(ahh)
    except:
        ahh_f = 0.0

    # ── 特征组0: odds基础 ──
    odds_feat = [
        ahh_f,
        norm_home, norm_away,
        odds_gap,
        norm_home - norm_away,
    ]

    # ── 特征组1: 历史胜率（主客场） ──
    h_hw20 = _m(c.get('hw',[]), 20); a_hw20 = _m(ca.get('hw',[]), 20)
    h_aw20 = _m(c.get('aw',[]), 20); a_aw20 = _m(ca.get('aw',[]), 20)
    h_dr20 = _m(c.get('dr',[]), 20); a_dr20 = _m(ca.get('dr',[]), 20)
    hist_feat = [
        h_hw20, a_aw20,
        h_aw20, a_hw20,
        h_dr20 - a_dr20,
        (h_hw20 - a_hw20) + (h_aw20 - a_aw20),  # 综合主客队实力差
    ]

    # ── 特征组2: 进球期望 ──
    h_gf10 = _m(c.get('gf',[]), 10); a_gf10 = _m(ca.get('gf',[]), 10)
    h_ga10 = _m(c.get('ga',[]), 10); a_ga10 = _m(ca.get('ga',[]), 10)
    gf_diff = h_gf10 - a_ga10
    tot_avg = (_m(c.get('tot',[]),10) + _m(ca.get('tot',[]),10)) / 2
    goals_feat = [
        gf_diff,
        tot_avg,
        h_gf10 - a_gf10,
        _std(c.get('gf',[]), 10),
        _std(ca.get('gf',[]), 10),
    ]

    # ── 特征组3: 让球调整 ──
    adj_home_prob = max(0.1, min(0.9, norm_home + ahh_f * 0.1))
    adj_feat = [
        adj_home_prob,
        odds_gap * abs(ahh_f),
    ]

    # ── 特征组4: 交互特征 ──
    interact_feat = [
        (h_hw20 - a_hw20) * ahh_f,
        (h_dr20 + a_dr20) / 2,
        odds_gap * (h_hw20 - a_hw20),
        norm_home * (h_hw20 - 0.5),
        norm_away * (a_aw20 - 0.5),
    ]

    # ── 特征组5: 近期形式（近5场） ──
    h_hw5 = _m(c.get('hw',[]), 5); a_aw5 = _m(ca.get('aw',[]), 5)
    h_gf5 = _m(c.get('gf',[]), 5); a_ga5 = _m(ca.get('ga',[]), 5)
    recent_feat = [
        h_hw5, a_aw5,
        h_gf5 - a_ga5,
        h_hw5 - h_hw20,  # 近期vs长期趋势
        a_aw5 - a_aw20,
    ]

    # ── 特征组6: H2H ──
    h2h_feat = [
        _h2h_wr(h2h, home, away, 5),
        _h2h_wr(h2h, home, away, 10),
        _h2h_wr(h2h, home, away, 5) * ahh_f,
    ]

    # ── 特征组7: 主客场特定 ──
    h_home_hw5 = _m(c.get('home_hw',[]), 5)
    a_away_aw5 = _m(ca.get('away_aw',[]), 5)
    home_adv_feat = [
        h_home_hw5 - a_away_aw5,
        _m(c.get('home_gf',[]), 5) - _m(ca.get('away_ga',[]), 5),
    ]

    return {
        'odds': odds_feat,
        'hist': hist_feat,
        'goals': goals_feat,
        'adj': adj_feat,
        'interact': interact_feat,
        'recent': recent_feat,
        'h2h': h2h_feat,
        'home_adv': home_adv_feat,
    }

def concat_features(feat_dict, groups):
    result = []
    for g in groups:
        result.extend(feat_dict.get(g, []))
    return result

# ═══════════════════════════════════════════════════════════════════
# 特征组配置 & 模型配置
# ═══════════════════════════════════════════════════════════════════

FEATURE_CONFIGS = [
    # 最保守：仅odds基础
    ['odds'],
    # 基础+历史
    ['odds', 'hist'],
    # 基础+历史+进球
    ['odds', 'hist', 'goals'],
    # 基础+历史+近期
    ['odds', 'hist', 'recent'],
    # 基础+历史+近期+H2H
    ['odds', 'hist', 'recent', 'h2h'],
    # 基础+历史+近期+H2H+主客场
    ['odds', 'hist', 'recent', 'h2h', 'home_adv'],
    # 全量（但不加interaction，避免噪声）
    ['odds', 'hist', 'goals', 'adj', 'recent', 'h2h', 'home_adv'],
    # 全量含interaction
    ['odds', 'hist', 'goals', 'adj', 'interact', 'recent', 'h2h', 'home_adv'],
]

MODEL_CONFIGS = []
for depth in [3, 5, 7]:
    MODEL_CONFIGS.append(
        (f'XGB_d{depth}', lambda d=depth: XGBClassifier(
            n_estimators=200, max_depth=d, learning_rate=0.05,
            subsample=0.8, colsample_bytree=0.8,
            min_child_weight=3, random_state=42, n_jobs=-1,
            eval_metric='logloss', use_label_encoder=False))
    )

# 保守XGB（防过拟合）
MODEL_CONFIGS.append(
    ('XGB_cons', lambda: XGBClassifier(
        n_estimators=150, max_depth=3, learning_rate=0.03,
        subsample=0.7, colsample_bytree=0.7,
        min_child_weight=5, reg_alpha=0.2, reg_lambda=2.0,
        random_state=42, n_jobs=-1,
        eval_metric='logloss', use_label_encoder=False))
)

MODEL_CONFIGS += [
    ('HistGB', lambda: HistGradientBoostingClassifier(max_iter=200, max_depth=5, learning_rate=0.05, random_state=42)),
    ('RF_d6', lambda: RandomForestClassifier(n_estimators=200, max_depth=6, min_samples_leaf=8, random_state=42, n_jobs=-1)),
    ('RF_d4', lambda: RandomForestClassifier(n_estimators=150, max_depth=4, min_samples_leaf=10, random_state=42, n_jobs=-1)),
    ('GBT_d4', lambda: GradientBoostingClassifier(n_estimators=150, max_depth=4, learning_rate=0.05, random_state=42)),
]

# ═══════════════════════════════════════════════════════════════════
# 训练单个联赛
# ═══════════════════════════════════════════════════════════════════

def train_league(code, all_features_list, labels):
    n = len(labels)
    if n < 60:
        return None, None, None, f'数据太少({n})'

    split = max(30, int(n * 0.8))
    indices = np.random.permutation(n)
    train_idx, test_idx = indices[:split], indices[split:]
    y = np.array(labels)
    y_tr, y_te = y[train_idx], y[test_idx]

    best_acc = 0
    best_config = None
    best_model = None
    results_log = []

    for fidx, feat_groups in enumerate(FEATURE_CONFIGS):
        X_tr_f = np.array([concat_features(all_features_list[i], feat_groups) for i in train_idx], dtype=float)
        X_te_f = np.array([concat_features(all_features_list[i], feat_groups) for i in test_idx], dtype=float)
        if X_tr_f.shape[1] == 0:
            continue

        for mname, model_fn in MODEL_CONFIGS:
            try:
                model = model_fn()
                model.fit(X_tr_f, y_tr)
                acc = accuracy_score(y_te, model.predict(X_te_f))

                try:
                    cal = CalibratedClassifierCV(model, cv=3, method='isotonic')
                    cal.fit(X_tr_f, y_tr)
                    cal_acc = accuracy_score(y_te, cal.predict(X_te_f))
                except:
                    cal_acc = acc
                    cal = model

                results_log.append({
                    'feat_idx': fidx, 'model': mname,
                    'acc': acc, 'cal_acc': cal_acc,
                    'feat_dim': X_tr_f.shape[1],
                    'feat_groups': '+'.join(feat_groups),
                })

                if cal_acc > best_acc:
                    best_acc = cal_acc
                    best_config = (fidx, feat_groups, mname)
                    best_model = cal
            except:
                continue

    if best_model is None:
        return None, None, None, '全部配置失败'

    # 全量数据重新拟合
    X_full_f = np.array([concat_features(all_features_list[i], best_config[1]) for i in range(n)], dtype=float)
    scaler = StandardScaler()
    scaler.fit(X_full_f)
    X_s = scaler.transform(X_full_f)
    best_model.fit(X_s, y)

    return best_model, scaler, {
        'acc': best_acc,
        'feat_groups': '+'.join(best_config[1]),
        'model_name': best_config[2],
        'n_features': X_full_f.shape[1],
        'n_samples': n,
        'league': code,
        'search_log': results_log,
    }, None


def prepare_league_data(code):
    full_file = ODDS_DIR / f'{code}_full_odds.csv'
    if not full_file.exists():
        return None, None, 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) < 60:
        return None, None, None, f'数据太少({len(df)})'

    df['label'] = (df['FTHG'] + df['PinnAHh'] > df['FTAG']).astype(int)

    hist_files = list(ODDS_DIR.glob(f'{code}_S*_fixtures.csv'))
    if not hist_files:
        return None, None, None, f'无历史数据'
    all_hist = pd.concat([pd.read_csv(f, usecols=['HomeTeam','AwayTeam','FTHG','FTAG'], low_memory=False) for f in hist_files], ignore_index=True)
    tc = build_team_hist(all_hist)
    h2h = build_h2h_cache(all_hist)

    all_features = []
    y_rows = []
    for _, r in df.iterrows():
        try:
            feat = build_full_features(
                float(r['PinnAHh']), float(r['PinnAHH']), float(r['PinnAHA']),
                str(r['HomeTeam']), str(r['AwayTeam']), tc, h2h
            )
            all_features.append(feat)
            y_rows.append(int(r['label']))
        except:
            continue

    if len(all_features) < 60:
        return None, None, None, f'特征构建后不足({len(all_features)})'

    return all_features, np.array(y_rows), None, None


def train_and_select_best(code):
    print(f"\n{'='*50}")
    print(f"训练 {code} ({LEAGUE_NAMES.get(code, code)})")
    print(f"{'='*50}")

    all_features, y, _, err = prepare_league_data(code)
    if err:
        print(f"  → {err}")
        return None, err

    print(f"  样本数: {len(y)}, 候选: {len(MODEL_CONFIGS)}模型 × {len(FEATURE_CONFIGS)}特征组 = {len(MODEL_CONFIGS)*len(FEATURE_CONFIGS)}种组合")

    model, scaler, info, _ = train_league(code, all_features, y)
    if model is None:
        print(f"  → {info}")
        return None, info

    print(f"  最优: feat={info['feat_groups']}, model={info['model_name']}, acc={info['acc']:.1%}, n_feat={info['n_features']}")

    out_dir = OUT_DIR / code
    out_dir.mkdir(parents=True, exist_ok=True)
    joblib.dump({
        'model': model, 'scaler': scaler,
        'acc': info['acc'],
        'feat_groups': info['feat_groups'],
        'model_name': info['model_name'],
        'n_features': info['n_features'],
        'n_samples': info['n_samples'],
        'league': code,
    }, out_dir / 'optimized_pipeline.pkl')

    cfg = {
        'league': code,
        'league_name': LEAGUE_NAMES.get(code, code),
        'accuracy': float(info['acc']),
        'n_samples': int(info['n_samples']),
        'model_type': info['model_name'],
        'feature_groups': info['feat_groups'],
    }
    with open(out_dir / 'optimized_config.json', 'w') as f:
        json.dump(cfg, f, indent=2)

    with open(out_dir / 'search_log.json', 'w') as f:
        json.dump(info['search_log'], f, indent=2)

    return info['acc'], info['n_samples']


if __name__ == '__main__':
    print("=" * 60)
    print("ah_1year_v6 训练开始（per-league AutoML 独立最优）")
    print("=" * 60)
    print(f"XGBoost可用: {USE_XGB}")
    print(f"特征组: odds, hist, goals, adj, interact, recent, h2h, home_adv")
    print(f"模型: {', '.join(m for m,_ in MODEL_CONFIGS)}")

    results = {}
    for code in sorted(LEAGUE_NAMES.keys()):
        try:
            acc, n = train_and_select_best(code)
            if acc is not None:
                results[code] = {'acc': acc, 'n': n, 'status': '✅成功'}
            else:
                results[code] = {'status': f'⚠️{n}'}
        except Exception as e:
            import traceback; traceback.print_exc()
            results[code] = {'status': f'❌{e}'}

    # 汇总
    print("\n" + "=" * 70)
    print("ah_1year_v6 训练结果汇总")
    print("=" * 70)
    print(f"{'联赛':<6} {'名称':<8} {'准确率':<10} {'样本':<6} {'特征组':<40} {'模型':<15} {'状态'}")
    print("-" * 110)
    for code, r in sorted(results.items()):
        name = LEAGUE_NAMES.get(code, code)
        if 'acc' in r:
            print(f"{code:<6} {name:<8} {r['acc']:.1%}       {r['n']:<6}  {r.get('feat_groups',''):<40} {r.get('model_name',''):<15} {r['status']}")
        else:
            print(f"{code:<6} {name:<8} {'-':<10} {'-':<6}  {'-':<40} {'-':<15} {r.get('status','N/A')}")

    summary = {code: {'league_name': LEAGUE_NAMES.get(code, code), **r}
                for code, r in results.items()}
    with open(OUT_DIR / 'ah_1year_v6_summary.json', 'w') as f:
        json.dump(summary, f, indent=2, ensure_ascii=False)
    print(f"\n模型保存: {OUT_DIR}")
