#!/usr/bin/env python3
"""Evaluate 1year-V1 optimized models on recent data (2025-12-01+)."""
import json, warnings
from pathlib import Path
from collections import defaultdict
import pandas as pd
import numpy as np
import joblib

warnings.filterwarnings('ignore')

REPO_ROOT = Path('/root/.openclaw/workspace-flj_bot1/football-predictor-repro')
HISTORICAL_DIR = REPO_ROOT / 'historical_data'
MODEL_DIR = REPO_ROOT / 'models' / '1year_V1_optimized'
RECENT_SINCE = '2025-12-01'

LEAGUES = ['a1', 'br1', 'cz1', 'd1', 'e0', 'e1', 'e2', 'e2n', 'e3', 'f1', 'g1', 'i1', 'p1', 'sc1', 'tr1']

LEAGUE_NAMES = {
    'a1': '澳超', 'br1': '巴甲', 'cz1': '捷克甲', 'd1': '德甲',
    'e0': '英超', 'e1': '英甲', 'e2': '英冠', 'e2n': '英乙',
    'e3': '英格兰第五级', 'f1': '法甲', 'g1': '希腊超', 'i1': '意甲',
    'p1': '葡超', 'sc1': '苏超', 'tr1': '土超'
}

# Leagues where R2 uses 114 features (with B365 odds)
ODDS_LEAGUES = {'cz1', 'd1', 'e1', 'e2', 'e2n', 'e3', 'f1', 'i1', 'p1', 'sc1', 'tr1'}

BASE_FEATURES = [
    'home_gf_last3', 'home_ga_last3', 'away_gf_last3', 'away_ga_last3', 'home_over_last3', 'away_over_last3',
    'home_gf_last5', 'home_ga_last5', 'away_gf_last5', 'away_ga_last5', 'home_over_last5', 'away_over_last5',
    'home_home_gf_last5', 'home_home_ga_last5', 'away_away_gf_last5', 'away_away_ga_last5', 'home_home_over_last5', 'away_away_over_last5',
    'home_gf_last10', 'home_ga_last10', 'away_gf_last10', 'away_ga_last10', 'home_over_last10', 'away_over_last10',
    'home_home_gf_last10', 'home_home_ga_last10', 'away_away_gf_last10', 'away_away_ga_last10', 'home_home_over_last10', 'away_away_over_last10',
    'home_gf_last15', 'home_ga_last15', 'away_gf_last15', 'away_ga_last15', 'home_over_last15', 'away_over_last15',
    'home_home_gf_last15', 'home_home_ga_last15', 'away_away_gf_last15', 'away_away_ga_last15', 'home_home_over_last15', 'away_away_over_last15',
    'home_gf_last20', 'home_ga_last20', 'away_gf_last20', 'away_ga_last20', 'home_over_last20', 'away_over_last20',
    'home_home_gf_last20', 'home_home_ga_last20', 'away_away_gf_last20', 'away_away_ga_last20', 'home_home_over_last20', 'away_away_over_last20',
    'home_gf_avg', 'home_ga_avg', 'away_gf_avg', 'away_ga_avg', 'home_tot_avg', 'away_tot_avg',
    'home_over_rate', 'away_over_rate', 'home_home_gf_avg', 'home_home_ga_avg', 'away_away_gf_avg', 'away_away_ga_avg',
    'home_home_over_rate', 'away_away_over_rate', 'home_momentum', 'away_momentum', 'home_recent_form', 'away_recent_form',
    'home_goal_trend', 'away_goal_trend', 'home_gf_var', 'home_ga_var', 'home_gf_std', 'home_ga_std',
    'away_gf_var', 'away_ga_var', 'away_gf_std', 'away_ga_std', 'expected_goals_home', 'expected_goals_away',
    'expected_goals_proxy', 'combined_over_rate', 'home_away_gf_diff', 'home_away_ga_diff', 'home_away_tot_diff',
    'home_away_over_diff', 'form_ratio', 'momentum_advantage', 'home_clean_sheet_rate', 'away_clean_sheet_rate',
    'home_btts_rate', 'away_btts_rate', 'combined_btts_rate'
]

