"""
howtoturnpro — WorldTour salary model v0.6 "team hierarchy" (14 Sep 2026).

Idea (Sergio): a team pays by its hierarchy, decided when each contract was signed.
  T1  the leader(s) the whole team works for            weight 10 (proven: GT podium / WT wins) or 6 (leader of a smaller team)
  T2  second tier: co-leaders, leaders of smaller races, protected super-domestiques with wins (Yates, Almeida, Kuss)   4 (proven) / 3
  T3  key men on the climbs, road captains, strong domestiques (Großschartner, Politt, Wellens)                        2
  T4  ordinary domestiques (tempo at the front until it gets hard)                                                      1
  Neo first or second WorldTour season at signing                                                                       0.5
Each team's wage pool (budget x wage share, inherited from v0.4) is split over its riders in proportion to these weights,
after the reported salaries (anchors) are taken out. Inside a tier the weight moves a little with results at signing and age.
Tier and results are taken AT SIGNING (data/signing_v0.csv): a rider paid on a 2023 contract is priced on 2021-23.
Market value 2026 = the same machinery on today's results and today's tier.
"""
import io, os, sys, json, contextlib
import pandas as pd, numpy as np
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, f'{ROOT}/model')

SRC = open(f'{ROOT}/model/salary_model.py', encoding='utf-8').read().replace("df.to_csv(OUT, index=False)", "pass")
def base_frame():
    g = {'__name__': 'v06', '__file__': f'{ROOT}/model/salary_model.py'}
    with contextlib.redirect_stdout(io.StringIO()):
        exec(compile(SRC, 'salary_model.py', 'exec'), g)
    return g

SD = json.load(open(f'{ROOT}/site/data/site_data.json', encoding='utf-8'))
PCS2SLUG = {r['pcs_name']: r['slug'] for r in SD['riders'] if r['tier'] == 'WT'}
H = json.load(open(f'{ROOT}/data/history/rider_history.json', encoding='utf-8'))
SLUG2PCS = {r['slug']: r['pcs_slug'] for r in SD['riders']}
import salary_model as MODEL
SIGN = pd.read_csv(f'{ROOT}/data/signing_v0.csv').dropna(subset=['slug']).drop_duplicates('slug').set_index('slug')
ROLES = pd.read_csv(f'{ROOT}/data/roles_v0.csv').dropna(subset=['slug']).drop_duplicates('slug').set_index('slug')
REV = f'{ROOT}/data/roles_reviewed.csv'
if os.path.exists(REV):
    rv = pd.read_csv(REV).set_index('slug')
    for c in ('role_primary', 'role_secondary'):
        ROLES.loc[rv.index[rv[c].notna()], c] = rv[c].dropna()
LEADER_ROLES = {'GC leader', 'Classics leader', 'Sprinter', 'Time-triallist', 'Puncheur', 'Climber'}
HELPER_KEY = {'Super-domestique', 'Road captain'}

def results_upto(slug, year):
    """wins, podiums, WT seasons and vp for the three seasons up to `year` (inclusive)."""
    h = H.get(SLUG2PCS.get(slug))
    if not h: return 0, 0, 0, 0.0
    w = p = 0; wt = 0; pts = {}
    for s in h['seasons']:
        if s['year'] <= year and s['cat'] in ('WT', 'PRT'): wt += 1
        if year - 2 <= s['year'] <= year:
            w += s['wins']; p += s['podiums']
        pts[s['year']] = s['pts']
    vp = 0.5 * pts.get(year, 0) + 0.3 * pts.get(year - 1, 0) + 0.2 * pts.get(year - 2, 0)
    return w, p, wt, vp

ROLE_MULT = {'GC leader': 1.15, 'Sprinter': 1.10, 'Classics leader': 1.05, 'Time-triallist': 1.0, 'Puncheur': 1.0, 'Climber': 1.0,
             'Super-domestique': 1.0, 'Road captain': 1.0, 'Domestique': 1.0, 'Breakaway rider': 1.0, 'Lead-out': 1.0, 'Neo / new to WT': 1.0}

