"""Validate causal replay and produce audit tables, frozen-region holdout, and case evidence."""
from pathlib import Path
from collections import Counter,defaultdict
import hashlib,json,csv,sys,statistics
ROOT=Path(__file__).resolve().parents[1];REPO=ROOT.parents[1];sys.path.insert(0,str(REPO))
from tools.region_flow_shadow.core import region_metrics,quantile,FlowShadow
A=ROOT/'analysis';runs=json.loads((A/'runs.json').read_text());allrows=[];checks=[];holdout=[];lineages=[];perf=[];focus=[];evolution=[];merges=[];nativecounts=[]
FOCUS={'odkcollect-Rep1':'g16s70','odkcollect-Rep2':'g11s37','activitydiary-Rep1':'g21s63','activitydiary-Rep2':'g27s96','omninotes-Rep1':'g2s5','omninotes-Rep2':'g6s25','scarletnotes-Rep1':'g40s130','scarletnotes-Rep2':'g66s196','wikipedia-Rep1':'g68s440','wikipedia-Rep2':'g86s411'}
def save(name,rows):
    (A/(name+'.json')).write_text(json.dumps(rows,ensure_ascii=False,separators=(',',':'))+'\n')
    if isinstance(rows,list) and rows:
        keys=list(dict.fromkeys(k for row in rows for k in row));f=(A/(name+'.csv')).open('w');w=csv.DictWriter(f,keys);w.writeheader();w.writerows({k:json.dumps(v,ensure_ascii=False) if isinstance(v,(list,dict)) else v for k,v in r.items()} for r in rows);f.close()
for run in runs:
    rid=run['run'];events=[json.loads(l) for l in (ROOT/run['events']).open()]
    reducer=FlowShadow(run['run_id'])
    for e in events:reducer.consume(e)
    assert len(reducer.events)==run['count']
    for interval in [100,250,500]:
        directory=A/f'replay/{rid}/{interval}';index=json.loads((directory/'index.json').read_text());pp=json.loads((directory/'performance.json').read_text());assert pp['accepted']==run['count'] and not pp['errors'];pp['run']=rid;perf.append(pp)
        previous=None;history=defaultdict(list);seq_budget=[]
        for meta in index['snapshots']:
            s=json.loads((directory/(meta['id']+'.json')).read_text());n=s['confirmed_count'];past=events[:n]
            counts=Counter((e['source_state'],e['target_state']) for e in past);tri=Counter((e['source_state'],e['model_action_identity'],e['target_state']) for e in past)
            assert s['max_event_seq']==past[-1]['event_seq'] and s['total_edge_weight']==n
            assert {(e['source'],e['target']):e['weight'] for e in s['edges']}==dict(counts)
            assert {(e['source'],e['model_action'],e['target']):e['weight'] for e in s['action_edges']}==dict(tri)
            known={st for e in past for st in [e['source_state'],e['target_state']]}
            assert set(st for r in s['communities'] for st in r['members'])==known
            assert sum(r['metrics']['source_steps'] for r in s['communities'])==n
            assert all(o['event_seq']<=s['max_event_seq'] for o in s['entry_occurrences'])
            assert all(w['observed_through_event_seq']<=s['max_event_seq'] and all(v<=s['max_event_seq'] for v in w['events']) for w in s['trajectory_windows'])
            rows=[dict(run=rid,interval=interval,snapshot=s['id'],confirmed_count=n,max_event_seq=s['max_event_seq'],community=r['id'],lineage=r['lineage_id'],lineage_status=r['lineage_status'],members=r['members'],functions=r['functions'],mixed=r['mixed_entry_functions'],navigation_candidates=r['navigation_candidate_states'],**{k:v for k,v in r['metrics'].items() if k not in ['episodes','top_paths']}) for r in s['communities']]
            allrows+=rows
            evolution.append(dict(run=rid,interval=interval,snapshot=s['id'],count=n,communities=len(rows),non_singleton=sum(len(r['members'])>=2 for r in rows),merge_count=sum(c['kind']=='merge' for c in s['lineage_events']),split_count=sum(c['kind']=='split' for c in s['lineage_events']),continued=sum(r['lineage_status'] not in ['birth','uncertain_split_merge_or_drift'] for r in rows),median_link_jaccard=statistics.median([l['jaccard'] for l in s['lineage_links']]) if s['lineage_links'] else None))
            for r in rows:history[r['lineage']].append(r)
            focal=next((r for r in rows if FOCUS[rid] in r['members']),None)
            if interval==250 and focal:
                focus.append(focal)
            if previous and interval==250:
                old={r['id']:r for r in previous['communities']};new={r['id']:r for r in s['communities']}
                for change in s['lineage_events']:
                    if change['kind']!='merge':continue
                    rr=new[change['current'][0]];previous_sets=[old[i]['members'] for i in change['previous']]
                    # Save actual intervening flow, not an invented causal explanation for Infomap's optimizer.
                    delta=events[previous['confirmed_count']:n];from_partition={st:k for k,c in enumerate(previous_sets) for st in c}
                    cross=[e for e in delta if e['source_state'] in from_partition and e['target_state'] in from_partition and from_partition[e['source_state']]!=from_partition[e['target_state']]]
                    merges.append(dict(run=rid,at=n,previous_at=previous['confirmed_count'],snapshot=s['id'],community=rr['id'],previous_members=previous_sets,new_members=rr['members'],navigation_candidates=rr['navigation_candidate_states'],mixed=rr['mixed_entry_functions'],functions=rr['functions'],cross_old_community_events=[{k:e.get(k) for k in ['event_seq','transition_identity','source_state','target_state','model_action_identity','action_type']} for e in cross],new_period_events=len(delta),previous_source_steps=[old[i]['metrics']['source_steps'] for i in change['previous']],current_source_steps=rr['metrics']['source_steps'],interpretation='Temporal evidence and flow change; not proof of unique optimization cause'))
            if n in run['checkpoints'] and interval==250:
                for r in s['communities']:
                    future=region_metrics(reducer.events[n:],r['members']);R=set(r['members'])
                    within_future=[e for e in reducer.events[n:] if e['source_state'] in R]
                    # Reset local visit segments when slicing to prevent inventing boundary entry at the cutoff.
                    holdout.append(dict(run=rid,prefix=n/run['count'],at=n,snapshot=s['id'],community=r['id'],lineage=r['lineage_id'],members=r['members'],size=len(R),training_steps=r['metrics']['source_steps'],training_recent=r['metrics']['recent_progress'],functions=r['functions'],future_source_steps=future['source_steps'],future_entries=future['boundary_entries'],future_reentries=future['reentries'],future_new_state=future['progress']['new_state'],future_new_transition=future['progress']['new_transition'],future_native_unvisited=future['progress']['native_unvisited_positive'],future_native_new_model=future['progress']['native_new_model'],future_new_targets=sorted({e['target_state'] for e in within_future if e['_new_state']}),evaluation_only=True))
            previous=s
        for lid,rr in history.items():lineages.append(dict(run=rid,interval=interval,lineage=lid,snapshots=len(rr),first_count=rr[0]['confirmed_count'],last_count=rr[-1]['confirmed_count'],observed_span=rr[-1]['confirmed_count']-rr[0]['confirmed_count'],first_members=rr[0]['members'],last_members=rr[-1]['members']))
        checks.append(dict(run=rid,interval=interval,events=run['count'],snapshots=len(index['snapshots']),graph_weight_exact=True,prefix_no_future=True,budget_conserved=True))
    nativecounts.append(dict(run=rid,events=len(events),native_selection_available=sum(bool(e.get('model_progress')) for e in events),native_new_model=sum(e.get('native_new_model_action') is True for e in events),native_selected_unvisited=sum((e.get('model_progress') or {}).get('selected_model_action_unvisited') is True for e in events)))
