#!/usr/bin/env python3
"""
独立亚盘（Asian Handicap）预测模型
- 训练数据：2023-2025年历史比赛
- 核心思路：把"庄家亚盘赔率方向"作为重要特征输入
- 目标：建立独立的亚盘上下盘预测模型
"""
from __future__ import annotations
import os, sys, re, json, warnings
from pathlib import Path
from collections import defaultdict
from datetime import datetime, timedelta, timezone
from typing import Dict, List, Optional, Tuple

import joblib
import numpy as np
import pandas as pd
from sklearn.ensemble import ExtraTreesClassifier, RandomForestClassifier, HistGradientBoostingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import accuracy_score, classification_report

warnings.filterwarnings('ignore')

REPO_ROOT = Path('/root/.openclaw/workspace-flj_bot1/football-predictor-repro')
HIST_DIR = REPO_ROOT / 'historical_data'
AH_MODEL_DIR = REPO_ROOT / 'models/ah_models'
AH_MODEL_DIR.mkdir(parents=True, exist_ok=True)

# 支持的联赛及中文名
LEAGUE_INFO: Dict[str, Dict] = {
    'e2': {'name': '英冠', 'country': 'England', 'league': 'Championship'},
    'e3': {'name': '英乙', 'country': 'England', 'league': 'League Two'},
    'e1': {'name': '英甲', 'country': 'England', 'league': 'League One'},
    'd1': {'name': '德甲', 'country': 'Germany', 'league': 'Bundesliga'},
    'f1': {'name': '法甲', 'country': 'France', 'league': 'Ligue 1'},
    'i1': {'name': '意甲', 'country': 'Italy', 'league': 'Serie A'},
    'sp1': {'name': '西甲', 'country': 'Spain', 'league': 'La Liga'},
    'sc1': {'name': '苏超', 'country': 'Scotland', 'league': 'Premiership'},
    'tr1': {'name': '土超', 'country': 'Turkey', 'league': 'Süper Lig'},
    'p1': {'name': '葡超', 'country': 'Portugal', 'league': 'Primeira Liga'},
    'e2n': {'name': '荷乙', 'country': 'Netherlands', 'league': 'Eerste Divisie'},
    'cz1': {'name': '捷克甲', 'country': 'Czech-Republic', 'league': 'Czech Liga'},
}


def prob(odds: float) -> float:
    """赔率转概率（不含水）"""
    if odds and float(odds) > 1:
        return 1.0 / float(odds)
    return np.nan


def build_ah_dataset(code: str) -> pd.DataFrame:
    """为一个联赛构建亚盘训练数据集"""
    hp = HIST_DIR / f'{code}_history.csv'
    if not hp.exists():
        return pd.DataFrame()

    df = pd.read_csv(hp, low_memory=False)
    df['Date'] = pd.to_datetime(df['Date'], errors='coerce', dayfirst=True)
    df = df[(df['Date'] >= '2023-01-01') & (df['Date'] <= '2025-12-31')]
    df = df.dropna(subset=['FTHG', 'FTAG', 'AHh', 'B365AHH', 'B365AHA'])
    df = df[df['B365AHH'] > 1]  # 有效赔率

    # 目标变量：上盘=1, 下盘=0, 走盘=剔除
    df['gd_after_ah'] = df['FTHG'] - df['FTAG'] + df['AHh']
    df['is_push'] = df['gd_after_ah'].abs() < 0.01
    df = df[~df['is_push']]  # 剔除走盘

    df['target'] = (df['gd_after_ah'] > 0).astype(int)  # 1=上盘, 0=下盘

    # 庄家亚盘方向（核心特征！）
    df['bm_ah_home_prob'] = df['B365AHH'].apply(prob)
    df['bm_ah_away_prob'] = df['B365AHA'].apply(prob)
    overround = df['bm_ah_home_prob'] + df['bm_ah_away_prob']
    df['bm_ah_home_prob_net'] = df['bm_ah_home_prob'] / overround
    df['bm_ah_away_prob_net'] = df['bm_ah_away_prob'] / overround
    df['bm_ah_direction'] = (df['bm_ah_home_prob_net'] > 0.5).astype(int)  # 1=庄家看上盘, 0=庄家看下盘
    df['bm_ah_conf'] = abs(df['bm_ah_home_prob_net'] - 0.5) * 2  # 0-1 置信度
    df['bm_ah_balance'] = abs(df['B365AHH'] - df['B365AHA'])  # 赔率差

    # 庄家欧赔方向
    if 'B365H' in df.columns and 'B365A' in df.columns:
        df['bm_3way_home_prob'] = df['B365H'].apply(prob)
        df['bm_3way_away_prob'] = df['B365A'].apply(prob)
        overround3 = df['bm_3way_home_prob'] + df['B365A'].apply(prob) + df.get('B365D', pd.Series([np.nan]*len(df))).apply(prob)
        df['bm_3way_home_prob'] = df['bm_3way_home_prob'] / overround3.replace(0, np.nan)
        df['bm_3way_direction'] = (df['bm_3way_home_prob'] > 0.5).astype(int)

    # AH盘口类型
    df['ah_line_type'] = df['AHh'].apply(
        lambda x: 'strong_home' if x <= -1.0 else ('medium_home' if x < 0 else ('neutral' if x == 0 else ('medium_away' if x <= 0.75 else 'strong_away')))
    )

    return df