B365_OU = ['b365_over_odds', 'b365_under_odds', 'b365_over_prob', 'b365_under_prob',
           'b365_fair_over', 'b365_margin', 'market_over_prob', 'b365_over_under_ratio']
B365_1X2 = ['b365_home_odds', 'b365_draw_odds', 'b365_away_odds',
            'b365_home_prob', 'b365_draw_prob', 'b365_away_prob',
            'b365_home_away_gap', 'b365_home_draw_gap', 'b365_draw_away_gap']


class FeatureBuilder:
    def __init__(self, use_b365_odds=False):
        self.use_b365_odds = use_b365_odds
        self.team_hist = None
    
    def reset(self):
        self.team_hist = defaultdict(lambda: {
            "gf": [], "ga": [], "tot": [], "over": [],
            "home_gf": [], "home_ga": [], "home_over": [],
            "away_gf": [], "away_ga": [], "away_over": [],
            "gf_last3": [], "ga_last3": [], "over_last3": [],
            "gf_last5": [], "ga_last5": [], "over_last5": [],
            "gf_last10": [], "ga_last10": [], "over_last10": [],
            "gf_last15": [], "ga_last15": [], "over_last15": [],
            "gf_last20": [], "ga_last20": [], "over_last20": [],
            "momentum": [], "recent_form": [], "goal_trend": [],
            "home_clean": [], "away_clean": [],
            "btts_yes": [], "btts_no": [],
        })
    
    def seed(self, df):
        self.reset()
        for _, match in df.iterrows():
            self._update(match)
    
    def _update(self, match):
        home, away = str(match["HomeTeam"]), str(match["AwayTeam"])
        hh, ah = self.team_hist[home], self.team_hist[away]
        fthg = float(match.get('FTHG', 0) or 0)
        ftag = float(match.get('FTAG', 0) or 0)
        tg = fthg + ftag; ov = 1 if tg > 2.5 else 0
        for w in [3,5,10,15,20]:
            hh[f"gf_last{w}"].append(tg); hh[f"ga_last{w}"].append(tg); hh[f"over_last{w}"].append(ov)
            ah[f"gf_last{w}"].append(tg); ah[f"ga_last{w}"].append(tg); ah[f"over_last{w}"].append(ov)
            if w in [5,10,15,20]:
                hh["home_gf"].append(fthg); hh["home_ga"].append(ftag); hh["home_over"].append(ov)
                ah["away_gf"].append(fthg); ah["away_ga"].append(ftag); ah["away_over"].append(ov)
        hh["gf"].append(tg); hh["ga"].append(tg); hh["tot"].append(tg); hh["over"].append(ov)
        ah["gf"].append(tg); ah["ga"].append(tg); ah["tot"].append(tg); ah["over"].append(ov)
        hh["home_clean"].append(1 if ftag==0 else 0); ah["away_clean"].append(1 if fthg==0 else 0)
        hh["btts_yes"].append(1 if fthg>0 and ftag>0 else 0); ah["btts_yes"].append(1 if fthg>0 and ftag>0 else 0)
        hh["btts_no"].append(1 if fthg==0 or ftag==0 else 0); ah["btts_no"].append(1 if fthg==0 or ftag==0 else 0)
        hh["momentum"].append(1 if tg>ftag else 0.5); ah["momentum"].append(1 if tg>fthg else 0.5)
        hh["recent_form"].append(1 if tg>ftag else 0.5); ah["recent_form"].append(1 if tg>fthg else 0.5)
        if len(hh["gf_last10"])>=2:
            r,o = hh["gf_last10"][-5:], hh["gf_last10"][:-5]
            hh["goal_trend"].append(np.mean(r)-np.mean(o) if o else 0)
        if len(ah["gf_last10"])>=2:
            r,o = ah["gf_last10"][-5:], ah["gf_last10"][:-5]
            ah["goal_trend"].append(np.mean(r)-np.mean(o) if o else 0)
    
    def build(self, match):
        home, away = str(match["HomeTeam"]), str(match["AwayTeam"])
        hh, ah = self.team_hist[home], self.team_hist[away]
        row = {}
        
        for w in [3,5,10,15,20]:
            row[f"home_gf_last{w}"] = np.mean(hh[f"gf_last{w}"]) if hh[f"gf_last{w}"] else 1.4
            row[f"home_ga_last{w}"] = np.mean(hh[f"ga_last{w}"]) if hh[f"ga_last{w}"] else 1.4
            row[f"away_gf_last{w}"] = np.mean(ah[f"gf_last{w}"]) if ah[f"gf_last{w}"] else 1.2
            row[f"away_ga_last{w}"] = np.mean(ah[f"ga_last{w}"]) if ah[f"ga_last{w}"] else 1.2
            row[f"home_over_last{w}"] = np.mean(hh[f"over_last{w}"]) if hh[f"over_last{w}"] else 0.5
            row[f"away_over_last{w}"] = np.mean(ah[f"over_last{w}"]) if ah[f"over_last{w}"] else 0.5
            if w in [5,10,15,20]:
                row[f"home_home_gf_last{w}"] = np.mean(hh["home_gf"][-w:]) if len(hh["home_gf"])>=w else 1.4
                row[f"home_home_ga_last{w}"] = np.mean(hh["home_ga"][-w:]) if len(hh["home_ga"])>=w else 1.4
                row[f"away_away_gf_last{w}"] = np.mean(ah["away_gf"][-w:]) if len(ah["away_gf"])>=w else 1.2
                row[f"away_away_ga_last{w}"] = np.mean(ah["away_ga"][-w:]) if len(ah["away_ga"])>=w else 1.2
                row[f"home_home_over_last{w}"] = np.mean(hh["home_over"][-w:]) if len(hh["home_over"])>=w else 0.5
                row[f"away_away_over_last{w}"] = np.mean(ah["away_over"][-w:]) if len(ah["away_over"])>=w else 0.5
        
        row["home_gf_avg"] = np.mean(hh["gf"]) if hh["gf"] else 1.4
        row["home_ga_avg"] = np.mean(hh["ga"]) if hh["ga"] else 1.4
        row["away_gf_avg"] = np.mean(ah["gf"]) if ah["gf"] else 1.2
        row["away_ga_avg"] = np.mean(ah["ga"]) if ah["ga"] else 1.2
        row["home_tot_avg"] = np.mean(hh["tot"]) if hh["tot"] else 2.6
        row["away_tot_avg"] = np.mean(ah["tot"]) if ah["tot"] else 2.6
        row["home_over_rate"] = np.mean(hh["over"]) if hh["over"] else 0.5
        row["away_over_rate"] = np.mean(ah["over"]) if ah["over"] else 0.5
        row["home_home_gf_avg"] = np.mean(hh["home_gf"]) if hh["home_gf"] else row["home_gf_avg"]
        row["home_home_ga_avg"] = np.mean(hh["home_ga"]) if hh["home_ga"] else row["home_ga_avg"]
        row["away_away_gf_avg"] = np.mean(ah["away_gf"]) if ah["away_gf"] else row["away_gf_avg"]
        row["away_away_ga_avg"] = np.mean(ah["away_ga"]) if ah["away_ga"] else row["away_ga_avg"]
        row["home_home_over_rate"] = np.mean(hh["home_over"]) if hh["home_over"] else row["home_over_rate"]
        row["away_away_over_rate"] = np.mean(ah["away_over"]) if ah["away_over"] else row["away_over_rate"]
        row["home_momentum"] = np.mean(hh["momentum"][-10:]) if len(hh["momentum"])>=10 else 0.5
        row["away_momentum"] = np.mean(ah["momentum"][-10:]) if len(ah["momentum"])>=10 else 0.5
        row["home_recent_form"] = np.mean(hh["recent_form"][-5:]) if len(hh["recent_form"])>=5 else 0.5
        row["away_recent_form"] = np.mean(ah["recent_form"][-5:]) if len(ah["recent_form"])>=5 else 0.5
        row["home_goal_trend"] = hh["goal_trend"][-1] if hh["goal_trend"] else 0
        row["away_goal_trend"] = ah["goal_trend"][-1] if ah["goal_trend"] else 0
        row["home_gf_var"] = np.var(hh["gf_last20"]) if len(hh["gf_last20"])>=2 else 0.5
        row["home_ga_var"] = np.var(hh["ga_last20"]) if len(hh["ga_last20"])>=2 else 0.5
        row["home_gf_std"] = np.std(hh["gf_last20"]) if len(hh["gf_last20"])>=2 else 0.5
        row["home_ga_std"] = np.std(hh["ga_last20"]) if len(hh["ga_last20"])>=2 else 0.5
        row["away_gf_var"] = np.var(ah["gf_last20"]) if len(ah["gf_last20"])>=2 else 0.5
        row["away_ga_var"] = np.var(ah["ga_last20"]) if len(ah["ga_last20"])>=2 else 0.5
        row["away_gf_std"] = np.std(ah["gf_last20"]) if len(ah["gf_last20"])>=2 else 0.5
        row["away_ga_std"] = np.std(ah["ga_last20"]) if len(ah["ga_last20"])>=2 else 0.5
        row["expected_goals_home"] = row["home_gf_avg"] + row["away_ga_avg"]
        row["expected_goals_away"] = row["away_gf_avg"] + row["home_ga_avg"]
        row["expected_goals_proxy"] = (row["expected_goals_home"]+row["expected_goals_away"])/2
        row["combined_over_rate"] = (row["home_over_rate"]+row["away_over_rate"])/2
        row["home_away_gf_diff"] = row["home_gf_avg"] - row["away_gf_avg"]
        row["home_away_ga_diff"] = row["home_ga_avg"] - row["away_ga_avg"]
        row["home_away_tot_diff"] = row["home_tot_avg"] - row["away_tot_avg"]
        row["home_away_over_diff"] = row["home_over_rate"] - row["away_over_rate"]
        row["form_ratio"] = row["home_momentum"]/(row["away_momentum"]+0.01)
        row["momentum_advantage"] = row["home_momentum"] - row["away_momentum"]
        row["home_clean_sheet_rate"] = np.mean(hh["home_clean"]) if hh["home_clean"] else 0.3
        row["away_clean_sheet_rate"] = np.mean(ah["away_clean"]) if ah["away_clean"] else 0.3
        row["home_btts_rate"] = np.mean(hh["btts_yes"]) if hh["btts_yes"] else 0.45
        row["away_btts_rate"] = np.mean(ah["btts_yes"]) if ah["btts_yes"] else 0.45
        row["combined_btts_rate"] = (row["home_btts_rate"]+row["away_btts_rate"])/2
        
        if self.use_b365_odds:
            ov = float(match.get('B365>2.5', np.nan))
            un = float(match.get('B365<2.5', np.nan))
            if pd.notna(ov) and pd.notna(un) and ov > 1:
                row['b365_over_odds'] = ov; row['b365_under_odds'] = un
                ov_prob = 1.0/ov; un_prob = 1.0/un
                row['b365_over_prob'] = ov_prob; row['b365_under_prob'] = un_prob
                fair_over = ov_prob/(ov_prob+un_prob)
                row['b365_fair_over'] = fair_over
                row['b365_margin'] = ov_prob+un_prob-1.0
                row['market_over_prob'] = fair_over
                row['b365_over_under_ratio'] = ov/(un+0.01)
            
            home_odds = float(match.get('B365H', np.nan))
            draw_odds = float(match.get('B365D', np.nan))
            away_odds = float(match.get('B365A', np.nan))
            if pd.notna(home_odds) and pd.notna(draw_odds) and pd.notna(away_odds):
                row['b365_home_odds'] = home_odds; row['b365_draw_odds'] = draw_odds; row['b365_away_odds'] = away_odds
                hp = 1.0/home_odds; dp = 1.0/draw_odds; ap = 1.0/away_odds
                ps = hp+dp+ap
                if ps > 0:
                    row['b365_home_prob'] = hp/ps; row['b365_draw_prob'] = dp/ps; row['b365_away_prob'] = ap/ps
                    row['b365_home_away_gap'] = hp-ap; row['b365_home_draw_gap'] = hp-dp; row['b365_draw_away_gap'] = dp-ap
        
        return row
    
    def step(self, match):
        self._update(match)


