#!/usr/bin/env python3
"""
ah_1year_v6 专用预测脚本
- 特征严格对应 train_ah_1year_v6.py 训练时的特征组合
- 每个联赛从自己的最优模型目录读取配置
输出: predictions/ah_1year_v6_24h.csv
"""
import os, sys, json, time, warnings, joblib
from pathlib import Path
from collections import defaultdict
import numpy as np
import pandas as pd
import requests

warnings.filterwarnings('ignore')
np.random.seed(42)

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

API_KEY = os.environ.get('API_FOOTBALL_KEY', '57b64e857ebeffdc156f0c41d43c7894')
BASE = 'https://v3.football.api-sports.io'
HEADERS = {'x-apisports-key': API_KEY}
TZ = __import__('datetime').timezone(__import__('datetime').timedelta(hours=8))

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

LEAGUE_IDS = {
    'e0':39,'e1':40,'e2':41,'e2n':42,
    'd1':78,'d2':79,'f1':61,'f2':62,
    'i1':135,'i2':136,'sp1':140,'sp2':141,
    'tr1':203,'p1':94,'g1':197,
    'n1':88,'j1':98,'m1':253,'br1':71,
}

# ═══════════════════════════════════════════════════════════════════
# 特征构建（与 train_ah_1year_v6.py 完全一致）
# ═══════════════════════════════════════════════════════════════════

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 = 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):
    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

    odds_feat = [ahh_f, norm_home, norm_away, odds_gap, norm_home - norm_away]

    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)]

    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)]

    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)]

    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),
    ]

    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, a_aw5 - a_aw20]

    h2h_feat = [_h2h_wr(h2h, home, away, 5), _h2h_wr(h2h, home, away, 10), _h2h_wr(h2h, home, away, 5) * ahh_f]

    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

def parse_asian_handicap(bookmakers):
    all_h = {}
    for bm in bookmakers:
        for bet in bm.get('bets', []):
            if bet['name'] != 'Asian Handicap':
                continue
            for v in bet.get('values', []):
                raw = v.get('value', ''); odd_s = v.get('odd', '')
                if not raw or not odd_s: continue
                try: odd = float(odd_s)
                except: continue
                parts = raw.split()
                if len(parts) < 2: continue
                side, h_str = parts[0], parts[1]
                sign = 1 if h_str.startswith('+') else -1
                h_num = float(h_str.replace('+','').replace('-',''))
                handicap = sign * h_num
                if handicap not in all_h: all_h[handicap] = [None, None]
                if side in ('Home','H'): all_h[handicap][0] = odd
                elif side in ('Away','A'): all_h[handicap][1] = odd
    valid = [(h, ho, ao) for h, (ho, ao) in all_h.items() if ho is not None and ao is not None]
    if not valid: return None, None, None
    def balance_score(item):
        h, ho, ao = item
        if ho <= 1 or ao <= 1: return 999
        return abs(1/ho - 1/ao)
    return min(valid, key=balance_score)

def rate_call():
    time.sleep(0.5)

