#!/usr/bin/env python3
"""Goal Number 1year V4 models predictor"""
import json as _json, warnings as _warnings, re as _re
from pathlib import Path
from collections import defaultdict
from datetime import datetime, timezone, timedelta
import numpy as np
import pandas as pd
import joblib
import requests

_warnings.filterwarnings('ignore')

REPO = Path(__file__).resolve().parents[1]
HIST = REPO / 'historical_data'
MDIR = REPO / 'models' / '1year_V1_optimized'
ODIR = REPO / 'experiments' / 'goal_number_1year'
ODIR.mkdir(parents=True, exist_ok=True)
ARTIFACTS = REPO / 'artifacts'
ARTIFACTS.mkdir(parents=True, exist_ok=True)
APIKEY = __import__('os').environ.get('API_FOOTBALL_KEY', '57b64e857ebeffdc156f0c41d43c7894')
BASE = 'https://v3.football.api-sports.io'

LEAGUES = {
    'a1':(16,'澳超'),'br1':(71,'巴甲'),'cz1':(314,'捷克甲'),
    'd1':(78,'德甲'),'e0':(39,'英超'),'e1':(40,'英甲'),
    'e2':(41,'英冠'),'e2n':(62,'英乙'),'f1':(61,'法甲'),
    'g1':(197,'希腊超'),'i1':(135,'意甲'),'p1':(94,'葡超'),
    'sc1':(381,'苏超'),'tr1':(203,'土超'),
}

def api_get(ep, params):
    try:
        r = requests.get(f'{BASE}/{ep}', params=params,
                         headers={'x-apisports-key': APIKEY}, timeout=20)
        return r.json()
    except Exception as e:
        print(f'  API error: {e}')
        return {}

def fetch_upcoming(api_id, name, hours=168):
    now = datetime.now(timezone.utc)
    for season in [2025, 2026]:
        data = api_get('fixtures', {
            'league': api_id, 'season': season,
            'next': 30, 'timezone': 'Asia/Shanghai'
        })
        if data.get('results', 0) > 0:
            break
    matches = []
    for resp in data.get('response', []):
        fix = resp['fixture']
        teams = resp['teams']
        date = fix.get('date', '')[:19]
        status = fix.get('status', {}).get('short', '')
        if status not in ('NS',):
            continue
        if not date:
            continue
        try:
            dt = datetime.fromisoformat(date.replace('Z', '+00:00'))
            if dt.tzinfo is None:
                dt = dt.replace(tzinfo=timezone.utc)
            delta = (dt - now).total_seconds() / 3600
            if delta > hours or delta < 0:
                continue
        except:
            pass
        home = teams['home']['name']
        away = teams['away']['name']
        fid = fix['id']
        oo = uo = None
        od = api_get('odds', {'fixture': fid})
        for bp in od.get('response', []):
            for bm in bp.get('bookmakers', []):
                for bet in bm.get('bets', []):
                    if 'Over/Under' in str(bet.get('name', '')):
                        for val in bet.get('values', []):
                            v = str(val.get('value', ''))
                            o = float(val.get('odd', 0))
                            if o <= 1:
                                continue
                            m = _re.match(r'^(Over|Under)\s+([0-9.]+)$', v, _re.I)
                            if m and abs(float(m.group(2)) - 2.5) < 0.01:
                                if m.group(1).lower() == 'over':
                                    oo = o
                                else:
                                    uo = o
        matches.append({
            'fixture_id': fid, 'date': date, 'HomeTeam': home, 'AwayTeam': away,
            'league_name': name, 'over_odds': oo, 'under_odds': uo
        })
    return matches

def fetch_csv(code):
    p = HIST / f'{code}_history.csv'
    if not p.exists():
        return pd.DataFrame()
    try:
        df = pd.read_csv(p, low_memory=False)
        df['Date'] = pd.to_datetime(df['Date'], dayfirst=True, errors='coerce')
        df = df.dropna(subset=['Date', 'FTHG', 'FTAG'])
        df = df.sort_values('Date').reset_index(drop=True)
        df['FTHG'] = pd.to_numeric(df['FTHG'], errors='coerce')
        df['FTAG'] = pd.to_numeric(df['FTAG'], errors='coerce')
        df['tot'] = df['FTHG'] + df['FTAG']
        cutoff = datetime.now() - timedelta(days=730)
        df = df[df['Date'] < cutoff]
        return df
    except:
        return pd.DataFrame()