def build_team_histories(df: pd.DataFrame, code: str) -> Tuple[defaultdict, defaultdict]:
    """从历史数据建立球队历史特征"""
    # 按日期排序
    df = df.sort_values('Date').reset_index(drop=True)

    team_stats = defaultdict(lambda: {
        'home_gf': [], 'home_ga': [], 'home_tot': [], 'home_over': [],
        'away_gf': [], 'away_ga': [], 'away_tot': [], 'away_over': [],
        'ah_home_wins': [], 'ah_away_wins': [],
        'gf': [], 'ga': [], 'tot': [], 'over': [],
    })

    league_tot = []
    league_over = []
    league_ah_home_wins = []
    league_ah_total = []

    # 累积统计
    for _, row in df.iterrows():
        hg = float(row['FTHG'])
        ag = float(row['FTAG'])
        tot = hg + ag
        is_over = int(tot > 2.5)
        ahh = float(row['AHh'])
        gd = hg - ag + ahh
        ah_home_win = int(gd > 0)

        home = str(row['HomeTeam'])
        away = str(row['AwayTeam'])

        # 更新主队统计
        team_stats[home]['gf'].append(hg)
        team_stats[home]['ga'].append(ag)
        team_stats[home]['tot'].append(tot)
        team_stats[home]['over'].append(is_over)
        team_stats[home]['home_gf'].append(hg)
        team_stats[home]['home_ga'].append(ag)
        team_stats[home]['home_tot'].append(tot)
        team_stats[home]['home_over'].append(is_over)
        team_stats[home]['ah_home_wins'].append(ah_home_win)

        # 更新客队统计
        team_stats[away]['gf'].append(ag)
        team_stats[away]['ga'].append(hg)
        team_stats[away]['tot'].append(tot)
        team_stats[away]['over'].append(is_over)
        team_stats[away]['away_gf'].append(ag)
        team_stats[away]['away_ga'].append(hg)
        team_stats[away]['away_tot'].append(tot)
        team_stats[away]['away_over'].append(is_over)
        team_stats[away]['ah_away_wins'].append(1 - ah_home_win)

        # 联赛统计
        league_tot.append(tot)
        league_over.append(is_over)
        league_ah_home_wins.append(ah_home_win)
        league_ah_total.append(1)

    league_hist = {
        'tot': league_tot,
        'over': league_over,
        'ah_home_win_rate': league_ah_home_wins,
    }

    return team_stats, league_hist


