from pathlib import Path
from collections import Counter,defaultdict
import json,csv,sys,math
import numpy as np
import networkx as nx
ROOT=Path(__file__).resolve().parents[1];sys.path.insert(0,str(Path(__file__).parent));from analyze import dump,csvout,overlap,metrics
runs={p.stem:json.loads(p.read_text()) for p in (ROOT/'analysis/runs').glob('*.json')};summary=[];comparisons=[];prefix=[];counterexamples=[];rep=[];overlaps=[];transitions=[];allregions=[];validation=[]
FOCUS={'odkcollect':'odkcollect.download','activitydiary':'activitydiary.add-activity','omninotes':'omninotes.settings','scarletnotes':'scarletnotes.create-note-empty-state','wikipedia':'wikipedia.global-search'}
for rid,r in sorted(runs.items()):
 es=[json.loads(l) for l in (ROOT/f'analysis/traces/{rid}.jsonl').open()];byid={e['transition_id']:e for e in es};regions=r['regions'];groups=r['entry_groups'];app=r['summary']['app']
 for method in ['A-final','A-flow','Infomap-G1','Infomap-G2','Infomap-G3','PPR-G1','PPR-G2','PPR-G3']:
  rs=[x for x in regions if x['method']==method and x['metrics']['size']>=2]
  for lo,hi in [(2,99999),(2,5),(6,15),(16,99999)]:
   zz=[z for z in rs if lo<=z['metrics']['size']<=hi]
   summary.append(dict(run=rid,method=method,size_stratum=f'{lo}-{hi}',regions=len(zz),median_size=float(np.median([z['metrics']['size'] for z in zz])) if zz else None,median_p_stay=float(np.median([z['metrics']['p_stay'] for z in zz if z['metrics']['p_stay'] is not None])) if zz else None,median_dwell_p90=float(np.median([z['metrics']['dwell_p90'] for z in zz if z['metrics']['dwell_p90'] is not None])) if any(z['metrics']['dwell_p90'] is not None for z in zz) else None,median_reentries=float(np.median([z['metrics']['confirmed_reentries_within_segment'] for z in zz])) if zz else None,union_source_steps=sum(any(e['state'] in z['members'] for z in zz) for e in es),budget_sum_warning='overlapping regions not additive'))
 for group in groups:
  if group['function']!=FOCUS[app] and not (app=='activitydiary' and group['function']=='activitydiary.edit-existing-activity'):continue
  for method in ['A-final','A-flow','Infomap-G1','PPR-G1']:
   rr=[z for z in regions if z['method']==method and (method!='PPR-G1' or z.get('entry')==group['id'])]
   def score(z):
    aa=next((a for a in z['entry_associations'] if a['entry']==group['id']),None)
    return (aa['entry_targets_in_region'] if aa else 0,aa['next5_fraction'] or 0 if aa else 0,-len(z['members']))
   if not rr:continue
   z=max(rr,key=score);aa=next((a for a in z['entry_associations'] if a['entry']==group['id']),None);comparisons.append(dict(run=rid,function=group['function'],source_activity=group['source_activity'],entry=group['id'],executions=group['executions'],method=method,region=z['id'],header=z.get('header'),members=z['members'],size=len(z['members']),target_fraction=aa['target_fraction'] if aa else 0,next5_fraction=aa['next5_fraction'] if aa else 0,source_steps=z['metrics']['source_steps'],p_stay=z['metrics']['p_stay'],dwell_p90=z['metrics']['dwell_p90'],reentries=z['metrics']['confirmed_reentries_within_segment'],late=z['metrics']['late'],best_region_selection='posthoc maximum entry-target executions; no semantic accuracy claim'))
 # Every region all memberships preserved; show overlap explicitly for PPR entry pairs, no forced partition.
 pp=[z for z in regions if z['method']=='PPR-G1']
 for i,a in enumerate(pp):
  for b in pp[i+1:]:overlaps.append(dict(run=rid,a=a['id'],function_a=a.get('function'),b=b['id'],function_b=b.get('function'),intersection=len(set(a['members'])&set(b['members'])),jaccard=overlap(a['members'],b['members'])))
 # Known shared editing exact state across source-Activity-compatible Add/Edit groups.
 if app=='activitydiary':
  adds=[g for g in groups if g['function']=='activitydiary.add-activity'];edits=[g for g in groups if g['function']=='activitydiary.edit-existing-activity']
  for a in adds:
   for b in edits:
    if a['source_activity']!=b['source_activity']:continue
    for t in sorted(set(a['targets'])&set(b['targets'])):
     ae=next(byid[x] for x in a['events'] if byid[x]['target_state']==t);be=next(byid[x] for x in b['events'] if byid[x]['target_state']==t)
     counterexamples.append(dict(run=rid,kind='shared_edit_target',target=t,add=ae['transition_id'],edit=be['transition_id'],add_order=ae['order'],edit_order=be['order'],source_activity=a['source_activity'],different_entry_modes=True,shared_region_ids=[z['id'] for z in pp if t in z['members']]))
 # Flow between Infomap modules, only confirmed links (including Back, selfloops separately).
 im=[z for z in regions if z['method']=='Infomap-G1'];assignment={s:z['id'] for z in im for s in z['members']};assert set(assignment)==set(r['states'])
 cc=Counter((assignment[e['state']],assignment[e['target_state']],e['action_type']=='MODEL_BACK') for e in es)
 transitions.extend(dict(run=rid,source_region=u,target_region=v,back=b,executions=n) for (u,v,b),n in cc.items())
 # Prefix/final cluster stability measured only over training-observed States, not future IDs.
 for p in r['prefix']:
  if p['graph']!='G1':continue
  R=set(p['members']);future_ids={e['target_state'] for e in es[p['cutoff_order']:]}-set(s for e in es[:p['cutoff_order']] for s in [e['state'],e['target_state']])
  final=[z for z in regions if z['method']==p['method'] and (not p.get('function') or z.get('function')==p['function'])]
  best=max((overlap(R,z['members']) for z in final),default=None)
  prefix.append(dict(run=rid,prefix=p['prefix'],method=p['method'],header=p.get('header'),function=p.get('function'),source_activity=p.get('source_activity'),size=len(R),training_steps=p['training']['source_steps'],training_entries=p['training']['confirmed_entries'],holdout_steps=p['holdout']['source_steps'],holdout_entries=p['holdout']['confirmed_entries'],holdout_reentries=p['holdout']['confirmed_reentries_within_segment'],holdout_complete_dwell_p90=p['holdout']['dwell_p90'],holdout_new_state=p['holdout']['all']['new_state'],holdout_new_transition=p['holdout']['all']['new_transition'],holdout_native_new_model=p['holdout']['all']['native_new_model'],final_best_jaccard=best,future_new_states_outside_frozen_members=len(future_ids),members=p['members']))
 # Attribution failure: navigation hub in PPR, not claimed semantic false positive gold.
 hubs=[s for s,v in sorted(r['states'].items(),key=lambda x:-x[1]['visits']) if any(('MainActivity' in a or 'MainMenu' in a or 'ManageActivity' in a) for a in v['activities'])][:3]
 for p in pp:
  hit=set(hubs)&set(p['members'])
  if hit:counterexamples.append(dict(run=rid,kind='ppr_public_navigation_inclusion',region=p['id'],function=p.get('function'),hubs=sorted(hit),hub_activities={s:r['states'][s]['activities'] for s in hit},region_size=len(p['members']),budget=p['metrics']['budget_share'],interpretation='shared navigation evidence; not independent manual semantic gold'))
 # Actual fork / merge histories: two distinct observed incoming sources to a shared member, preserve real short sequences.
 for z in im:
  if z['metrics']['internal_flow']<100:continue
  candidates=defaultdict(dict)
  for e in es:
   if e['state']!=e['target_state'] and e['state'] in z['members'] and e['target_state'] in z['members']:candidates[e['target_state']].setdefault(e['state'],e)
  for target,srcs in candidates.items():
   if len(srcs)>=2:
    paths=[]
    for e in list(srcs.values())[:2]:paths.append([x['transition_id'] for x in es[max(0,e['order']-2):e['order']+1] if x['segment']==e['segment']])
    counterexamples.append(dict(run=rid,kind='observed_merge_paths',region=z['id'],target=target,paths=paths,claim='separate confirmed histories, not a fabricated single path'));break
 allregions.extend(dict(run=rid,region=z['id'],members=z['members'],**z['metrics']) for z in im if len(z['members'])>=2)
 # No pseudo adjacency at gaps; conservation/partition/source counting.
 assert sum(x['weight'] for x in r['edges'])==len(es)
 assert sum(len(z['members']) for z in im)==len(r['states'])
 for z in regions:
  R=set(z['members']);assert z['metrics']['source_steps']==sum(e['state'] in R for e in es)
 for p in r['prefix']:
  cut=p['cutoff_order'];seen={s for e in es[:cut] for s in [e['state'],e['target_state']]};assert set(p['members'])<=seen
  assert p['holdout']['source_steps']==sum(e['state'] in p['members'] for e in es[cut:])
 validation.append(dict(run=rid,weighted_execution_conservation=True,infomap_partition_complete=True,prefix_members_observed_only=True,holdout_budget_recount=True,source_budget_recount=True))