def get_best_config(league):
    """Load best config/pipeline for a league."""
    ld = MODEL_DIR / league
    configs = {}
    for key, cfg_name, pkl_name in [
        ('R1','optimized_config.json','optimized_pipeline.pkl'),
        ('R2','optimized_r2_config.json','optimized_r2_pipeline.pkl'),
    ]:
        cp = ld / cfg_name; pp = ld / pkl_name
        if cp.exists() and pp.exists():
            with open(cp) as f:
                configs[key] = (json.load(f), pkl_name)
    if not configs:
        return None, None, None, None
    best_key = max(configs.keys(), key=lambda k: configs[k][0].get('bet_acc', 0))
    cfg, pkl_name = configs[best_key]
    pipeline = joblib.load(ld / pkl_name)
    return cfg, pipeline, best_key, pkl_name


def evaluate_league(league):
    hp = HISTORICAL_DIR / f'{league}_history.csv'
    if not hp.exists():
        return None
    
    df = pd.read_csv(hp, low_memory=False)
    df['Date'] = pd.to_datetime(df['Date'], dayfirst=True, errors='coerce')
    df = df.dropna(subset=['Date','FTHG','FTAG']).sort_values('Date').reset_index(drop=True)
    
    recent_df = df[df['Date'] >= RECENT_SINCE].copy()
    if len(recent_df) < 3:
        return None
    
    cfg, pipeline, model_key, _ = get_best_config(league)
    if cfg is None:
        return None
    
    train_acc = cfg.get('overall_acc', 0)
    bet_acc = cfg.get('bet_acc', 0)
    train_bet_count = cfg.get('bet_count', 0)
    threshold = cfg.get('threshold', 0.6)
    
    use_b365 = league in ODDS_LEAGUES
    feature_list = BASE_FEATURES + (B365_OU + B365_1X2 if use_b365 else [])
    
    # Seed histories
    first_recent = recent_df['Date'].min()
    seed_df = df[df['Date'] < first_recent]
    fb = FeatureBuilder(use_b365_odds=use_b365)
    fb.seed(seed_df)
    
    y_true, y_pred, y_prob = [], [], []
    for _, match in recent_df.iterrows():
        row = fb.build(match)
        is_over = 1 if (float(match['FTHG'])+float(match['FTAG'])) > 2.5 else 0
        
        feat_vec = {f: row.get(f, 0.0) for f in feature_list}
        feat_df = pd.DataFrame([feat_vec])
        
        try:
            proba = pipeline.predict_proba(feat_df)[0]
            y_prob.append(proba[1])
            y_pred.append(1 if proba[1] >= threshold else 0)
            y_true.append(is_over)
        except Exception:
            try:
                proba = pipeline.predict_proba(feat_df.values)[0]
                y_prob.append(proba[1])
                y_pred.append(1 if proba[1] >= threshold else 0)
                y_true.append(is_over)
            except Exception:
                pass
        
        fb.step(match)
    
    y_true = np.array(y_true)
    y_pred = np.array(y_pred)
    y_prob = np.array(y_prob)
    
    n_total = len(y_true)
    if n_total == 0:
        return None
    
    n_correct = int((y_pred == y_true).sum())
    overall_acc = n_correct / n_total
    
    hc_mask = (y_prob >= threshold) | (y_prob <= (1 - threshold))
    n_hc = int(hc_mask.sum())
    hc_acc = (y_pred[hc_mask] == y_true[hc_mask]).mean() if n_hc > 0 else 0.0
    
    strict_mask = (y_prob >= 0.7) | (y_prob <= 0.3)
    n_strict = int(strict_mask.sum())
    strict_acc = (y_pred[strict_mask] == y_true[strict_mask]).mean() if n_strict > 0 else 0.0
    
    overfit_gap = bet_acc - overall_acc
    
    return {
        'league': league,
        'league_name': LEAGUE_NAMES.get(league, league),
        'model_key': model_key,
        'train_overall_acc': float(train_acc),
        'train_bet_acc': float(bet_acc),
        'train_bet_count': int(train_bet_count),
        'recent_overall_acc': float(overall_acc),
        'recent_hc_acc': float(hc_acc),
        'recent_strict_acc': float(strict_acc),
        'n_recent': int(n_total),
        'n_high_conf': int(n_hc),
        'n_strict': int(n_strict),
        'threshold': float(threshold),
        'overfit_gap': float(overfit_gap),
        'feature_count': len(feature_list),
    }