class FB:
    """FeatureBuilder - builds goal scoring features from historical match data."""
    def __init__(self):
        # gf=goals for, ga=goals against, tot=total, over=over 2.5
        # gf3/ga3/ov3 = last 3, gf5/ga5/ov5 = last 5, etc.
        # hgf/hga/hov = home goals for/against/over at home
        # agf/aga/aov = away goals for/against/over at away
        # gf/ga/over = all matches (rolling window)
        self.th = defaultdict(lambda: {
            "gf": [], "ga": [], "tot": [], "over": [],
            "gf3": [], "ga3": [], "ov3": [],
            "gf5": [], "ga5": [], "ov5": [],
            "gf10": [], "ga10": [], "ov10": [],
            "gf15": [], "ga15": [], "ov15": [],
            "gf20": [], "ga20": [], "ov20": [],
            "hgf": [], "hga": [], "hov": [],
            "agf": [], "aga": [], "aov": [],
            "hc": [], "ac": [], "bt": [],
            "mom": [], "rf": [], "gt": [],
        })

    def seed(self, df):
        for _, r in df.iterrows():
            self._step(r)

    def _step(self, r):
        h = str(r.get('HomeTeam', ''))
        a = str(r.get('AwayTeam', ''))
        hh, aa = self.th[h], self.th[a]
        try:
            hg = float(r['FTHG']); ag = float(r['FTAG']); tot = hg + ag
        except:
            return
        io = 1 if tot > 2.5 else 0
        # Rolling windows
        for arr in [hh, aa]:
            arr["gf"].append(tot); arr["ga"].append(tot); arr["tot"].append(tot); arr["over"].append(io)
            arr["gf3"].append(tot); arr["ga3"].append(tot); arr["ov3"].append(io)
            arr["gf5"].append(tot); arr["ga5"].append(tot); arr["ov5"].append(io)
            arr["gf10"].append(tot); arr["ga10"].append(tot); arr["ov10"].append(io)
            arr["gf15"].append(tot); arr["ga15"].append(tot); arr["ov15"].append(io)
            arr["gf20"].append(tot); arr["ga20"].append(tot); arr["ov20"].append(io)
            arr["hc"].append(1 if ag == 0 else 0)
            arr["ac"].append(1 if hg == 0 else 0)
            arr["bt"].append(1 if (hg > 0 and ag > 0) else 0)
        # Home/Away specific
        hh["hgf"].append(hg); hh["hga"].append(ag); hh["hov"].append(io)
        aa["agf"].append(ag); aa["aga"].append(hg); aa["aov"].append(io)
        # Momentum
        for arr in [hh, aa]:
            if len(arr["gf"]) >= 2:
                r5 = np.mean(arr["gf"][-5:]); o5 = np.mean(arr["gf"][:-5])
                arr["mom"].append(r5 - o5)
                r5o = np.mean(arr["over"][-5:]); o5o = np.mean(arr["over"][:-5])
                arr["rf"].append(r5o - o5o)
            if len(arr["gf10"]) >= 2:
                r = arr["gf10"][-5:]; o = arr["gf10"][:-5]
                arr["gt"].append(np.mean(r) - np.mean(o) if len(o) else 0)

    def build(self, home, away):
        hh, aa = self.th[home], self.th[away]
        def mn(v, n):
            v = list(v)
            return float(np.mean(v[-n:])) if len(v) >= n else (float(np.mean(v)) if v else 1.4)
        def on(v, n):
            v = list(v)
            return float(np.mean(v[-n:])) if len(v) >= n else (float(np.mean(v)) if v else 0.5)
        def std(v):
            v = list(v)
            return float(np.std(v)) if len(v) >= 2 else 0.5
        def var(v):
            v = list(v)
            return float(np.var(v)) if len(v) >= 2 else 0.5
        d = {}
        for p, s in [('home', hh), ('away', aa)]:
            for n in [3, 5, 10, 15, 20]:
                gk = f"gf{n}"; gak = f"ga{n}"; ok = f"ov{n}"
                d[f"{p}_gf_{n}"] = mn(s[gk], n)
                d[f"{p}_ga_{n}"] = mn(s[gak], n)
                d[f"{p}_ov_{n}"] = on(s[ok], n)
            for n in [5, 10, 15, 20]:
                d[f"{p}_hgf_{n}"] = mn(s["hgf"], n) if s["hgf"] else d.get(f"{p}_gf_{n}", 1.4)
                d[f"{p}_hga_{n}"] = mn(s["hga"], n) if s["hga"] else d.get(f"{p}_ga_{n}", 1.4)
                d[f"{p}_hov_{n}"] = on(s["hov"], n) if s["hov"] else d.get(f"{p}_ov_{n}", 0.5)
                d[f"{p}_agf_{n}"] = mn(s["agf"], n) if s["agf"] else d.get(f"{p}_gf_{n}", 1.4)
                d[f"{p}_aga_{n}"] = mn(s["aga"], n) if s["aga"] else d.get(f"{p}_ga_{n}", 1.4)
                d[f"{p}_aov_{n}"] = on(s["aov"], n) if s["aov"] else d.get(f"{p}_ov_{n}", 0.5)
            d[f"{p}_gavg"] = np.mean(s["gf"]) if s["gf"] else 1.4
            d[f"{p}_gavga"] = np.mean(s["ga"]) if s["ga"] else 1.4
            d[f"{p}_tavg"] = np.mean(s["tot"]) if s["tot"] else 2.6
            d[f"{p}_orate"] = np.mean(s["over"]) if s["over"] else 0.5
            d[f"{p}_gstd"] = std(s["gf"])
            d[f"{p}_gastd"] = std(s["ga"])
            d[f"{p}_gvar"] = var(s["gf"])
            d[f"{p}_gavar"] = var(s["ga"])
            d[f"{p}_hgfavg"] = np.mean(s["hgf"]) if s["hgf"] else d[f"{p}_gavg"]
            d[f"{p}_hgavga"] = np.mean(s["hga"]) if s["hga"] else d[f"{p}_gavga"]
            d[f"{p}_hovrat"] = np.mean(s["hov"]) if s["hov"] else d[f"{p}_orate"]
            d[f"{p}_agfavg"] = np.mean(s["agf"]) if s["agf"] else d[f"{p}_gavg"]
            d[f"{p}_agavga"] = np.mean(s["aga"]) if s["aga"] else d[f"{p}_gavga"]
            d[f"{p}_aovrat"] = np.mean(s["aov"]) if s["aov"] else d[f"{p}_orate"]
            d[f"{p}_mom"] = np.mean(s["mom"][-10:]) if len(s["mom"]) >= 10 else 0.0
            d[f"{p}_rf"] = np.mean(s["rf"][-5:]) if len(s["rf"]) >= 5 else 0.0
            d[f"{p}_gt"] = s["gt"][-1] if s["gt"] else 0.0
            d[f"{p}_cl"] = np.mean(s["hc"]) if s["hc"] else 0.3
            d[f"{p}_bt"] = np.mean(s["bt"]) if s["bt"] else 0.45
        d["eg_h"] = d["home_gavg"] + d["away_agavga"]
        d["eg_a"] = d["away_agfavg"] + d["home_hgavga"]
        d["eg_p"] = (d["eg_h"] + d["eg_a"]) / 2
        d["cor"] = (d["home_orate"] + d["away_orate"]) / 2
        d["hgfd"] = d["home_gavg"] - d["away_agfavg"]
        d["hgad"] = d["home_gavga"] - d["away_agavga"]
        d["htd"] = d["home_tavg"] - d["away_tavg"]
        d["hod"] = d["home_orate"] - d["away_orate"]
        d["fr"] = d["home_mom"] / (d["away_mom"] + 0.001)
        d["ma"] = d["home_mom"] - d["away_mom"]
        d["ccl"] = (d["home_cl"] + d["away_cl"]) / 2
        d["cbt"] = (d["home_bt"] + d["away_bt"]) / 2
        return d

