from pathlib import Path
import json,hashlib,ast,re,unicodedata
import numpy as np
import pandas as pd
from source_data import load_games
R=Path(__file__).resolve().parent;ROOT=R.parent
def norm(v):return re.sub(r'\s+',' ',unicodedata.normalize('NFKC',str(v)).casefold()).strip()
x=pd.read_parquet(R/'games.parquet');tr=pd.read_parquet(R/'transitions.parquet');p=pd.read_csv(R/'portfolios.csv')
G=load_games()
C=pd.read_parquet(ROOT/'2026-09-05/exports/creators.parquet')
checks={}
def check(n,v):
 checks[n]=bool(v)
 assert v,n
check('source_184664',len(G)==184664 and G.appid.nunique()==184664)
check('primary_unique_games',x.appid.nunique()==len(x)==93013)
check('name_grouping_no_page_merging',x.groupby('dev').credit_norm.nunique().eq(1).all())
check('primary_credit_key',x.dev.eq('name:'+x.credit_norm).all())
check('verified_kof_date_override',x.loc[x.appid.eq(1498570),'first_date'].iloc[0]==pd.Timestamp('2022-02-17',tz='UTC'))
check('unique_creator_period_votes',not tr.duplicated(['dev','period']).any())
check('gap_bounds',tr.gap_days.between(30,3650).all())
check('chronological_predecessors',tr.prev_first_date.lt(tr.first_date).all())
check('predecessor_count_source',tr.prev_reviews.eq(tr.prev_appid.map(x.set_index('appid').reviews)).all())
check('predecessor_same_developer',tr.dev.eq(tr.prev_appid.map(x.set_index('appid').dev)).all())
check('similarity_range',tr.similarity.dropna().between(0,1).all())
q=x[x.year.le(2025)]
check('portfolio_totals',p.total_reviews.sum()==q.reviews.sum() and p.n.sum()==len(q))
check('portfolio_peaks',p.set_index('dev').peak_reviews.eq(q.groupby('dev').reviews.max()).all())
check('shape_7427',p.eligible_shape.sum()==7427)
check('strong_shape_1248',(p.eligible_shape&p.peak_reviews.ge(1000)).sum()==1248)
rows=pd.read_csv(R/'transition_groups.csv')
for period,z in tr.groupby('period'):
 for axis in ['prior_band','gap_band','similarity_band']:
  rr=rows[rows.period.eq(period)&rows.axis.eq(axis)]
  check(period+'_'+axis+'_denominator',rr.n.sum()==len(z))
  check(period+'_'+axis+'_numerator',rr.ge100.sum()==z.reviews.ge(100).sum())
  check(period+'_'+axis+'_expected_calibrated',abs(rr.expected100.sum()-rr.ge100.sum())<1e-5)
 for band,a in z.groupby('prior_band'):
  rr=rows[rows.period.eq(period)&rows.axis.eq('prior_band')&rows.value.eq(band)].iloc[0]
  check(period+'_'+band+'_source_counts',rr.n==len(a) and rr.ge1000==a.reviews.ge(1000).sum() and rr.liked100==(a.reviews.ge(100)&a.positive_pct.ge(80)).sum())

# All-developer-credit sensitivity: a co-developed title belongs to each named
# credited developer for this alternative only. This is not independent player data.
c=C[C.role.eq('developers')].copy();c['norm']=c.name.map(norm)
amb=set(pd.read_csv(R/'ambiguous_names.csv').name)
c=c[c.norm.ne('')&~c.norm.isin(amb)].drop_duplicates(['appid','norm'])
base=G[G.main&~G.name.fillna('').str.contains(r'\b(?:prologue|playtest|demo|preview)\b',case=False,regex=True)]
ac=base[['appid','name','first_date','year','month','reviews','positive_pct']].merge(c[['appid','norm']],on='appid').sort_values(['norm','first_date','appid'])
ac['prev_reviews']=ac.groupby('norm').reviews.shift();ac['previous_date']=ac.groupby('norm').first_date.shift()
ac['gap']=(ac.first_date-ac.previous_date).dt.total_seconds()/86400
ac=ac[ac.gap.between(30,3650)&ac.year.between(2023,2025)].sort_values('first_date').groupby('norm').tail(1)
ac['prior_band']=pd.cut(ac.prev_reviews,[-1,9,99,999,9999,np.inf],labels=['<10','10-99','100-999','1000-9999','10000+']).astype(str)
sens=[]
for band,z in ac.groupby('prior_band'):sens.append(dict(prior_band=band,n=len(z),ge100=int(z.reviews.ge(100).sum()),ge1000=int(z.reviews.ge(1000).sum()),median_reviews=float(z.reviews.median())))
pd.DataFrame(sens).to_csv(R/'all_credits_sensitivity.csv',index=False)

orig=pd.to_datetime(x.original_release_date.where(x.original_release_date>0),unit='s',utc=True,errors='coerce')
flag=orig.lt(x.first_date-pd.Timedelta(days=365))|x.known_ea_history
flags=pd.Series(flag.to_numpy(),index=x.appid)
date_clean=~tr.appid.map(flags)&~tr.prev_appid.map(flags)
dr=[]
for period,z in tr[date_clean].groupby('period'):
 for band,a in z.groupby('prior_band'):dr.append(dict(period=period,prior_band=band,n=len(a),ge100=int(a.reviews.ge(100).sum()),ge1000=int(a.reviews.ge(1000).sum()),median_reviews=float(a.reviews.median())))
pd.DataFrame(dr).to_csv(R/'date_history_sensitivity.csv',index=False)

# Verify the old-peak variable against explicit preceding rows on a fixed sample.
for a in tr.sample(100,random_state=90645).itertuples():
 history=x[x.dev.eq(a.dev)&x.ordinal.lt(a.ordinal)]
 check('prior_peak_'+str(a.appid),a.previous_max_now==history.reviews.max())
f=json.loads((R/'direction_labels_frozen.json').read_text())
check('direction_labels_frozen',hashlib.sha256((R/'direction_labels.json').read_bytes()).hexdigest()==f['sha256'])
labels=json.loads((R/'direction_labels.json').read_text())
check('direction_24_unique',len(labels)==len({a['appid'] for a in labels})==24)
check('network_disabled',json.loads((ROOT/'network_policy.json').read_text())['network_paused'])
check('no_asset_downloader',not(ROOT/'images.py').exists())
for src in R.glob('*.py'):ast.parse(src.read_text())
check('scripts_parse',True)
check('figures_exist',all((R/f'{name}.{ext}').stat().st_size>1000 for name in ['followup_reception','catalog_context'] for ext in ['png','svg']))
manifest={}
for rel in ['analysis-2026-09-05/games.parquet','analysis-2026-09-05/tags.parquet','2026-09-05/exports/creators.parquet']:
 h=hashlib.sha256()
 with (ROOT/rel).open('rb') as f:
  for buf in iter(lambda:f.read(1024*1024),b''):h.update(buf)
 manifest[rel]=h.hexdigest()
(R/'source_manifest.json').write_text(json.dumps(manifest,indent=2))
(R/'validation.json').write_text(json.dumps(dict(passed=sum(checks.values()),checks=checks,script_hashes={s.name:hashlib.sha256(s.read_bytes()).hexdigest() for s in R.glob('*.py')}),indent=2))
print('Passed',len(checks),'checks')
print('ALL CREDITS\n',pd.DataFrame(sens).to_string(index=False))
print('DATE HISTORY\n',pd.DataFrame(dr).query("period == '2023_2025'").to_string(index=False))