summary=[]
for iv in [100,250,500]:
    pp=[p for p in perf if p['interval']==iv];ss=[]
    for r in runs:ss+=json.loads((A/f'replay/{r["run"]}/{iv}/index.json').read_text())['snapshots']
    er=[r for r in evolution if r['interval']==iv]
    summary.append(dict(interval=iv,snapshots=len(ss),infomap_median_ms=statistics.median(s['infomap_ms'] for s in ss),infomap_p90_ms=quantile([s['infomap_ms'] for s in ss],.9),infomap_max_ms=max(s['infomap_ms'] for s in ss),total_compute_seconds=sum(s['total_compute_ms'] for s in ss)/1000,total_process_seconds=sum(p['wall_seconds'] for p in pp),max_process_rss_kib=max(p['process_peak_rss_kib'] for p in pp),max_snapshot_bytes=max(s['bytes'] for s in ss),total_snapshot_bytes=sum(s['bytes'] for s in ss),maximum_periodic_delay_confirmed_steps=iv-1,merges=sum(r['merge_count'] for r in er),splits=sum(r['split_count'] for r in er),median_link_jaccard=statistics.median(r['median_link_jaccard'] for r in er if r['median_link_jaccard'] is not None)))
for name,rows in [('snapshot-regions',allrows),('replay-validation',checks),('prefix-holdout',holdout),('lineage-summary',lineages),('performance-summary',summary),('snapshot-evolution',evolution),('focus-evolution',focus),('merge-evidence',merges),('model-progress-availability',nativecounts)]:save(name,rows)
save('parameters',dict(default_interval=250,reason='tradeoff; 100 minimizes step latency, 500 cheaper; not optimality proof',label_mode='ORACLE_REPLAY',native_inventory='NOT_REIMPLEMENTED',restarts='archive UNKNOWN; use certified trajectory gaps; Native distinguishes requested restart boundary'))
print('VALIDATED',len(checks),'SNAPSHOTS',len(evolution),'CONFIRMED',sum(r['count'] for r in runs));print(json.dumps(summary,indent=2))