def assign_tiers(p, year_col, vp_col):
    """Tier per rider, decided within each team on results up to the pricing year.
    T1: the top leader by results at signing (a second one only for a superstar pair, both >= 2000).
    T2: up to 3 further leaders with vp >= 600; young talents bought as future leaders (age <= 23, vp >= 400);
        protected super-domestiques with wins (Kuss).
    T3: remaining leaders with results (vp >= 250), all super-domestiques / road captains, and prospects (age <= 23, vp >= 150).
    Neo: <= 1 pro season at signing with little to show, or the Neo role. Tagged prospects (junior/U23 stars) get extra weight."""
    tiers = pd.Series('T4', index=p.index); proven = pd.Series(False, index=p.index); prospect = pd.Series(False, index=p.index)
    for team, g in p.groupby('Team'):
        lead = g[g['role'].isin(LEADER_ROLES)].sort_values(vp_col, ascending=False)
        n_t1 = 0; n_t2 = 0
        for i, r in lead.iterrows():
            vp = r[vp_col]; young = r['age_sign'] <= 23
            if (n_t1 == 0 and vp >= 500) or (n_t1 == 1 and vp >= 2000):
                tiers[i] = 'T1'; proven[i] = vp >= 1200; n_t1 += 1
            elif vp >= 900 or (n_t2 < 3 and vp >= 600) or (young and vp >= 400) or (n_t2 < 3 and vp >= 400 and r['career'] >= 3000):   # a >=900 leader is T2 whatever the count (Milan behind Pedersen/Ayuso/Skjelmose/Ciccone)   # established winners (Monuments, GTs) keep T2 status on a quieter year
                tiers[i] = 'T2'; proven[i] = vp >= 900 or r['win_sign'] >= 4 or (young and vp >= 500); n_t2 += 1
            elif vp >= 250 or (young and vp >= 150):
                tiers[i] = 'T3'
        for i, r in g[g['role'].isin(HELPER_KEY)].iterrows():
            tiers[i] = 'T2' if (r['win_sign'] >= 3 and r[vp_col] >= 800 and r['age_sign'] <= 33) else 'T3'   # protected helpers with real wins (Kuss); Benson's mountain SD band is 0.7-1.3M, so ordinary super-domestiques stay T3
        for i, r in g.iterrows():
            vp = r[vp_col]
            # two WorldTour seasons behind him means the deal being priced is a SECOND contract, not a first:
            # that is what the T4b band exists for. Only a genuine first-year pro belongs in Neo.
            # v0.8: 'new to the WorldTour' is not the same as 'new to professional cycling'. The Neo band
            # is for a genuine first-or-second-year professional; an experienced rider stepping up from a
            # ProTeam keeps his status and is placed by his results like anyone else.
            if tiers[i] in ('T4',) and r.get('pro_sign', 0) <= 2 and (r['role'] == 'Neo / new to WT' or r['wt_sign'] <= 1):
                tiers[i] = 'Neo'
            if tiers[i] == 'Neo' and (str(r.get('sec', '')) == 'Prospect' or (r['age_sign'] <= 19 and vp >= 20) or (r['age_sign'] <= 21 and vp >= 100)):
                prospect[i] = True
    return tiers, proven, prospect

W = {'T1': 16.0, 'T1u': 8.0, 'T2': 9.0, 'T2u': 6.0, 'T3': 3.0, 'T4': 1.0, 'Neo': 0.5}

def weights(p, tiers, proven, vp_col, age_col, contract=True, W=W, prospect=None):
    w = pd.Series(1.0, index=p.index)
    for i in p.index:
        t = tiers[i]
        base = W['T1'] if (t == 'T1' and proven[i]) else W['T1u'] if t == 'T1' else W['T2'] if (t == 'T2' and proven[i]) else W['T2u'] if t == 'T2' else W[t]
        if prospect is not None and prospect[i]: base = W['Neo'] * 2.5          # a hyped junior / U23 talent is bought at a premium
        w[i] = base * ROLE_MULT.get(p.loc[i, 'role'], 1.0)
    # inside a tier: move with results at signing (percentile within the tier, league-wide) -> x0.8 .. x1.2
    for t, g in p.assign(_t=tiers).groupby('_t'):
        if len(g) > 1:
            pct = g[vp_col].rank(pct=True)
            w[g.index] *= 0.8 + 0.4 * pct
    a = p[age_col]
    w[a >= 35] *= 0.85
    if contract:
        w[(a <= 22)] *= 0.85                                   # first contracts are cheap
        w[(a <= 24) & (p['contract_len'] >= 4) & (tiers != 'T1')] *= 0.9
    return w

def allocate(p, w, anchor, BUDGET, wage_share):
    """Split each team's pool over unanchored riders by weight, floors respected; anchors fixed."""
    sal = pd.Series(0.0, index=p.index)
    for team, g in p.groupby('Team'):
        pool = BUDGET[team] * 1e6 * wage_share(BUDGET[team])
        anch = anchor[g.index].notna()
        sal[g.index[anch]] = anchor[g.index[anch]]
        left = pool - anchor[g.index[anch]].sum()
        free = g.index[~anch]
        fixed = pd.Index([])
        for _ in range(10):
            mov = free.difference(fixed)
            if len(mov) == 0: break
            budget_left = left - sal[fixed].sum()
            alloc = w[mov] / w[mov].sum() * budget_left
            fl = p.loc[mov, 'floor']
            below = alloc < fl
            sal[mov] = alloc
            if not below.any(): break
            sal[mov[below]] = fl[below]; fixed = fixed.union(mov[below])
    return sal

