"""
Grid search: RF model predictions x outcomes x filter types x thresholds.
Compares:
  1. Pure CLV (Pinnacle closing)
  2. RF pA >= threshold + CLV > 5%  (current)
  3. RF pA >= threshold only (no CLV gate)
  4. RF implied edge vs B365 (model p > 1/B365 by X%)
Across Home, Draw, Away outcomes.
"""
import pandas as pd, numpy as np, glob, os, warnings
from sklearn.ensemble import RandomForestClassifier
from sklearn.calibration import CalibratedClassifierCV
warnings.filterwarnings('ignore')

START  = 1000.0
KM     = 0.25
MAXPCT = 0.25

def remove_margin(h, d, a):
    ov = 1/h + 1/d + 1/a
    return 1/h/ov, 1/d/ov, 1/a/ov

def kelly_compound(bets_df, odds_col, p_col, result, km=KM, maxpct=MAXPCT):
    br = START; peak = br; max_dd = 0; log_rets = []
    for _, row in bets_df.iterrows():
        p = row[p_col]; b = row[odds_col] - 1
        kf = max((p*b - (1-p))/b, 0) if b > 0 else 0
        frac = min(kf * km, maxpct)
        won = row['FTR'] == result
        pnl_unit = b if won else -1.0
        br_new = max(br + frac*br*pnl_unit, 0.01)
        peak = max(peak, br_new)
        max_dd = max(max_dd, (peak-br_new)/peak)
        log_rets.append(np.log(br_new/br))
        br = br_new
    if len(bets_df) < 2:
        return br, 0, max_dd, 0
    yrs = max((bets_df['Date'].max()-bets_df['Date'].min()).days/365, 0.1)
    cagr = (br/START)**(1/yrs)-1
    sharpe = np.mean(log_rets)/np.std(log_rets)*np.sqrt(252) if np.std(log_rets)>0 else 0
    return br, cagr, max_dd, sharpe

# ── Load odds data ────────────────────────────────────────────────────────
print("Loading odds...")
files = glob.glob('E:/soccer/league_data/*.csv')
dfs = []
for f in files:
    try:
        d = pd.read_csv(f, encoding='utf-8-sig')
        d['div'] = os.path.basename(f).split('_')[0]
        d['season_str'] = os.path.basename(f).replace('.csv','').split('_')[1]
        dfs.append(d)
    except: pass
odds = pd.concat(dfs, ignore_index=True)
odds = odds.dropna(subset=['B365H','B365D','B365A','PSCH','PSCD','PSCA','FTR','Date'])
odds['Date'] = pd.to_datetime(odds['Date'], dayfirst=True, errors='coerce')
odds = odds.dropna(subset=['Date']).sort_values('Date').reset_index(drop=True)
odds['season_yr'] = odds['Date'].dt.year.apply(lambda y: y if odds.loc[odds['Date'].dt.year==y].index[0] >= 0 else y)
# Numeric season from season_str
def ss_to_yr(s):
    try: return 2000 + int(s[:2])
    except: return 0
odds['season_yr'] = odds['season_str'].apply(ss_to_yr)

# Pinnacle fair probs
res = np.array([remove_margin(r.PSCH, r.PSCD, r.PSCA) for _, r in odds.iterrows()])
odds['pin_pH'], odds['pin_pD'], odds['pin_pA'] = res[:,0], res[:,1], res[:,2]
odds['edge_H'] = (odds['B365H'] - 1/odds['pin_pH']) / (1/odds['pin_pH'])
odds['edge_D'] = (odds['B365D'] - 1/odds['pin_pD']) / (1/odds['pin_pD'])
odds['edge_A'] = (odds['B365A'] - 1/odds['pin_pA']) / (1/odds['pin_pA'])

# ── Load model features ───────────────────────────────────────────────────
print("Loading features...")
try:
    feat = pd.read_csv('E:/soccer/raw_api/match_features.csv')
    feat['date'] = pd.to_datetime(feat['date'])
    w_cols = [c for c in feat.columns if '_w5' in c or '_w10' in c]
    hf = feat[feat['is_home']==1][['fixture_id','date','div','season','home_team','away_team']+w_cols].copy()
    hf.columns = ['fixture_id','date','div','season','home_team','away_team']+[f'h_{c}' for c in w_cols]
    af = feat[feat['is_home']==0][['fixture_id']+w_cols].copy()
    af.columns = ['fixture_id']+[f'a_{c}' for c in w_cols]
    matches = hf.merge(af, on='fixture_id')
    feat_cols = [f'h_{c}' for c in w_cols] + [f'a_{c}' for c in w_cols]
    for hc, ac in zip([f'h_{c}' for c in w_cols], [f'a_{c}' for c in w_cols]):
        matches['diff_'+hc[2:]] = matches[hc] - matches[ac]
    diff_cols = ['diff_'+c for c in w_cols]
    X_cols = feat_cols + diff_cols
    HAS_FEATURES = True
    print(f"  {len(matches):,} matches, {len(X_cols)} features")
except Exception as e:
    print(f"  No features: {e}")
    HAS_FEATURES = False

# ── Merge features into odds ──────────────────────────────────────────────
if HAS_FEATURES:
    import unicodedata, re
    def norm(s):
        s = str(s).lower().strip()
        s = unicodedata.normalize('NFKD', s).encode('ascii','ignore').decode('ascii')
        s = re.sub(r'\b(fc|afc|sc|cf|cd|sd|ss|us|uc|rc|ac|as|sv|1\.)\b','',s).strip()
        return re.sub(r'\s+',' ',s).strip()
    matches['h_norm'] = matches['home_team'].apply(norm)
    matches['a_norm'] = matches['away_team'].apply(norm)
    odds['h_norm']    = odds['HomeTeam'].apply(norm)
    odds['a_norm']    = odds['AwayTeam'].apply(norm)
    odds['date_str']  = odds['Date'].dt.strftime('%Y-%m-%d')
    matches['date_str'] = matches['date'].dt.strftime('%Y-%m-%d')
    merged = matches.merge(
        odds[['date_str','div','h_norm','a_norm','B365H','B365D','B365A',
              'pin_pH','pin_pD','pin_pA','edge_H','edge_D','edge_A','FTR',
              'Date','season_yr']],
        on=['date_str','div','h_norm','a_norm'], how='inner')
    merged = merged.dropna(subset=X_cols)
    print(f"  {len(merged):,} merged")