def extract_features_at_match(row_idx: int, df: pd.DataFrame, team_stats: defaultdict, league_hist: dict) -> dict:
    """为某场比赛提取特征（使用截止到该比赛之前的历史）"""
    row = df.iloc[row_idx]
    home = str(row['HomeTeam'])
    away = str(row['AwayTeam'])
    ahh = float(row['AHh'])

    def mean5(v):
        if len(v) == 0:
            return 2.5
        vals = v[-5:] if len(v) >= 5 else v
        return float(np.mean(vals)) if vals else 2.5

    def mean3(v):
        if len(v) == 0:
            return 2.5
        vals = v[-3:] if len(v) >= 3 else v
        return float(np.mean(vals)) if vals else 2.5

    def win_rate(v):
        if len(v) == 0:
            return 0.5
        return float(np.mean(v))

    def streak(v, target):
        s = 0
        for x in reversed(v):
            if x == target:
                s += 1
            else:
                break
        return s

    # 主队历史（排除当前比赛）
    h = team_stats[home]
    a = team_stats[away]

    # 近期特征（用最近5场）
    features = {
        # === 核心特征：庄家亚盘方向 ===
        'bm_ah_direction': row['bm_ah_direction'],
        'bm_ah_conf': row['bm_ah_conf'],
        'bm_ah_balance': row['bm_ah_balance'],
        'bm_ah_home_prob_net': row['bm_ah_home_prob_net'],
        'bm_ah_away_prob_net': row['bm_ah_away_prob_net'],

        # AH盘口类型
        'ahh': ahh,
        'ahh_abs': abs(ahh),
        'ah_strong_home': 1 if ahh <= -1.0 else 0,
        'ah_medium_home': 1 if -1.0 < ahh < 0 else 0,
        'ah_neutral': 1 if abs(ahh) <= 0.01 else 0,
        'ah_medium_away': 1 if 0 < ahh <= 0.75 else 0,
        'ah_strong_away': 1 if ahh > 0.75 else 0,

        # 庄家3way方向
        'bm_3way_direction': row.get('bm_3way_direction', 0.5),
        'bm_3way_home_prob': row.get('bm_3way_home_prob', 0.5),

        # === 主队特征 ===
        'home_gf_avg': mean5(h['gf']) if h['gf'] else 1.5,
        'home_ga_avg': mean5(h['ga']) if h['ga'] else 1.5,
        'home_tot_avg': mean5(h['tot']) if h['tot'] else 2.5,
        'home_over_rate': win_rate(h['over'][-10:]) if h['over'] else 0.5,
        'home_gf_last3': mean3(h['gf']) if h['gf'] else 1.5,
        'home_ga_last3': mean3(h['ga']) if h['ga'] else 1.5,
        'home_over_last5': win_rate(h['over'][-5:]) if h['over'] else 0.5,

        # 主队主场特征
        'home_home_gf': mean5(h['home_gf']) if h['home_gf'] else 1.5,
        'home_home_ga': mean5(h['home_ga']) if h['home_ga'] else 1.5,
        'home_home_over': win_rate(h['home_over'][-5:]) if h['home_over'] else 0.5,
        'home_ah_win_rate': win_rate(h['ah_home_wins'][-10:]) if h['ah_home_wins'] else 0.5,
        'home_ah_streak': streak(h['ah_home_wins'], 1) if h['ah_home_wins'] else 0,

        # === 客队特征 ===
        'away_gf_avg': mean5(a['gf']) if a['gf'] else 1.2,
        'away_ga_avg': mean5(a['ga']) if a['ga'] else 1.2,
        'away_tot_avg': mean5(a['tot']) if a['tot'] else 2.5,
        'away_over_rate': win_rate(a['over'][-10:]) if a['over'] else 0.5,
        'away_gf_last3': mean3(a['gf']) if a['gf'] else 1.2,
        'away_ga_last3': mean3(a['ga']) if a['ga'] else 1.2,
        'away_over_last5': win_rate(a['over'][-5:]) if a['over'] else 0.5,

        # 客队客场特征
        'away_away_gf': mean5(a['away_gf']) if a['away_gf'] else 1.2,
        'away_away_ga': mean5(a['away_ga']) if a['away_ga'] else 1.2,
        'away_away_over': win_rate(a['away_over'][-5:]) if a['away_over'] else 0.5,
        'away_ah_win_rate': win_rate(a['ah_away_wins'][-10:]) if a['ah_away_wins'] else 0.5,

        # === 联赛特征 ===
        'league_ah_home_win_rate': float(np.mean(league_hist.get('ah_home_win_rate', [0.5]))) if league_hist.get('ah_home_win_rate') else 0.5,
        'league_tot_avg': float(np.mean(league_hist.get('tot', [2.5]))) if league_hist.get('tot') else 2.5,
        'league_over_rate': float(np.mean(league_hist.get('over', [0.5]))) if league_hist.get('over') else 0.5,

        # === 对比特征 ===
        'form_diff': (win_rate(h['over'][-5:]) if h['over'] else 0.5) - (win_rate(a['over'][-5:]) if a['over'] else 0.5),
        'eg_diff': (mean5(h['gf']) if h['gf'] else 1.5) - (mean5(a['ga']) if a['ga'] else 1.2),
        'home_away_gf_diff': (mean5(h['home_gf']) if h['home_gf'] else 1.5) - (mean5(a['away_gf']) if a['away_gf'] else 1.2),
        'home_away_def_diff': (mean5(h['home_ga']) if h['home_ga'] else 1.5) - (mean5(a['away_ga']) if a['away_ga'] else 1.2),

        # 庄家方向与基本面的对比
        'bm_dir_vs_home_form': row['bm_ah_direction'] - (win_rate(h['ah_home_wins'][-5:]) if h['ah_home_wins'] else 0.5),
        'bm_dir_vs_away_form': (1 - row['bm_ah_direction']) - (win_rate(a['ah_away_wins'][-5:]) if a['ah_away_wins'] else 0.5),

        # 时间特征
        'day_of_week': row['Date'].dayofweek if hasattr(row['Date'], 'dayofweek') else 3,
        'month': row['Date'].month if hasattr(row['Date'], 'month') else 6,
    }

    return features