if __name__ == '__main__':
    print("="*105)
    print("1year-V1 Optimized Models: Recent Data Evaluation (2025-12-01 onwards)")
    print("="*105)
    
    results = []
    for league in LEAGUES:
        print(f"\nEvaluating {league} ({LEAGUE_NAMES.get(league, league)})...", end=' ', flush=True)
        try:
            r = evaluate_league(league)
            if r:
                results.append(r)
                print(f"Done. recent={r['recent_overall_acc']:.1%}, hc={r['recent_hc_acc']:.1%}, n={r['n_recent']}, feat={r['feature_count']}")
            else:
                print("No data or no model")
        except Exception as e:
            import traceback
            print(f"ERROR: {e}")
            traceback.print_exc()
    
    print("\n" + "="*105)
    print("SUMMARY TABLE")
    print("="*105)
    
    header = f"{'League':<8} {'Name':<8} {'Model':>5} {'Train%':>7} {'TrainHC%':>9} {'Recent%':>8} {'HC%':>7} {'Strict%':>7} {'N':>4} {'N_HC':>5} {'N_ST':>4} {'Overfit':>8}"
    print(header)
    print("-"*105)
    
    for r in sorted(results, key=lambda x: x['recent_overall_acc'], reverse=True):
        of = "⚠️ YES" if r['overfit_gap'] > 0.10 else ("△ mild" if r['overfit_gap'] > 0.05 else "✓ OK")
        print(f"{r['league']:<8} {r['league_name']:<8} {r['model_key']:>5} "
              f"{r['train_overall_acc']:>7.1%} "
              f"{r['train_bet_acc']:>9.1%} "
              f"{r['recent_overall_acc']:>8.1%} "
              f"{r['recent_hc_acc']:>7.1%} "
              f"{r['recent_strict_acc']:>7.1%} "
              f"{r['n_recent']:>4} "
              f"{r['n_high_conf']:>5} "
              f"{r['n_strict']:>4} "
              f"{of:>8}")
    
    print("-"*105)
    
    if results:
        avg_recent = np.mean([r['recent_overall_acc'] for r in results])
        avg_hc = np.mean([r['recent_hc_acc'] for r in results if r['n_high_conf'] > 0])
        overfitting = [r['league'] for r in results if r['overfit_gap'] > 0.10]
        print(f"\nAverage recent accuracy: {avg_recent:.1%} ({len(results)} leagues)")
        print(f"Average HC accuracy: {avg_hc:.1%}")
        print(f"Leagues with significant overfitting: {overfitting}")
    
    out_path = REPO_ROOT / 'eval_recent_results.json'
    with open(out_path, 'w') as f:
        json.dump(results, f, indent=2, default=str)
    print(f"\nResults saved to {out_path}")