def generate_predictions(hours=24):
    now = pd.Timestamp.now(TZ)
    end = now + pd.Timedelta(hours=hours)
    results = []
    session = requests.Session()
    session.headers.update(HEADERS)

    for code in sorted(LEAGUE_IDS.keys()):
        model_dir = MODEL_DIR / code
        pipeline_path = model_dir / 'optimized_pipeline.pkl'
        if not pipeline_path.exists():
            print(f"  [{code}] 无模型文件")
            continue

        model_data = joblib.load(pipeline_path)
        scaler = model_data['scaler']
        model = model_data['model']
        acc = model_data.get('acc', 0)
        feat_groups_str = model_data.get('feat_groups', 'odds')
        feat_groups = feat_groups_str.split('+') if '+' in feat_groups_str else [feat_groups_str]
        n_features = model_data.get('n_features', 0)
        league_name = LEAGUE_NAMES.get(code, code)

        print(f"  [{code}] {league_name} | acc={acc:.1%} | feat={feat_groups_str} | n_feat={n_features}")

        # 构建空缓存（预测时无历史数据）
        team_cache = {}
        h2h_cache = {}

        rate_call()
        try:
            r = session.get(f'{BASE}/fixtures', params={
                'league': LEAGUE_IDS[code], 'season': 2025,
                'from': now.strftime('%Y-%m-%d'),
                'to': end.strftime('%Y-%m-%d'),
                'timezone': 'Asia/Shanghai',
            }, timeout=30)
            fixtures = r.json().get('response', [])
        except Exception as e:
            print(f"  [{code}] 获取赛程失败: {e}")
            continue

        for fix in fixtures:
            fi = fix['fixture']
            teams = fix['teams']
            kickoff = pd.Timestamp(fi['date'], tz='UTC').tz_convert(TZ)
            if kickoff < now or kickoff > end:
                continue

            home = teams['home']['name']
            away = teams['away']['name']
            fid = fi['id']

            rate_call()
            try:
                odd_resp = session.get(f'{BASE}/odds', params={
                    'fixture': fid, 'timezone': 'Asia/Shanghai'
                }, timeout=8)
                odds_data = odd_resp.json().get('response', [])
            except:
                continue

            if not odds_data:
                continue

            bookmakers = odds_data[0].get('bookmakers', [])
            ah_h, ah_home_odd, ah_away_odd = parse_asian_handicap(bookmakers)
            if ah_h is None:
                continue

            try:
                full_feat = build_full_features(float(ah_h), float(ah_home_odd), float(ah_away_odd), home, away, team_cache, h2h_cache)
                feat_vec = concat_features(full_feat, feat_groups)
                feat_arr = np.array([feat_vec], dtype=float)
                feat_s = scaler.transform(feat_arr)
                proba = model.predict_proba(feat_s)[0]
                prob_up = float(proba[1] * 100)
                prob_down = float(proba[0] * 100)
            except Exception as e:
                print(f"  [{code}] fid={fid} 预测失败: {e}")
                prob_up, prob_down = 50.0, 50.0

            conf = max(prob_up, prob_down)
            # ah_h < 0: 主队让球 → 上盘=主队赢；ah_h > 0: 客队让球 → 上盘=客队赢
            if prob_up >= prob_down:
                pred = '主队' if ah_h <= 0 else '客队'
            else:
                pred = '客队' if ah_h <= 0 else '主队'

            if conf >= 72: rec = '★★★ 强烈推荐'
            elif conf >= 62: rec = '★★ 推荐'
            elif conf >= 52: rec = '★ 谨慎推荐'
            else: rec = '✗ 不建议'

            if ah_h < 0: hand = f"主队让{abs(ah_h):.2f}"
            elif ah_h > 0: hand = f"客队让{ah_h:.2f}"
            else: hand = "平手"

            results.append({
                '联赛': f"{code.upper()} {league_name}",
                '开赛时间': kickoff.strftime('%m-%d %H:%M'),
                '主队': home, '客队': away,
                '让球说明': hand,
                '预测方向': pred,
                '上盘概率': round(prob_up, 1),
                '下盘概率': round(prob_down, 1),
                '置信度': round(conf, 1),
                '推荐等级': rec,
                '历史准确率': round(acc * 100, 1),
                '模型配置': f"{feat_groups_str} {model_data.get('model_name','')}",
                '生成时间': pd.Timestamp.now(TZ).strftime('%Y-%m-%d %H:%M'),
            })

    df = pd.DataFrame(results)
    return df

if __name__ == '__main__':
    print("=" * 60)
    print("ah_1year_v6 预测生成")
    print("=" * 60)
    df = generate_predictions(hours=24)
    csv_path = PRED_DIR / 'ah_1year_v6_24h.csv'
    xlsx_path = PRED_DIR / 'ah_1year_v6_24h.xlsx'
    df.to_csv(csv_path, index=False, encoding='utf-8-sig')
    try: df.to_excel(xlsx_path, index=False)
    except: pass
    print(f"\n共 {len(df)} 条预测 → {csv_path}")
    if len(df) > 0:
        high = df[df['置信度'] >= 62].sort_values('置信度', ascending=False)
        print(f"\n★★ 推荐 ({len(high)} 条)：")
        for _, r in high.iterrows():
            print(f"  {r['联赛']} {r['开赛时间']} {r['主队']} vs {r['客队']} | {r['让球说明']} | 押{r['预测方向']} {r['置信度']}% | {r['推荐等级']} | 历史{r['历史准确率']}%")
