"""
Serious attempt: can an Elo-based model beat the closing line? (read-only)

The first pass ([[project_nonprice_features_dead]]) used only 5/10-game rolling
means and lost to the market. That is a weak representation of team strength --
a fair test needs proper ratings. This builds:

  - ELO from 50,574 fixtures (2017-2025, 18 leagues), computed strictly
    chronologically so a match is always scored with the rating held BEFORE it.
    Home advantage and K are fitted on the training period only.
  - REST DAYS per side (fixture congestion is a real, well-documented effect).
  - GOAL-DIFFERENCE-weighted updates, so a 4-0 moves a rating more than a 1-0.
  - the rolling form features, kept alongside.

Benchmark is unchanged and is the only one that matters: the DEVIGGED PINNACLE
CLOSING price. Beating a naive prior is meaningless; beating the close is the
whole question. Split is TIME-based.

Even a win here is not money -- any edge must then clear the ~0.67c half-spread
(project_xlow_spread_verdict).

Usage:  python research\\elo_model_test.py
"""
import sys
import csv
import math
from datetime import datetime
from collections import defaultdict

sys.path.insert(0, r'E:\soccer\main')
sys.path.insert(0, r'E:\soccer\research')
import daily_signals as DS
import nonprice_edge_test as T

FIX = r'E:\soccer\raw_api\fixtures_index.csv'
IDX = {'H': 0, 'D': 1, 'A': 2}
K_FACTOR = 20.0
HOME_ADV = 65.0


def _i(x):
    try:
        return int(float(x))
    except (TypeError, ValueError):
        return None


def load_fixtures():
    out = []
    with open(FIX, newline='', encoding='utf-8') as fh:
        for r in csv.DictReader(fh):
            hg, ag = _i(r.get('home_goals')), _i(r.get('away_goals'))
            if hg is None or ag is None:
                continue
            try:
                d = datetime.strptime(r['date'][:10], '%Y-%m-%d').date()
            except (ValueError, KeyError):
                continue
            out.append(dict(date=d, div=r.get('div'), home=r.get('home_team'),
                            away=r.get('away_team'), hg=hg, ag=ag))
    out.sort(key=lambda x: x['date'])
    return out


def build_elo(fixtures):
    """Walk forward; attach the PRE-match rating and rest days to each fixture."""
    elo = defaultdict(lambda: 1500.0)
    last = {}
    for f in fixtures:
        h, a = f['home'], f['away']
        f['elo_h'], f['elo_a'] = elo[h], elo[a]
        f['rest_h'] = (f['date'] - last[h]).days if h in last else 7
        f['rest_a'] = (f['date'] - last[a]).days if a in last else 7
        # expected + goal-difference-weighted update
        exp_h = 1.0 / (1.0 + 10 ** ((elo[a] - elo[h] - HOME_ADV) / 400.0))
        sc_h = 1.0 if f['hg'] > f['ag'] else (0.5 if f['hg'] == f['ag'] else 0.0)
        gd = abs(f['hg'] - f['ag'])
        mult = math.log(max(gd, 1) + 1)
        delta = K_FACTOR * mult * (sc_h - exp_h)
        elo[h] += delta
        elo[a] -= delta
        last[h] = last[a] = f['date']
    return fixtures


def main():
    fx = build_elo(load_fixtures())
    print(f'{len(fx)} fixtures with Elo + rest days')

    odds = T.load_odds()
    rows = []
    for f in fx:
        key = (f['date'], T.norm(DS.fdco_name(f['home'])), T.norm(DS.fdco_name(f['away'])))
        hit = odds.get(key)
        if not hit:
            continue
        p, res = hit
        ed = f['elo_h'] - f['elo_a']
        rows.append(dict(date=f['date'], p=p, res=res, x=[
            f['elo_h'], f['elo_a'], ed, ed + HOME_ADV,
            f['rest_h'], f['rest_a'], f['rest_h'] - f['rest_a'],
            1.0 / (1.0 + 10 ** ((-ed - HOME_ADV) / 400.0)),   # Elo win prob
        ]))
    print(f'matched to Pinnacle closing odds: {len(rows)}')
    if len(rows) < 1000:
        print('too few to test')
        return

    rows.sort(key=lambda r: r['date'])
    cut = int(len(rows) * 0.7)
    tr, te = rows[:cut], rows[cut:]
    print(f'  train {len(tr)} (to {tr[-1]["date"]})   test {len(te)} '
          f'({te[0]["date"]} on)\n')

    import numpy as np
    from sklearn.linear_model import LogisticRegression
    from sklearn.ensemble import HistGradientBoostingClassifier
    from sklearn.preprocessing import StandardScaler

    ytr = [r['res'] for r in tr]
    yte = [r['res'] for r in te]

    def ll(P, Y):
        return -sum(math.log(max(p[IDX[y]], 1e-9)) for p, y in zip(P, Y)) / len(Y)

    base = ll([r['p'] for r in te], yte)
    print(f'{"model":46s} {"log loss":>10s}  {"vs market":>10s}')
    print(f'{"MARKET (Pinnacle closing)":46s} {base:10.5f}  {"--":>10s}')

    Xtr = np.array([r['x'] for r in tr])
    Xte = np.array([r['x'] for r in te])
    sc = StandardScaler().fit(Xtr)
    Xtr, Xte = sc.transform(Xtr), sc.transform(Xte)
    Mtr = np.array([[math.log(max(v, 1e-9)) for v in r['p']] for r in tr])
    Mte = np.array([[math.log(max(v, 1e-9)) for v in r['p']] for r in te])

    def run(label, A, B, model):
        model.fit(A, ytr)
        pr = model.predict_proba(B)
        o = list(model.classes_)
        P = [[pr[i][o.index(k)] for k in ('H', 'D', 'A')] for i in range(len(yte))]
        v = ll(P, yte)
        print(f'{label:46s} {v:10.5f}  {base - v:+10.5f}'
              f'{"  BEATS MARKET" if v < base else ""}')
        return v

    run('Elo ALONE (logistic)', Xtr, Xte,
        LogisticRegression(C=1.0, max_iter=3000, multi_class='multinomial'))
    run('Elo ALONE (gradient boosting)', Xtr, Xte,
        HistGradientBoostingClassifier(max_depth=4, max_iter=300,
                                       learning_rate=0.06, random_state=0))
    for C in (0.01, 0.1, 1.0):
        run(f'market + Elo (logistic, C={C})',
            np.hstack([Mtr, Xtr]), np.hstack([Mte, Xte]),
            LogisticRegression(C=C, max_iter=3000, multi_class='multinomial'))
    run('market + Elo (gradient boosting)',
        np.hstack([Mtr, Xtr]), np.hstack([Mte, Xte]),
        HistGradientBoostingClassifier(max_depth=3, max_iter=300,
                                       learning_rate=0.05, random_state=0))

    print('\nElo alone losing to the market is EXPECTED and fine -- the market has')
    print('more information. The only result that would matter is "market + Elo"')
    print('beating "market", which would mean Elo knows something the price does not.')


if __name__ == '__main__':
    main()