# Cross-rep correspondences use reviewed entry function and activity, never State-ID equality.
for app in FOCUS:
 a=runs[app+'-Rep1'];b=runs[app+'-Rep2']
 for aa in [g for g in a['entry_groups'] if g['function']==FOCUS[app]]:
  for bb in [g for g in b['entry_groups'] if g['function']==FOCUS[app]]:
   if aa['source_activity']!=bb['source_activity']:continue
   rep.append(dict(app=app,function=FOCUS[app],source_activity=aa['source_activity'],rep1_entry=aa['id'],rep2_entry=bb['id'],rep1_executions=aa['executions'],rep2_executions=bb['executions'],rep1_targets=aa['targets'],rep2_targets=bb['targets'],identity_digest_intersection=len(set(aa['concrete_identities'])&set(bb['concrete_identities'])),evidence='inherited functional candidate + same source Activity + archived GUI/action; no State ID equivalence asserted',gold=False))
rankings={}
for name,key in [('internal_flow',lambda r:r['internal_flow']),('complete_dwell_max',lambda r:r['dwell_max'] or -1),('complete_dwell_p90',lambda r:r['dwell_p90'] or -1),('reentries',lambda r:r['confirmed_reentries_within_segment']),('late_discovery_high_flow',lambda r:r['late']['new_transition'] if r['internal_flow']>=100 else -1)]:rankings[name]=[dict(rank=i+1,**r) for i,r in enumerate(sorted(allregions,key=key,reverse=True)[:3])]
for name,obj in [('method-summary',summary),('focus-comparison',comparisons),('prefix-summary',prefix),('cross-rep-evidence',rep),('ppr-overlap',overlaps),('inter-region-transitions',transitions)]:dump('analysis/'+name+'.json',obj);csvout('analysis/'+name+'.csv',obj)
dump('analysis/counterexamples.json',counterexamples);dump('analysis/automatic-top3.json',rankings);dump('evidence/numerical-validation.json',dict(status='PASS',runs=validation,scope='conservation and prefix invariants, not scientific gold adjudication'))
print('SUMMARY_PASS',len(comparisons),len(prefix),len(counterexamples))