FEATURE_COLUMNS = [
    'bm_ah_direction', 'bm_ah_conf', 'bm_ah_balance',
    'bm_ah_home_prob_net', 'bm_ah_away_prob_net',
    'ahh', 'ahh_abs',
    'ah_strong_home', 'ah_medium_home', 'ah_neutral', 'ah_medium_away', 'ah_strong_away',
    'bm_3way_direction', 'bm_3way_home_prob',
    'home_gf_avg', 'home_ga_avg', 'home_tot_avg', 'home_over_rate',
    'home_gf_last3', 'home_ga_last3', 'home_over_last5',
    'home_home_gf', 'home_home_ga', 'home_home_over',
    'home_ah_win_rate', 'home_ah_streak',
    'away_gf_avg', 'away_ga_avg', 'away_tot_avg', 'away_over_rate',
    'away_gf_last3', 'away_ga_last3', 'away_over_last5',
    'away_away_gf', 'away_away_ga', 'away_away_over',
    'away_ah_win_rate',
    'league_ah_home_win_rate', 'league_tot_avg', 'league_over_rate',
    'form_diff', 'eg_diff', 'home_away_gf_diff', 'home_away_def_diff',
    'bm_dir_vs_home_form', 'bm_dir_vs_away_form',
    'day_of_week', 'month',
]


def train_ah_model(code: str, df: pd.DataFrame) -> dict:
    """训练单个联赛的亚盘模型"""
    print(f"\n训练 {code} 亚盘模型...")
    print(f"  数据量: {len(df)} 场 (排除走盘)")

    team_stats, league_hist = build_team_histories(df, code)

    # 构建特征和目标
    X_list = []
    y_list = []

    for idx in range(len(df)):
        row = df.iloc[idx]
        home = str(row['HomeTeam'])
        away = str(row['AwayTeam'])

        # 确认有足够历史（至少10场）
        if (len(team_stats[home]['gf']) < 5 or len(team_stats[away]['gf']) < 5):
            continue

        features = extract_features_at_match(idx, df, team_stats, league_hist)
        X_list.append(features)
        y_list.append(int(row['target']))

        # 更新历史（追加当前比赛，用于下一场）
        hg = float(row['FTHG'])
        ag = float(row['FTAG'])
        tot = hg + ag
        is_over = int(tot > 2.5)
        ahh = float(row['AHh'])
        gd = hg - ag + ahh
        ah_hw = int(gd > 0)

        h = team_stats[home]
        a = team_stats[away]
        h['gf'].append(hg); h['ga'].append(ag); h['tot'].append(tot); h['over'].append(is_over)
        h['home_gf'].append(hg); h['home_ga'].append(ag); h['home_tot'].append(tot); h['home_over'].append(is_over)
        h['ah_home_wins'].append(ah_hw)
        a['gf'].append(ag); a['ga'].append(hg); a['tot'].append(tot); a['over'].append(is_over)
        a['away_gf'].append(ag); a['away_ga'].append(hg); a['away_tot'].append(tot); a['away_over'].append(is_over)
        a['ah_away_wins'].append(1 - ah_hw)
        league_hist['tot'].append(tot)
        league_hist['over'].append(is_over)
        league_hist['ah_home_win_rate'].append(ah_hw)

    if len(X_list) < 100:
        print(f"  跳过: 有效样本不足 ({len(X_list)})")
        return None

    X = pd.DataFrame(X_list)[FEATURE_COLUMNS].fillna(0)
    y = np.array(y_list)

    # 时间切分：前80%训练，后20%测试
    split = int(len(X) * 0.8)
    X_train, X_test = X.iloc[:split], X.iloc[split:]
    y_train, y_test = y[:split], y[split:]

    print(f"  训练集: {len(X_train)}, 测试集: {len(X_test)}")
    print(f"  上盘率: {y.mean():.1%} (全量)")

    # 尝试多个模型
    models = {
        'ET': ExtraTreesClassifier(n_estimators=300, max_depth=8, min_samples_leaf=10, random_state=42, n_jobs=-1),
        'HGB': HistGradientBoostingClassifier(max_depth=5, max_iter=200, learning_rate=0.05, random_state=42),
        'RF': RandomForestClassifier(n_estimators=300, max_depth=6, min_samples_leaf=15, random_state=42, n_jobs=-1),
    }

    best_acc = 0
    best_name = None
    best_model = None
    best_scaler = None
    results = {}

    for name, clf in models.items():
        scaler = StandardScaler()
        X_train_s = scaler.fit_transform(X_train)
        X_test_s = scaler.transform(X_test)
        clf.fit(X_train_s, y_train)
        y_pred = clf.predict(X_test_s)
        acc = accuracy_score(y_test, y_pred)
        results[name] = acc
        print(f"  {name}: 测试准确率 = {acc:.1%}")
        if acc > best_acc:
            best_acc = acc
            best_name = name
            best_model = clf
            best_scaler = scaler

    # 特征重要性
    if hasattr(best_model, 'feature_importances_'):
        fi = pd.Series(best_model.feature_importances_, index=FEATURE_COLUMNS).sort_values(ascending=False)
        print(f"\n  Top 10 重要特征:")
        for feat, imp in fi.head(10).items():
            print(f"    {feat}: {imp:.3f}")

    # 计算测试集庄家基准
    bm_correct = (X_test['bm_ah_direction'] == y_test).mean()
    print(f"\n  庄家基准准确率: {bm_correct:.1%}")
    print(f"  模型提升: +{(best_acc - bm_correct)*100:.1f}pp")

    return {
        'model': best_model,
        'scaler': best_scaler,
        'feature_columns': FEATURE_COLUMNS,
        'league': code,
        'league_name': LEAGUE_INFO.get(code, {}).get('name', code),
        'train_size': len(X_train),
        'test_size': len(X_test),
        'accuracy': best_acc,
        'bookmaker_accuracy': bm_correct,
        'model_name': best_name,
    }