def run(exclude=None, W=W, verbose=False):
    g = base_frame(); df, BUDGET, wage_share = g['df'], g['BUDGET'], g['wage_share']
    df['slug'] = df['Rider'].map(PCS2SLUG)
    p = df[df['in_pool']].copy()
    p = p.join(SIGN[['price_year', 'age_at_signing', 'vp_at_signing', 'vp_now', 'contract_len', 'price_year_src']], on='slug')
    p = p.join(ROLES[['role_primary', 'role_secondary']].rename(columns={'role_primary': 'role', 'role_secondary': 'sec'}), on='slug')
    p['role'] = p['role'].fillna('Domestique'); p['price_year'] = p['price_year'].fillna(2025).astype(int)
    p['vp_at_signing'] = p['vp_at_signing'].fillna(p['VP']); p['vp_now'] = p['vp_now'].fillna(p['VP'])
    p['age_sign'] = p['age_at_signing'].fillna(p['age']); p['contract_len'] = p['contract_len'].fillna(3)
    res = p.apply(lambda r: results_upto(r['slug'], r['price_year']), axis=1, result_type='expand'); res.columns = ['win_sign', 'pod_sign', 'wt_sign', '_vp']
    p = pd.concat([p, res], axis=1)
    now = p.apply(lambda r: results_upto(r['slug'], 2026), axis=1, result_type='expand'); now.columns = ['win_now', 'pod_now', 'wt_now', '_vpn']
    p = pd.concat([p, now], axis=1)
    anchor = p['anchor'].copy()
    if exclude is not None: anchor[p['Rider'] == exclude] = np.nan
    tiers, proven, prospect = assign_tiers(p, 'price_year', 'vp_at_signing')
    w = weights(p, tiers, proven, 'vp_at_signing', 'age_sign', contract=True, W=W, prospect=prospect)
    p['tier'] = tiers; p['tier_proven'] = proven; p['prospect'] = prospect; p['weight'] = w.round(2)
    p['salary_est'] = allocate(p, w, anchor, BUDGET, wage_share).round(-3)
    # market value: today's results, today's tier, no anchors (what he would fetch now), league total = wage bill
    p2 = p.copy(); p2['win_sign'] = p['win_now']; p2['pod_sign'] = p['pod_now']; p2['wt_sign'] = p['wt_now']; p2['age_sign'] = p['age']
    t2, pr2, pp2 = assign_tiers(p2, 'x', 'vp_now')
    w2 = weights(p2, t2, pr2, 'vp_now', 'age', contract=False, W=W, prospect=pp2)
    mv = allocate(p2, w2, pd.Series(np.nan, index=p.index), BUDGET, wage_share)
    p['tier_now'] = t2; p['market_value'] = mv.round(-3)
    p['is_anchor'] = anchor.notna()
    if verbose:
        print('riders', len(p), 'wage M', round(p['salary_est'].sum() / 1e6, 1), 'median k', round(p['salary_est'].median() / 1e3), 'mean k', round(p['salary_est'].mean() / 1e3),
              'over 1M', int((p['salary_est'] >= 1e6).sum()), 'at floor', int((p['salary_est'] <= p['floor']).sum()))
        print(p['tier'].value_counts().to_dict())
    return p

def loo(W=W, quiet=True):
    base = run(W=W)
    rows = []
    for _, a in base[base['is_anchor']].iterrows():
        d = run(exclude=a['Rider'], W=W)
        r = d[d['Rider'] == a['Rider']].iloc[0]
        rows.append({'rider': a['Rider'], 'team': a['Team'], 'tier': r['tier'], 'proven': bool(r['tier_proven']), 'price_year': int(r['price_year']),
                     'reported': int(a['anchor']), 'model_loo': int(r['salary_est']), 'diff_pct': round((r['salary_est'] - a['anchor']) / a['anchor'] * 100, 1)})
    v = pd.DataFrame(rows).sort_values('reported', ascending=False); ab = v['diff_pct'].abs()
    stats = {'median_abs': round(ab.median(), 1), 'mean_abs': round(ab.mean(), 1), 'median_diff': round(v['diff_pct'].median(), 1),
             'within25': int((ab <= 25).sum()), 'within40': int((ab <= 40).sum()), 'over50': int((ab > 50).sum()), 'under': int((v['diff_pct'] < 0).sum()),
             'median_k': round(base['salary_est'].median() / 1e3), 'mean_k': round(base['salary_est'].mean() / 1e3)}
    if not quiet: print(v.to_string(index=False)); print(stats)
    return v, stats, base

if __name__ == '__main__':
    v, st, base = loo(quiet=False)
    v.to_csv(f'{ROOT}/data/validation_loo_v06.csv', index=False)
    cols = ['Rider', 'Team', 'age', 'role', 'tier', 'tier_proven', 'price_year', 'vp_at_signing', 'weight', 'salary_est', 'tier_now', 'market_value', 'is_anchor']
    base.sort_values(['Team', 'salary_est'], ascending=[True, False])[cols].to_csv(f'{ROOT}/data/wt_salary_estimates_v06.csv', index=False)
