from common import *
import json
g=pd.read_parquet(R/'games.parquet');events=pd.read_csv(R/'backcatalog_events.csv');r=pd.read_parquet(R/'historical_reviews.parquet');root=R.parent/'game-lifetimes-2026-09-07'
controls=pd.read_parquet(root/'game_histories.parquet');controls=controls[controls.release_clock_valid&controls.appid.isin(g.appid)].copy()
meta=g.set_index('appid');timeline={a:np.sort(x.timestamp_created.to_numpy()) for a,x in r.groupby('appid')}
for a in controls.appid:timeline[a]=np.sort(pd.read_parquet(root/'completed'/f'{a}.parquet').timestamp_created.to_numpy())
release_dates={dev:a.date.to_numpy() for dev,a in g.groupby('developer_id')}
def count(a,lo,hi):
    ts=timeline[a];return int(np.searchsorted(ts,hi.timestamp())-np.searchsorted(ts,lo.timestamp()))
rows=[];peers=[]
for ev in events[events.isolated].itertuples():
    t=pd.Timestamp(ev.date);lo=t-pd.Timedelta(days=90);hi=t+pd.Timedelta(days=90)
    ids=[int(x) for x in ev.older_appids.split(';')];candidates=[]
    for c in controls.itertuples():
        game=meta.loc[c.appid]
        if game.developer_id==ev.developer_id or game.date>t-pd.Timedelta(days=365):continue
        dates=release_dates[game.developer_id]
        if any(t-pd.Timedelta(days=180)<dt<t+pd.Timedelta(days=180) for dt in dates):continue
        before=count(c.appid,lo,t);after=count(c.appid,t,hi)
        candidates.append((c.appid,before,after,(t-game.date).total_seconds()/86400/365.25))
    for a in ids:
        before=count(a,lo,t);after=count(a,t,hi)
        if before<3:continue
        age=(t-meta.loc[a,'date']).total_seconds()/86400/365.25
        possible=[(abs(np.log((b+.5)/(before+.5)))+.15*abs(cage-age),cid,b,z) for cid,b,z,cage in candidates if max(1,before/2)<=b<=before*2 and abs(cage-age)<=3]
        possible.sort();selected=possible[:5];support=len(selected)>=3;expected=before*np.median([(z+.5)/(b+.5) for _,cid,b,z in selected]) if support else np.nan
        rows.append(dict(new_appid=ev.new_appid,credit=ev.credit,new_game=ev.new_game,older_appid=a,older_game=meta.loc[a,'name'],pre90=before,post90=after,matched=support,controls=len(selected),expected_post90=expected))
        if support:peers.extend(dict(new_appid=ev.new_appid,older_appid=a,control_appid=cid,control_name=meta.loc[cid,'name'],control_pre=b,control_post=z,distance=dist) for dist,cid,b,z in selected)
d=pd.DataFrame(rows);d.to_csv(R/'backcatalog_control_pairs.csv',index=False);save(peers,'backcatalog_control_matches.csv')
matched=d[d.matched];group=matched.groupby(['new_appid','credit','new_game']).agg(older_games=('older_appid','size'),observed=('post90','sum'),expected=('expected_post90','sum'),before=('pre90','sum')).reset_index();group['difference']=group.observed-group.expected;group['oe']=group.observed/group.expected
group.to_csv(R/'backcatalog_control_events.csv',index=False)
summary={'eligible_older_game_event_pairs':len(d),'matched_pairs':len(matched),'events':len(group),'developers':group.credit.nunique(),'observed_post':int(group.observed.sum()),'expected_post':float(group.expected.sum()),'pooled_oe':float(group.observed.sum()/group.expected.sum()) if group.expected.sum() else None,'median_event_oe':float(group.oe.median()),'note':'Selected historical cases and controls from prior random lifetime sample, not randomized launches. Controls match baseline count within factor2 and age within3years; at least3 up to5, no recorded paid launch within180days. No causal interpretation; unsupported pairs excluded from both observed and expected.'}
(R/'backcatalog_control_summary.json').write_text(json.dumps(summary,indent=2));print(json.dumps(summary,indent=2))
