"""Reconstruct dates of currently returned reviews, not past deleted reviews/votes."""
from pathlib import Path
import json,re,hashlib
import numpy as np
import pandas as pd
R=Path(__file__).resolve().parent
policy=json.loads((R/'collection_policy.json').read_text());assert not policy['active']
t=pd.read_parquet(R/'targets.parquet').set_index('appid',drop=False)
v=pd.read_parquet(R/'reviews.parquet')
v['created']=pd.to_datetime(v.timestamp_created,unit='s',utc=True)
v['updated']=pd.to_datetime(v.timestamp_updated,unit='s',utc=True)
v['month']=v.created.dt.strftime('%Y-%m')
status=pd.DataFrame(json.loads((R/'collection_status.json').read_text())).set_index('appid')
free_status={r['appid']:r['collected'] for r in json.loads((R/'free_context_status.json').read_text())} if (R/'free_context_status.json').exists() else {}
all_pages=list((R/'review_pages').glob('*.json'))+list((R/'free_context_pages').glob('*.json'))
asof=max(pd.Timestamp(json.loads(p.read_text())['at']) for p in all_pages)
rows=[]
for appid,a in t.iterrows():
 z=v[v.appid.eq(appid)];oldest=z.created.min();lag=(oldest-a.first_date).total_seconds()/86400 if len(z) else np.nan
 # A source date >2 days after surviving purchase reviews is not a safe debut.
 usable=bool(len(z) and lag>=-2 and lag<=14)
 row=dict(appid=appid,name=a['name'],developer=a.credit,is_free=int(a.is_free),source_release=str(a.first_date),earliest_review=str(oldest),
  earliest_lag_days=lag,clock_usable=usable,n=len(z),summary_total=int(status.loc[appid,'summary_total']),snapshot_total=int(a.reviews),
  english=int(z.language.eq('english').sum()),positive=int(z.voted_up.sum()),all_acquisition_context_reviews=free_status.get(appid),early_access_reviews=int(z.written_during_early_access.sum()),
  reviews_before_source_date=int(z.created.lt(a.first_date).sum()),reviews_more_than2d_before=int(z.created.lt(a.first_date-pd.Timedelta(days=2)).sum()),
  edited_after_creation=int(z.timestamp_updated.gt(z.timestamp_created).sum()),median_playtime_at_review_minutes=float(z.playtime_at_review_minutes.median()))
 for days in [30,90,365]:
  end=a.first_date+pd.Timedelta(days=days);mature=asof>=end
  row[f'mature{days}']=bool(mature)
  row[f'count_by_day{days}']=int(z.created.lt(end).sum()) if mature and usable else None
  row[f'first_review_clock_{days}']=int(z.created.lt(oldest+pd.Timedelta(days=days)).sum()) if len(z) and asof>=oldest+pd.Timedelta(days=days) else None
 rows.append(row)
game=pd.DataFrame(rows);game.to_csv(R/'game_history.csv',index=False)
monthly=v.groupby(['appid','game','developer','month']).agg(reviews=('recommendationid','size'),positive_now=('voted_up','sum')).reset_index()
monthly.to_csv(R/'monthly_review_arrivals.csv',index=False)

pairs=[]
for dev,cat in t[t.is_free.eq(0)].groupby('credit'):
 cat=cat.sort_values('first_date');past=[]
 for a in cat.itertuples():
  if past:
   prior=v[v.appid.isin(past)];before=prior[prior.created.lt(a.first_date)]
   now_counts=prior.groupby('appid').size().reindex(past,fill_value=0);before_counts=before.groupby('appid').size().reindex(past,fill_value=0)
   previous=past[-1];focal=game[game.appid.eq(a.appid)].iloc[0]
   pairs.append(dict(developer=dev,appid=a.appid,name=a.name,release=str(a.first_date),previous_appid=previous,previous_name=t.loc[previous,'name'],
     previous_reviews_at_launch=int(before_counts.loc[previous]),previous_reviews_now=int(now_counts.loc[previous]),
     prior_catalog_reviews_at_launch=len(before),prior_catalog_reviews_now=len(prior),prior_games=len(past),
     strongest_prior_at_launch=int(before_counts.max()),strongest_prior_now=int(now_counts.max()),
     predecessor_band_at_launch='1000+' if before_counts.loc[previous]>=1000 else ('100-999' if before_counts.loc[previous]>=100 else '<100'),
     predecessor_band_now='1000+' if now_counts.loc[previous]>=1000 else ('100-999' if now_counts.loc[previous]>=100 else '<100'),
     count_by_day30=focal.count_by_day30,count_by_day90=focal.count_by_day90,count_by_day365=focal.count_by_day365,
     current_reviews=int(focal.n),clock_usable=bool(focal.clock_usable)))
  past.append(a.appid)