def get_model(code):
    ld = MDIR / code
    if not ld.exists():
        return None, None, None
    for cn, pn in [
        ('optimized_r2_config.json', 'optimized_r2_pipeline.pkl'),
        ('optimized_config.json', 'optimized_pipeline.pkl'),
    ]:
        cp, pp = ld / cn, ld / pn
        if cp.exists() and pp.exists():
            try:
                with open(cp) as f:
                    cfg = _json.load(f)
                pipe = joblib.load(pp)
                return cfg, pipe, cfg.get('threshold', 0.6)
            except:
                pass
    return None, None, None

def predict(hours=168):
    rows = []
    stats = {}
    for code, (api_id, name) in LEAGUES.items():
        print(f'\n{name}({code})...')
        cfg, pipe, th = get_model(code)
        if pipe is None:
            print('  No model')
            continue
        ba = cfg.get('bet_acc', 0)
        mn = cfg.get('model_name', '?')
        print(f'  {mn} th={th} acc={ba:.1%}')
        fb = FB()
        sdf = fetch_csv(code)
        if len(sdf) > 0:
            fb.seed(sdf)
            print(f'  Seeded {len(sdf)} rows')
        else:
            print('  No CSV data')
        ups = fetch_upcoming(api_id, name, hours=hours)
        print(f'  Upcoming: {len(ups)}')
        if not ups:
            continue
        fn = list(pipe.feature_names_in_)
        nf = len(fn)
        for m in ups:
            fd = fb.build(m['HomeTeam'], m['AwayTeam'])
            xv = np.zeros(nf, dtype=np.float64)
            for i, f in enumerate(fn):
                xv[i] = float(fd.get(f, 0.0))
            try:
                pred = int(pipe.predict([xv])[0])
                pr = pipe.predict_proba([xv])[0]
            except Exception as e:
                print(f'  Err {m["HomeTeam"]}: {e}')
                continue
            cs = pipe.classes_
            ci = {c: i for i, c in enumerate(cs)}
            op = float(pr[ci.get(1, 0)])
            up = float(pr[ci.get(0, 1)])
            conf = abs(op - 0.5) * 2
            pl = '大球' if pred == 1 else '小球'
            oo, uo = m.get('over_odds'), m.get('under_odds')
            rows.append({
                '开赛时间': m['date'][:16].replace('T', ' '),
                '联赛': name, '主队': m['HomeTeam'], '客队': m['AwayTeam'],
                '大球赔率': oo, '小球赔率': uo,
                '大球概率': round(op, 3), '小球概率': round(up, 3),
                '预测方向': pl, '置信度': round(conf, 3),
                '是否达阈值': '是' if conf >= th else '否',
                '推荐等级': '⭐⭐⭐' if conf >= th else ('⭐⭐' if conf >= th * 0.8 else '⭐'),
                '模型类型': f'V4-{mn}', '模型回测命中率': f'{ba:.1%}', '联赛代码': code,
            })
        stats[code] = {'league': name, 'model': mn, 'th': th, 'acc': ba, 'up': len(ups)}

    df = pd.DataFrame(rows)
    # Save to artifacts/ for web dashboard
    oc = ARTIFACTS / f'goal_number_1year_{hours}h.csv'
    ox = ARTIFACTS / f'goal_number_1year_{hours}h.xlsx'
    if len(df) > 0:
        df.to_csv(oc, index=False, encoding='utf-8-sig')
        try:
            df.to_excel(ox, index=False)
        except:
            pass
        print(f'\nSaved {len(df)} to {oc}')
    else:
        print('\nNo predictions!')
    sm = {
        'generated_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
        'hours': hours, 'total': len(df), 'stats': stats
    }
    with open(ODIR / 'summary.json', 'w', encoding='utf-8') as f:
        _json.dump(sm, f, ensure_ascii=False, indent=2)
    return df

if __name__ == '__main__':
    import argparse
    p = argparse.ArgumentParser()
    p.add_argument('--hours', type=int, default=168)
    args = p.parse_args()
    predict(args.hours)