# ── Walk-forward: predict H, D, A ────────────────────────────────────────
OOS_SEASONS = [2021, 2022, 2023, 2024, 2025]

if HAS_FEATURES:
    print("\nWalk-forward training (H, D, A)...")
    all_preds = []
    for test_yr in OOS_SEASONS:
        train = merged[merged['season'] < test_yr].dropna(subset=X_cols)
        test  = merged[merged['season'] == test_yr].dropna(subset=X_cols)
        if len(train) < 500 or len(test) < 50:
            continue
        X_tr, X_te = train[X_cols].values, test[X_cols].values
        for outcome, label in [('H','pH'), ('D','pD'), ('A','pA')]:
            y_tr = (train['FTR'] == outcome).astype(int).values
            rf = RandomForestClassifier(n_estimators=200, max_depth=10,
                                        min_samples_leaf=20, random_state=42, n_jobs=-1)
            cal = CalibratedClassifierCV(rf, cv=3, method='isotonic')
            cal.fit(X_tr, y_tr)
            proba = cal.predict_proba(X_te)[:,1]
            for i, (_, row) in enumerate(test.iterrows()):
                all_preds.append({'fixture_id': row['fixture_id'],
                                  'outcome': outcome, f'model_p': proba[i]})
        print(f"  Season {test_yr}: done")

    # Pivot predictions wide
    pred_df = pd.DataFrame(all_preds)
    pred_wide = pred_df.pivot_table(index='fixture_id', columns='outcome',
                                    values='model_p', aggfunc='first').reset_index()
    pred_wide.columns.name = None
    pred_wide = pred_wide.rename(columns={'H':'model_pH','D':'model_pD','A':'model_pA'})
    merged2 = merged.merge(pred_wide, on='fixture_id', how='inner')
    print(f"  {len(merged2):,} rows with model predictions")

# ── Comparison grid ───────────────────────────────────────────────────────
print("\n=== Strategy Comparison ===")
hdr = '{:<32} {:>5} {:>5} {:>8} {:>10} {:>7} {:>7}'
print(hdr.format('Strategy','Bets','WR%','AvgOdds','Final BR','CAGR','Sharpe'))
print('-'*82)

def show(name, bets, odds_col, p_col, result):
    if len(bets) < 5:
        return
    wr = (bets['FTR']==result).mean()*100
    br, cagr, dd, sh = kelly_compound(bets, odds_col, p_col, result)
    print('{:<32} {:>5} {:>4.0f}% {:>8.2f} {:>10,.0f} {:>+6.0f}% {:>7.2f}'.format(
        name, len(bets), wr, bets[odds_col].mean(), br, cagr*100, sh))

oos = odds[odds['Date'] >= '2021-07-01']

# 1. Pure CLV baselines
for outcome, ec, oc, pc in [('Away','edge_A','B365A','pin_pA'),
                              ('Home','edge_H','B365H','pin_pH'),
                              ('Draw','edge_D','B365D','pin_pD')]:
    for thresh in [0.05, 0.08]:
        sub = oos[oos[ec] >= thresh]
        show(f'Pure CLV {outcome} >{thresh*100:.0f}%', sub, oc, pc, outcome[0])

print()

if HAS_FEATURES:
    # 2. RF pA threshold only
    for outcome, mc, oc, pc in [('Away','model_pA','B365A','pin_pA'),
                                  ('Home','model_pH','B365H','pin_pH'),
                                  ('Draw','model_pD','B365D','pin_pD')]:
        for thresh in [0.30, 0.35, 0.40]:
            sub = merged2[merged2[mc] >= thresh]
            if len(sub) < 5: continue
            show(f'RF p{outcome[0]}>={thresh:.2f} only', sub, oc, pc, outcome[0])
    print()

    # 3. RF + CLV combined
    for outcome, mc, ec, oc, pc in [('Away','model_pA','edge_A','B365A','pin_pA'),
                                      ('Home','model_pH','edge_H','B365H','pin_pH'),
                                      ('Draw','model_pD','edge_D','B365D','pin_pD')]:
        for pa_t in [0.30, 0.35]:
            sub = merged2[(merged2[mc] >= pa_t) & (merged2[ec] >= 0.05)]
            if len(sub) < 5: continue
            show(f'RF+CLV {outcome} p>={pa_t:.2f}', sub, oc, pc, outcome[0])
    print()

    # 4. RF implied edge vs B365 (model p > 1/B365 by X%)
    print('--- RF implied edge (model p > 1/B365) ---')
    for outcome, mc, oc, pc in [('Away','model_pA','B365A','model_pA'),
                                  ('Home','model_pH','B365H','model_pH'),
                                  ('Draw','model_pD','B365D','model_pD')]:
        merged2[f'rf_edge_{outcome[0]}'] = merged2[mc] - 1/merged2[oc]
        for thresh in [0.02, 0.05, 0.08]:
            sub = merged2[merged2[f'rf_edge_{outcome[0]}'] >= thresh]
            if len(sub) < 5: continue
            show(f'RF edge {outcome} >{thresh*100:.0f}%', sub, oc, mc, outcome[0])
