from pathlib import Path
import json,hashlib,ast
import numpy as np
import pandas as pd
R=Path(__file__).resolve().parent;ROOT=R.parent
t=pd.read_parquet(R/'targets.parquet').set_index('appid',drop=False)
v=pd.read_parquet(R/'reviews.parquet');f=pd.read_parquet(R/'free_context_reviews.parquet')
v['created']=pd.to_datetime(v.timestamp_created,unit='s',utc=True)
h=pd.read_csv(R/'game_history.csv').set_index('appid');p=pd.read_csv(R/'historical_pairs.csv')
checks={}
def check(k,b):checks[k]=bool(b);assert b,k
paid_ids=set(t[t.is_free.eq(0)].appid);paid=v[v.appid.isin(paid_ids)]
check('27_fixed_targets',len(t)==27 and len(paid_ids)==24)
check('primary_15445_records',len(paid)==15445)
check('no_duplicate_main_records',not v.duplicated(['appid','recommendationid']).any())
check('no_duplicate_free_records',not f.duplicated(['appid','recommendationid']).any())
check('all_main_steam_purchase',v.steam_purchase.all())
check('free_context_targets',set(f.appid)=={769970,1673600,1746370})
unique=pd.concat([v[['appid','recommendationid']],f[['appid','recommendationid']]]).drop_duplicates()
check('within_25000_review_cap',len(unique)<=25000)
calls=[json.loads(s) for s in (R/'requests.jsonl').read_text().splitlines()]
probe_names=['byte_hist','snkrx_hist','byte_summary','snkrx_summary','byte_all_summary','snkrx_all_summary','byte_hist_steam','snkrx_hist_steam','byte_unfiltered_summary','snkrx_unfiltered_summary']
probe_calls=sum((R/(n+'.json')).exists() for n in probe_names)
check('shared_request_cap',len(calls)<=250 and len(calls)+probe_calls<=250)
check('request_targets_fixed',set(a['appid'] for a in calls)<=set(t.appid))
at=pd.to_datetime([a['at'] for a in calls],utc=True)
check('request_pacing',len(at)<2 or at.to_series().diff().dt.total_seconds().dropna().min()>=1.9)
status=json.loads((R/'collection_status.json').read_text());fs=json.loads((R/'free_context_status.json').read_text())
for a in status:
 appid=a['appid'];z=v[v.appid.eq(appid)]
 check('count_'+str(appid),len(z)==a['collected']==h.loc[appid,'n'])
 last=sorted((R/'review_pages').glob(f'{appid}_*.json'))[-1]
 check('cursor_exhausted_'+str(appid),json.loads(last.read_text())['reviews']==[])
 for days in [30,90,365]:
  value=h.loc[appid,f'count_by_day{days}']
  if pd.notna(value):check(f'day{days}_{appid}',int(value)==z.created.lt(t.loc[appid,'first_date']+pd.Timedelta(days=days)).sum())
 check('monotone_windows_'+str(appid),h.loc[appid,['count_by_day30','count_by_day90','count_by_day365']].dropna().astype(float).is_monotonic_increasing)
for a in fs:
 appid=a['appid'];check('free_count_'+str(appid),len(f[f.appid.eq(appid)])==a['collected'])
 last=sorted((R/'free_context_pages').glob(f'{appid}_*.json'))[-1]
 check('free_cursor_exhausted_'+str(appid),json.loads(last.read_text())['reviews']==[])
check('primary_clocks_coherent',h.loc[list(paid_ids),'clock_usable'].all())
check('18_primary_paid_pairs',len(p)==18)
for a in p.itertuples():
 cutoff=pd.Timestamp(a.release);earlier=t[t.credit.eq(a.developer)&t.is_free.eq(0)&t.first_date.lt(cutoff)]
 z=v[v.appid.isin(earlier.appid)]
 check('prior_catalog_'+str(a.appid),z.created.lt(cutoff).sum()==a.prior_catalog_reviews_at_launch)
 check('prior_single_'+str(a.appid),v[v.appid.eq(a.previous_appid)].created.lt(cutoff).sum()==a.previous_reviews_at_launch)
check('own_before_snkrx_219',p[p.appid.eq(915310)].previous_reviews_at_launch.iloc[0]==219)
check('horizon_kings_day90',h.loc[1224290,'count_by_day90']==166 and h.loc[2276830,'count_by_day90']==165)
for folder in ['review_pages','free_context_pages']:
 for path in (R/folder).glob('*.json'):
  raw=json.loads(path.read_text())
  check('privacy_'+path.stem+'_'+folder,all(not {'author','steamid','profileurl','avatar','personaname'}.intersection(r) for r in raw['reviews']))
for policy in ['collection_policy.json','free_context_policy.json','probe_policy.json']:
 check(policy+'_closed',not json.loads((R/policy).read_text())['active'])
bulk=(ROOT/'network_policy.json').read_bytes()
check('bulk_stays_paused',json.loads(bulk)['network_paused'])
check('bulk_policy_unchanged',hashlib.sha256(bulk).hexdigest()==json.loads((R/'collection_policy.json').read_text())['bulk_policy_sha256'])
manifest=json.loads((R/'own_review_coding_manifest.json').read_text())
check('review_pilot_frozen',hashlib.sha256((R/'own_review_pilot.json').read_bytes()).hexdigest()==manifest['pilot_sha256'])
check('review_labels_frozen',hashlib.sha256((R/'own_review_pilot_labels.csv').read_bytes()).hexdigest()==manifest['labels_sha256'])
labels=pd.read_csv(R/'own_review_pilot_labels.csv');check('100_pilot_records',len(labels)==labels.review_id.nunique()==100)
check('figures_exist',all((R/f'{n}.{e}').stat().st_size>1000 for n in ['age_comparison','own_game_history'] for e in ['png','svg']))
for s in R.glob('*.py'):ast.parse(s.read_text())
check('scripts_parse',True)
summary=dict(passed=sum(checks.values()),checks=checks,review_collection_requests=len(calls),feasibility_requests=probe_calls,
 unique_reviews=len(unique),primary_paid_reviews=len(paid),free_context_reviews=len(f),
 main_summary_differences=[dict(appid=a['appid'],collected=a['collected'],summary_total=a['summary_total']) for a in status if a['collected']!=a['summary_total']],
 source_hashes={n:hashlib.sha256((R/n).read_bytes()).hexdigest() for n in ['targets.parquet','reviews.parquet','free_context_reviews.parquet']},
 scripts={s.name:hashlib.sha256(s.read_bytes()).hexdigest() for s in R.glob('*.py')})
(R/'validation.json').write_text(json.dumps(summary,indent=2))
print(json.dumps({k:v for k,v in summary.items() if k not in ['checks','source_hashes','scripts']},indent=2))