pd.DataFrame(pairs).to_csv(R/'historical_pairs.csv',index=False)
if (R/'free_context_reviews.parquet').exists():
 f=pd.read_parquet(R/'free_context_reviews.parquet');f['created']=pd.to_datetime(f.timestamp_created,unit='s',utc=True)
 contexts=[]
 for pair in pairs:
  if pair['developer']!='Desert Fox':continue
  launch=pd.Timestamp(pair['release']);earlier=t[t.credit.eq('Desert Fox')&t.is_free.eq(1)&t.first_date.lt(launch)]
  z=f[f.appid.isin(earlier.appid)]
  contexts.append(dict(game=pair['name'],selected_paid_reviews_before=pair['prior_catalog_reviews_at_launch'],
       additional_currently_free_game_reviews_before=int(z.created.lt(launch).sum()),currently_free_games=' | '.join(earlier['name']),
       note='Paid-game channel uses Steam purchases; free-game supplement uses all acquisition types. These are selected review records, not buyers or a comparable paid-only total.'))
 pd.DataFrame(contexts).to_csv(R/'free_catalog_context.csv',index=False)

# Histogram discrepancies are measured against like-source dated review arrivals.
hist=[]
for appid,prefix in [(760330,'byte'),(915310,'snkrx')]:
 raw=json.loads((R/f'{prefix}_hist.json').read_text())['response']['results']['rollups']
 h={pd.to_datetime(r['date'],unit='s',utc=True).strftime('%Y-%m'):r['recommendations_up']+r['recommendations_down'] for r in raw}
 current=v[v.appid.eq(appid)].groupby('month').size().to_dict()
 for month in sorted(set(h)|set(current)):hist.append(dict(appid=appid,month=month,histogram_count=h.get(month,0),dated_steam_purchase_reviews=current.get(month,0)))
pd.DataFrame(hist).to_csv(R/'histogram_audit.csv',index=False)

# Topic screens find passages; they are not automatic judgments of motivation.
patterns={
 'build':r'\b(?:builds?|theorycraft\w*|synerg\w*|skill tree|passive tree|classes|class bonuses|team comp\w*)\b',
 'execution':r'\b(?:controls?|steer\w*|turning|turn radius|movement|dodg\w*|maneuv\w*|manoeuv\w*)\b',
 'presentation':r'\b(?:music|soundtrack|audio|sound|visuals?|graphics|aestheti\w*|juice|juicy)\b',
 'repeat':r'\b(?:replay\w*|repetiti\w*|repetitive|endless|grind\w*|variety|longevity)\b',
 'author':r'\b(?:a327ex|bytepath|snkrx|rad codex|voidspire|alvora|horizon.s gate|azalea|kingsvein|zachtronic\w*|zach[- ]?like|previous games?|other games?|same dev\w*|fan of)\b'
}
en=v[v.language.eq('english')].copy()
for key,pattern in patterns.items():en[key]=en.review.str.contains(pattern,case=False,regex=True,na=False)
en.to_parquet(R/'english_screened.parquet',index=False)
screen=[]
for appid,z in en.groupby('appid'):
 for key in patterns:screen.append(dict(appid=appid,game=z.game.iloc[0],signal=key,n=len(z),matches=int(z[key].sum())))
pd.DataFrame(screen).to_csv(R/'topic_screen_counts.csv',index=False)

# Freeze a random 50-review sample from each a327ex game for manual semantic coding.
pilot=[]
for appid in [760330,915310]:
 z=en[en.appid.eq(appid)].sample(n=50,random_state=906733).sort_values('timestamp_created')
 for a in z.itertuples():pilot.append(dict(appid=appid,game=a.game,review_id=a.recommendationid,text=a.review))
(R/'own_review_pilot.json').write_text(json.dumps(pilot,ensure_ascii=False,indent=2))
(R/'analysis_spec.json').write_text(json.dumps(dict(asof=str(asof),topic_patterns=patterns,
 history='Counts of currently returned Steam-purchase reviews created before an event date. Deleted/hidden/filtered-out reviews cannot be reconstructed. Current vote and current text can have been edited.',
 timing='Source release clock reported only if oldest surviving purchase review is within -2 to +14 days of source date; otherwise a first-observed-review clock is a separate diagnostic, not a replacement launch date.',
 selection='Five purposive catalogs plus one authorship comparison. Primary pair/age comparisons retain 24 currently paid games and the Steam-purchase channel. Three currently free Desert Fox games are separate context with all acquisition channels; their Steam-purchase count can be zero despite other reviews. Not Steam-wide probabilities or causal impact of a quiet release.',
 own_pilot='50 uniformly sampled English Steam-purchase reviews per game. Numerical vote and playtime hidden from the manual coding packet; text can reveal sentiment. Single-reader descriptive coding.',
 own_pilot_sha256=hashlib.sha256((R/'own_review_pilot.json').read_bytes()).hexdigest()),indent=2))
print('HISTORY\n',game[['name','n','english','summary_total','earliest_lag_days','clock_usable','count_by_day30','count_by_day90','count_by_day365']].round(2).to_string(index=False))
print('PAIRS\n',pd.DataFrame(pairs)[['developer','name','previous_name','previous_reviews_at_launch','previous_reviews_now','prior_catalog_reviews_at_launch','prior_catalog_reviews_now','count_by_day90']].to_string(index=False))
