"""Independent career reconstruction from literal single developer credits."""
from common import *
import re,json
g,t=load();cut=pd.Timestamp('2026-07-01',tz='UTC');mature=pd.Timestamp('2025-07-01',tz='UTC')
base=g[g.valid & g.date.le(cut) & g.dev.ne('')].copy()
def sequences(d):
    d=d.sort_values(['dev','date','appid']).copy();z=d.groupby('dev',sort=False)
    d['index']=z.cumcount()+1;d['debut']=z.date.transform('min');d['career_n']=z.appid.transform('size')
    d['prior_best']=z.reviews.cummax().groupby(d.dev).shift().fillna(0)
    d['prior_sum']=z.reviews.cumsum().groupby(d.dev).shift().fillna(0)
    for c in ['appid','date','reviews','pct','price','primary','selfpub','name']:d['prev_'+c]=z[c].shift()
    d['gap_days']=(d.date-d.prev_date).dt.total_seconds()/86400
    d['prior_band']=pd.cut(d.prior_best,[-1,49,555,np.inf],labels=['low','mid','hit']).astype(str)
    d['gap_band']=pd.cut(d.gap_days,[0,182.625,365.25,730.5,1461,np.inf],right=False,labels=['<6mo','6-12mo','12-24mo','24-48mo','48mo+']).astype(str)
    return d
allseq=sequences(base);paidseq=sequences(base[~base.free&~base.explicit]);paidseq.to_parquet(R/'career_games.parquet',index=False)
curves=[];surv=[];ladders=[];gaprows=[];pivotrows=[];continue_rows=[];cadrows=[];rungrows=[]
for label,d in [('all',allseq),('paid_nonexplicit',paidseq)]:
    for era in ['all_era','debut2015plus']:
        x=d if era=='all_era' else d[d.debut.ge(pd.Timestamp('2015-01-01',tz='UTC'))]
        score=x[x.date.le(mature)]
        for n in range(1,11):
            a=score[score['index'].eq(n)];curves.append(dict(population=label,era=era,index=n,prior='all',**stats(a)))
            for b in ['low','mid','hit']:
                curves.append(dict(population=label,era=era,index=n,prior=b,**stats(a[a.prior_band.eq(b)])))
    for n in [1,2,3,4]:
        starts=d[d['index'].eq(n)].copy()
        nextdates=d.set_index(['dev','index']).date
        starts['next_date']=[nextdates.get((dev,n+1),pd.NaT) for dev in starts.dev]
        for years in [0,3,5,8,10]:
            a=starts if years==0 else starts[starts.date.le(cut-pd.Timedelta(days=365.25*years))]
            k=int(a.next_date.notna().sum());fixed=int((a.next_date<=a.date+pd.Timedelta(days=365.25*years)).sum()) if years else None
            surv.append(dict(population=label,release_index=n,min_observation_years=years,n=len(a),ever_next=k,ever_next_rate=k/len(a) if len(a) else None,next_within_window=fixed,fixed_rate=fixed/len(a) if len(a) and years else None))
    x=d[d.debut.ge(pd.Timestamp('2015-01-01',tz='UTC')) & d.date.le(mature)&d['index'].gt(1)&d.gap_days.ge(30)].copy()
    fine=[0,10,25,50,100,250,556,1000,2500,10000,np.inf]
    x['fine_band']=pd.cut(x.prior_best,[-1]+fine[1:],right=False).astype(str)
    for b,a in x.groupby('fine_band'):ladders.append(dict(population=label,band=b,**stats(a)))
    for weighting,v in [('release',x),('one_latest_per_dev',x.drop_duplicates('dev',keep='last'))]:
        for prior in ['low','mid','hit']:
            priorpool=v[v.prior_band.eq(prior)]
            for band,a in priorpool.groupby('gap_band'):
                other=priorpool[priorpool.gap_band.ne(band)]
                gaprows.append(dict(population=label,weighting=weighting,prior=prior,gap=band,**stats(a),**compare(a,other,cols=('year','price_band'))))
    miss=x[x.prior_best.lt(50)].copy()
    # Title heuristics are sensitivity measurements, not verified franchise identities.
    def basename(s):
        s=re.sub(r'[™®©]','',str(s).casefold()).strip()
        return re.sub(r'\s*[:\-–—]?\s*(?:[2-9]|1[0-9]|ii|iii|iv|v|vi|vii|viii|ix|x)$','',s).strip(' :-')
    def sequel(row):
        a,b=basename(row['name']),basename(row['prev_name'])
        ca=str(row['name']).lower().split(':');cb=str(row['prev_name']).lower().split(':')
        return (len(a)>=5 and a==b and row['name']!=row['prev_name']) or (len(ca)>1 and len(cb)>1 and len(ca[0])>=6 and ca[0]==cb[0])
    miss['sequel']=miss.apply(sequel,axis=1)
    top5=t[t['rank'].le(5)].groupby('appid').tag.agg(set).to_dict()
    miss['shared5']=[len(top5.get(a,set())&top5.get(b,set())) for a,b in zip(miss.appid,miss.prev_appid)]
    miss['pivot']=miss.primary.ne(miss.prev_primary)
    for weighting,v in [('release',miss),('one_latest_per_dev',miss.drop_duplicates('dev',keep='last'))]:
        for dim in ['pivot','sequel']:
            for val,a in v.groupby(dim):pivotrows.append(dict(population=label,weighting=weighting,test=dim,value=str(val),**stats(a),**compare(a,v[v[dim].ne(val)],cols=('year','price_band','gap_band'),outcomes=('r50','r556'))))
        for val,mask in [('clean_break',v.shared5.le(1)),('heavy_overlap',v.shared5.ge(4)),('loved_miss',v.prev_reviews.ge(10)&v.prev_pct.ge(85)),('unloved_miss',v.prev_reviews.ge(10)&v.prev_pct.lt(70))]:
            pivotrows.append(dict(population=label,weighting=weighting,test='additional',value=val,**stats(v[mask])))
    # Continuation is recorded paid-release presence, not persistence of a person.
    first=d[d['index'].eq(1)&d.date.between(pd.Timestamp('2015-01-01',tz='UTC'),cut-pd.Timedelta(days=365.25*3))].copy()
    second=d[d['index'].eq(2)].set_index('dev').date
    first['next3']=first.dev.map(second).le(first.date+pd.Timedelta(days=365.25*3))
    for dim in ['r50','r556','ea','known_ea','price_band']:
        for val,a in first.groupby(dim,observed=True):continue_rows.append(dict(population=label,dimension=dim,value=str(val),n=len(a),continued3=int(a.next3.sum()),rate=a.next3.mean()))
    for dim,mask in [('pct85plus',first.pct.ge(85)&first.reviews.ge(10)),('pct_under70',first.pct.lt(70)&first.reviews.ge(10))]:
        a=first[mask];continue_rows.append(dict(population=label,dimension='positivity',value=dim,n=len(a),continued3=int(a.next3.sum()),rate=a.next3.mean()))
    # Reproduce cadence definition, but no interpretation as optimal production strategy.
    cs=[]
    for dev,a in d.groupby('dev',sort=False):
        debut=a.date.iloc[0]
        if not 2015<=debut.year<=2021:continue
        years=(cut-debut).total_seconds()/86400/365.25;n=len(a);rate=n/years
        band='<=1 per 3y' if rate<.34 else '1 per 1-3y' if rate<1.01 else '1-3 per yr' if rate<3.01 else '3+ per yr'
        cs.append(dict(dev=dev,n=n,years=years,cadence=band,serious=a.r50.any(),hit=a.r556.any(),hpy=a.r556.sum()/years,annual_reviews=a.reviews.sum()/years))
    cs=pd.DataFrame(cs)
    for scope,c in [('all',cs),('ever50',cs[cs.serious])]:
        for band,a in c.groupby('cadence'):cadrows.append(dict(population=label,scope=scope,cadence=band,n=len(a),median_games=a.n.median(),career_hit_rate=a.hit.mean(),hits_per_100_devyears=100*a.hpy.mean(),median_reviews_per_year=a.annual_reviews.median()))
    # Genre tags on first observed games; multiple memberships retained.
    for tag in ['Horror','Visual Novel','Simulation','Action','Casual','Platformer','Precision Platformer']:
        ids=member(t,[tag],7);a=first[first.appid.isin(ids)]
        laterhits=set(d[(d['index']>1)&d.r556].dev)
        foothold=a[a.r50]
        rungrows.append(dict(population=label,tag=tag,n=len(a),first50_rate=a.r50.mean(),first556_rate=a.r556.mean(),among_first50_later556=foothold.dev.isin(laterhits).mean(),continued3=a.next3.mean()))
