from pathlib import Path
import json,pandas as pd,numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
R=Path(__file__).resolve().parent
res=pd.read_csv(R/'event_results.csv');primary=res[res.anchor.eq('first_public_update')]
sea=pd.read_csv(R/'seasonal_comparisons.csv').merge(primary[['appid','pre90','post90','ratio90']],on='appid')
sea['old_ratio90']=sea.old_post90/sea.old_pre90
sea['ratio_of_ratios']=sea.ratio90/sea.old_ratio90
sea.to_csv(R/'seasonal_comparisons.csv',index=False)
# Audit coverage and contemporaneous announcements; quiet feed is not proof of no patch.
c=pd.read_csv(R/'control_candidates.csv');ev=pd.read_csv(R/'events.csv').set_index('appid');out=[]
reject={(241600,222730):'Own anniversary patch and Daily Deal in pre-window',(241600,270450):'Own patch and console launch in pre-window; new game announcement after',(241600,296970):'Own patch in pre-window and anniversary after',(247080,860890):'Major 2.0 update eight days before target event',(247080,1139890):'Only surviving feed item is from 2025; insufficient 2022 coverage',(242680,855860):'Public update preview within post-window',(242680,1139890):'Own update announcement in pre-window; sparse feed'}
for x in c.itertuples():
 t=pd.Timestamp(ev.loc[x.event_appid,'event'],tz='UTC');ns=json.loads((R/'news'/f'{x.control_appid}.json').read_text())['response']['appnews']['newsitems']
 near=[{'date':str(pd.Timestamp(n['date'],unit='s').date()),'title':n['title'],'url':n['url']} for n in ns if abs((pd.Timestamp(n['date'],unit='s',tz='UTC')-t).total_seconds())<=100*86400]
 reason=reject.get((x.event_appid,x.control_appid));row=x._asdict();row.pop('Index');row.update(usable_context=not bool(reason),audit=reason or 'No own-game patch identified in observed +/-100-day feed; untreated status unproven',nearby_announcements=json.dumps(near),feed_count=len(ns))
 if x.control_appid==1007040:row['audit']='No own-game patch identified; unrelated publisher promotion exists; untreated status unproven'
 if x.control_appid==925520:row['audit']='Only one old announcement; very weak coverage; retain solely as descriptive context'
 out.append(row)
pd.DataFrame(out).to_csv(R/'control_audit.csv',index=False)
sel=[(12900,'63667889','Existing owner remembered game through an article about the update.'),(247080,'118069831','Longtime owner says new 3.0 quality-of-life update prompted first review.'),(247080,'118081255','Reviewer welcomes faster loading.'),(247080,'117960481','Reviewer reports crash on launch after the large update.'),(242680,'212637322','Longtime player says content update finally prompted review after a decade.'),(242680,'212644723','Reviewer welcomes new custom options.'),(70300,'99778987','Reviewer thanks 2.3 update and praises 144 Hz presentation.')]
rows=[]
for app,rid,note in sel:
 d=pd.read_parquet(R/'review_windows'/f'{app}.parquet');x=d[d.recommendationid.astype(str).eq(rid)].iloc[0];rows.append({'game':ev.loc[app,'name'],'appid':app,'review_id':rid,'created':str(pd.Timestamp(x.timestamp_created,unit='s',tz='UTC').date()),'updated':str(pd.Timestamp(x.timestamp_updated,unit='s',tz='UTC').date()),'currently_positive':bool(x.voted_up),'interpretation':note,'source_file':f'review_windows/{app}.parquet'})
sn=pd.read_csv(R/'snkrx_update_mentions.csv');notes={'222663377':'Positive review wishes continued support.','222726597':'Positive review regrets lack of updates.','214458841':'Positive review asks creator for more updates.','143744577':'Positive review asks for more characters and larger levels.','169949608':'Negative review reports pause-menu failure and lost runs. Unverified bug report.','231814015':'Positive review criticizes NG+ progression, reroll economics and unclear difficulty explanations.','179140515':'Positive review calls game complete and rejects expectation of endless updates.'}
for rid,note in notes.items():
 x=sn[sn.recommendationid.astype(str).eq(rid)].iloc[0];rows.append({'game':'SNKRX','appid':915310,'review_id':rid,'created':str(x.created)[:10],'updated':str(pd.Timestamp(x.timestamp_updated,unit='s',tz='UTC').date()),'currently_positive':bool(x.voted_up),'interpretation':note,'source_file':'snkrx_update_mentions.csv'})
pd.DataFrame(rows).to_csv(R/'selected_review_evidence.csv',index=False)
# Panel axes intentionally differ: shape and local persistence, not audience ranking.
m=pd.read_csv(R/'event_months.csv');fig,axs=plt.subplots(4,2,figsize=(12,12));fig.patch.set_facecolor('#faf9f5')
for ax,e in zip(axs.flat,primary.itertuples()):
 d=m[m.appid.eq(e.appid)];ax.set_facecolor('#faf9f5');ax.bar(d.relative_month,d['count'],color=np.where(d.relative_month.lt(0),'#909b9f','#277d86'),width=.85);ax.axvline(-.5,color='#b66042',lw=1)
 ax.set_title(f'{e.name}  |  {e.event}',loc='left',fontsize=11);ax.set_xticks([-12,-6,0,6,11]);ax.set_xlim(-12.7,11.7);ax.set_ylim(bottom=0);ax.grid(axis='y',alpha=.16);ax.set_axisbelow(True);ax.spines[['top','right']].set_visible(False);ax.set_ylabel('Reviews created')
axs.flat[-1].axis('off');axs.flat[-1].text(0,.95,'Month 0 begins on the update date.\nGrey: before. Teal: after.\n\nEach panel uses its own count scale.\nNuclear Throne: only complete observed months.\nRogue Legacy: first public beta is the event.\n\nSteam-purchase reviews, all languages.\nSurviving records collected September 7, 2026.\nThese are not sales or causal effect estimates.',va='top',fontsize=11,linespacing=1.5)
fig.suptitle('Late updates: attention can spike, then recede',fontsize=19,x=.06,ha='left');fig.supxlabel('Calendar months relative to first public update');fig.tight_layout(rect=(0,.02,1,.95));fig.savefig(R/'review_trajectories.png',dpi=160);fig.savefig(R/'review_trajectories.svg');plt.close(fig)
print(sea[['name','pre90','post90','old_pre90','old_post90','ratio_of_ratios']].to_string(index=False))