def main():
    print("=" * 80)
    print("亚盘独立预测模型训练 - 2023-2025数据")
    print("=" * 80)

    all_results = {}

    for code in sorted(LEAGUE_INFO.keys()):
        hp = HIST_DIR / f'{code}_history.csv'
        if not hp.exists():
            print(f"\n跳过 {code}: 历史数据文件不存在")
            continue

        df = build_ah_dataset(code)
        if df.empty:
            print(f"\n跳过 {code}: 无有效数据")
            continue

        result = train_ah_model(code, df)
        if result is None:
            continue

        # 保存模型
        model_path = AH_MODEL_DIR / f'{code}_ah_model.pkl'
        scaler_path = AH_MODEL_DIR / f'{code}_ah_scaler.pkl'
        config_path = AH_MODEL_DIR / f'{code}_ah_config.json'

        joblib.dump(result['model'], model_path)
        joblib.dump(result['scaler'], scaler_path)
        config = {
            'league': code,
            'league_name': result['league_name'],
            'feature_columns': result['feature_columns'],
            'train_size': result['train_size'],
            'test_size': result['test_size'],
            'accuracy': result['accuracy'],
            'bookmaker_accuracy': result['bookmaker_accuracy'],
            'model_name': result['model_name'],
            'trained_at': datetime.now().isoformat(),
            'data_range': '2023-2025',
        }
        with open(config_path, 'w') as f:
            json.dump(config, f, indent=2, ensure_ascii=False)

        all_results[code] = config
        print(f"  → 已保存: {code}_ah_model.pkl")

    # 汇总
    print("\n" + "=" * 80)
    print("训练完成 - 汇总")
    print("=" * 80)
    print(f"\n{'联赛':<10} {'模型':<8} {'测试准确率':<12} {'庄家基准':<12} {'提升':<8}")
    print("-" * 55)
    for code, cfg in sorted(all_results.items(), key=lambda x: x[1]['accuracy'], reverse=True):
        lift = cfg['accuracy'] - cfg['bookmaker_accuracy']
        print(f"{cfg['league_name']:<10} {cfg['model_name']:<8} {cfg['accuracy']:.1%}       {cfg['bookmaker_accuracy']:.1%}       {'+' if lift > 0 else ''}{lift:.1%}")

    total = len(all_results)
    avg_acc = np.mean([v['accuracy'] for v in all_results.values()])
    avg_bm = np.mean([v['bookmaker_accuracy'] for v in all_results.values()])
    print("-" * 55)
    print(f"{'平均':<10} {'':<8} {avg_acc:.1%}       {avg_bm:.1%}       {'+' if avg_acc-avg_bm > 0 else ''}{(avg_acc-avg_bm):.1%}")
    print(f"\n模型保存在: {AH_MODEL_DIR}/")


if __name__ == '__main__':
    main()