save(curves,'career_hit_curves.csv');save(surv,'continuation_windows.csv');save(ladders,'career_ladder.csv');save(gaprows,'career_gaps.csv');save(pivotrows,'career_pivots.csv');save(continue_rows,'continuation_features.csv');save(cadrows,'cadence.csv');save(rungrows,'first_rung.csv')
# Separate matched-era replication of sessions 1/3 prior-hit headline.
rows=[];x=paidseq
early=x[x.year.between(2020,2022)];later=x[x.year.between(2023,2025)].copy()
priorh=early.groupby('dev').h50.max();priorr=early.groupby('dev').r556.max();ever_before=set(x[x.year.lt(2023)].dev)
later['state']=np.where(later.dev.map(priorh).fillna(False),'prior_h50',np.where(later.dev.isin(set(early.dev)),'prior_no_h50',np.where(later.dev.isin(ever_before),'older_veteran','first_observed')))
for weight,d in [('release',later),('first_per_dev',later.drop_duplicates('dev',keep='first'))]:
    for state,a in d.groupby('state'):rows.append(dict(weighting=weight,scope='all',state=state,**stats(a)))
    for state,a in d[d.selfpub].groupby('state'):rows.append(dict(weighting=weight,scope='selfpub_next',state=state,**stats(a)))
save(rows,'prior_hit_replication.csv')
recent=allseq[allseq.year.between(2023,2025)]
summary={'career_games_all':len(allseq),'career_devs_all':allseq.dev.nunique(),'career_games_paid_nonexplicit':len(paidseq),'career_devs_paid_nonexplicit':paidseq.dev.nunique(),'first_game_share_of_recent_556':recent.loc[recent.r556,'index'].eq(1).mean(),'one_game_developer_share_of_recent_556':recent.loc[recent.r556,'career_n'].eq(1).mean(),'cutoff':str(cut),'maturity':str(mature),'outcome_snapshot':'2026-09-05'}
(R/'career_summary.json').write_text(json.dumps(summary,indent=2));print(summary,flush=True)
