#!/usr/bin/env python3
"""Read-only saved-byte audit. Does not import any runtime, runner or Provider.
All new files stay beside this script; source ZIPs and prior reports are read-only.
"""
from pathlib import Path
from collections import defaultdict, Counter
import sys,json,csv,hashlib,statistics,math,bisect,re,sqlite3,subprocess,unicodedata
HERE=Path(__file__).resolve().parent;ROOT=HERE.parents[1]
sys.path.insert(0,str(ROOT/'tools'));import guitaboo_postprocess as g
OLD=ROOT/'reports/guitaboo-deep-analysis-20261107-delivery'
REVIEW=ROOT/'reports/guitaboo-probe-decision-review-20261107'
PERF=REVIEW/'performance-audit'
FROZEN='9a5b577b77a6a87efcd8e6e218693af6faed17ce'
def dump(p,x):g.dump(p,x)
def csvout(p,x):g.csvout(HERE/p,x)
def rows(p):return list(csv.DictReader(open(p)))
def pct(v,p):
 v=sorted(v)
 if not v:return None
 k=(len(v)-1)*p;a=math.floor(k);b=math.ceil(k);return v[a]+(v[b]-v[a])*(k-a)
def sha(p):return hashlib.sha256(Path(p).read_bytes()).hexdigest()
def objref(src,ref):return src.obj(ref.split('!',1)[1])if ref and '!'in ref else None
def props(raw):return dict(l.split('=',1)for l in raw.splitlines()if '='in l and not l.startswith('#'))
def compact(e):return {k:v for k,v in e.items()if k not in ['_ref','_line']}
def main():
 summaries=[json.load(open(p))for p in OLD.glob('run_*/summary.json')];summaries=[s for s in summaries if not s['reference']]
 summaries.sort(key=lambda s:(s['app'],s['condition']));assert len(summaries)==15
 oldprovider={(r['run_id'],r['request_id']):r for r in rows(PERF/'provider-latency.csv')}
 gateway_calls={c['id']:c for c in json.load(open(PERF/'gateway-call-records.json'))['calls']}
 probes=json.load(open(REVIEW/'probe-decision-audit.json'));assert len(probes)==150
 allreq=[];distributions=[];scenarios=[];configs=[];checks=[];lifecycles=[];cases=[];rankings=[];probestore=[]
 casekeys={('activitydiary',475),('activitydiary',564),('activitydiary',1201),('activitydiary',977),('activitydiary',1284),('omninotes',246),('omninotes',664),('omninotes',1531),('scarletnotes',130),('wikipedia',1492)}
 # Add one repeated Diary and Map from the same Jev run, selected deterministically.
 for label in ['Diary','Map']:
  match=[p for p in probes if p['run_id']=='run_2b38f68437b6480c8f26d01cec61b440'and label.lower()==p['control'].lower()and p['previous_equivalent_execution']=='YES']
  if not match:match=[p for p in probes if p['run_id']=='run_2b38f68437b6480c8f26d01cec61b440'and label.lower()in p['control'].lower()and p['previous_equivalent_execution']=='YES']
  if match:casekeys.add(('activitydiary',min(match,key=lambda p:p['step'])['step']))
 for s in summaries:
  rid=s['run_id'];src=g.Source(s['source']);print(s['app'],s['condition'],flush=True)
  events=[]
  for ln,l in enumerate(src.read(src.event_path).decode().splitlines(),1):
   e=json.loads(l);e['_line']=ln;e['_ref']=src.ref(src.event_path,ln);events.append(e)
  be=defaultdict(list)
  for e in events:be[e['event']].append(e)
  cfg=props(src.read('ape.properties').decode());pc=be['PROBE_CONFIG'][0]if be['PROBE_CONFIG']else{}
  frozenconfig=src.obj('config.snapshot.json');ack=src.obj('native-config.receipt.json')
  assert frozenconfig['code']['commit']==FROZEN and frozenconfig['artifacts']['jar']==s['jar_sha256']
  assert ack['native_acknowledged']and ack['properties_sha256']==hashlib.sha256(src.read('ape.properties')).hexdigest()
  configs.append({'run_id':rid,'app':s['app'],'condition':s['condition'],'jar_sha256':s['jar_sha256'],'commit':s['code']['commit'],'probeTimeoutMs':cfg.get('ape.jev.probeTimeoutMs'),'httpLifetimeMs':cfg.get('ape.jev.timeoutMs'),'synchronous':pc.get('semantic_review_synchronous'),'historyAudit':cfg.get('ape.jev.historyAudit','false (default)'),'config_ref':src.ref('ape.properties'),'event_ref':pc.get('_ref'),'fail_open_events':len(be['SEMANTIC_FAIL_OPEN']),'retained_reuse_event_count':None,'retained_reuse_count_limitation':'No dedicated retained-reuse event exists in the frozen source. Check DEADLINE and consume review bounds instead.'})
  configs[-1].update({'archive_commit_matches_frozen':True,'archive_jar_sha_matches_summary':True,'native_config_acknowledged':ack['native_acknowledged'],'native_ack_properties_sha_matches':True,'archive_config_snapshot_ref':src.ref('config.snapshot.json'),'native_config_ack_ref':src.ref('native-config.receipt.json')})
  reviewends={e['review_id']:e for e in be['SEMANTIC_REVIEW_TIMING']};reviewrequests=defaultdict(list)
  for e in be['JEV_REQUEST_SENT']:reviewrequests[e.get('review_id')].append(e)
  consumes=defaultdict(list);waits=defaultdict(list)
  for e in be['SEMANTIC_RESPONSE_CONSUMED']:consumes[e['request_id']].append(e)
  for e in be['SEMANTIC_WAIT_STARTED']:waits[e['request_id']].append(e)
  reqtable={r['request_id']:r for r in rows(OLD/rid/'requests.csv')if r['network']=='True'}
  ordinary=[e for e in be['SELECTION_DECISION']if e.get('selection_strategy')!='PROBE'];ordinaryns=[e['monotonic_ns']for e in ordinary]
  rr=[]
  for sent in be['JEV_REQUEST_SENT']:
   qid=sent['request_id'];old=oldprovider[(rid,qid)];rq=reqtable[qid];receipt=objref(src,rq['request_ref']);response=objref(src,rq['response_ref'])
   gateway=gateway_calls[old['gateway_call_id']]
   assert gateway['run_id']==rid and gateway['body_sha256']==sent['input_fingerprint']
   queue=gateway['sent_at']-gateway['received_at'];upstream=gateway['completed_at']-gateway['sent_at']
   assert abs(queue-float(old['gateway_queue_s']))<1e-7 and abs(upstream-float(old['gateway_upstream_s']))<1e-7
   end=reviewends.get(sent.get('review_id'));w=waits[qid][0]if waits[qid]else None;c=consumes[qid][0]if consumes[qid]else None
   measure=end['provider_wait_ns']/1e9 if end and len(reviewrequests[sent['review_id']])==1 else None
   dur=response['wait_ns']/1e9 if response and response.get('wait_ns')is not None else None
   ix=bisect.bisect_right(ordinaryns,w['monotonic_ns'])if w else len(ordinary);nxt=ordinary[ix]if ix<len(ordinary)else None
   between=[a for a in ordinary[ix:]if c and a['monotonic_ns']<c['monotonic_ns']]
   r={'run_id':rid,'app':s['app'],'condition':s['condition'],'request_id':qid,'step':sent['step'],'review_id':sent.get('review_id'),'wait_mode':sent.get('wait_mode'),
    'timeout_ms':receipt.get('timeout_ms'),'request_lifetime_ms':receipt.get('request_lifetime_ms'),'late_response_reuse':receipt.get('late_response_reuse'),
    'complete_receipt_s':dur,'ape_measured_await_s':measure,'wait_started_monotonic_ns':w['monotonic_ns']if w else None,'response_consumed_monotonic_ns':c['monotonic_ns']if c else None,
    'await_envelope_s':(c['monotonic_ns']-w['monotonic_ns'])/1e9 if w and c else None,'review_prepare_s':end['prepare_ns']/1e9 if end else None,
    'review_local_excluding_await_s':(end['prepare_ns']-end['provider_wait_ns'])/1e9 if end else None,
    'ordinary_selections_before_consume':len(between)if w and c else None,'next_ordinary_step':nxt['step']if nxt else None,'next_ordinary_after_wait_started_s':(nxt['monotonic_ns']-w['monotonic_ns'])/1e9 if nxt and w else None,
    'next_ordinary_after_consume_s':(nxt['monotonic_ns']-c['monotonic_ns'])/1e9 if nxt and c else None,
    'consumed_within_submitting_review':bool(c and end and c['step']==sent['step']and c['monotonic_ns']<=end['monotonic_ns']),
    'gateway_queue_s':queue,'gateway_upstream_s':upstream,'gateway_call_id':old['gateway_call_id'],'gateway_run_and_fingerprint_verified':True,'gateway_snapshot_ref':str(PERF/'gateway-call-records.json')+'#'+gateway['id'],'http_status':response.get('http_status'),
    'request_ref':rq['request_ref'],'response_ref':rq['response_ref'],'sent_ref':sent['_ref'],'wait_ref':w['_ref']if w else None,'consume_ref':c['_ref']if c else None,'timing_ref':end['_ref']if end else None,'next_ordinary_ref':nxt['_ref']if nxt else None}
   allreq.append(r);rr.append(r)
  for r in rr:
   if r['ape_measured_await_s']is not None:
    assert abs(r['ape_measured_await_s']-float(oldprovider[(rid,r['request_id'])]['ape_blocking_await_s']))<1e-7
    assert r['ordinary_selections_before_consume']==0 and r['consumed_within_submitting_review']
  dist={'run_id':rid,'app':s['app'],'condition':s['condition'],'requests':len(rr),'measured_await_requests':sum(r['ape_measured_await_s']is not None for r in rr),'gateway_queue_total_s':sum(r['gateway_queue_s']for r in rr),'gateway_upstream_total_s':sum(r['gateway_upstream_s']for r in rr),'config_timeout_ms':pc.get('timeout_ms')}
  for field,label in [('complete_receipt_s','complete'),('ape_measured_await_s','blocking')]:
   vs=[r[field]for r in rr if r[field]is not None];dist[label+'_total_s']=sum(vs)if vs else None
   for p in [.5,.9,.95,.99]:dist[label+'_p'+str(int(p*100))+'_s']=pct(vs,p)
   dist[label+'_max_s']=max(vs)if vs else None
   for cap in [3,5,8,10]:
    dist[f'{label}_gt{cap}_count']=sum(v>cap for v in vs)if vs else None
    dist[f'{label}_gt{cap}_total_s']=sum(v for v in vs if v>cap)if vs else None
    dist[f'{label}_excess_above{cap}_s']=sum(max(0,v-cap)for v in vs)if vs else None
  distributions.append(dist)
  for cap in [3,5,8,10]:
   known=[r for r in rr if r['ape_measured_await_s']is not None]
   scenarios.append({'run_id':rid,'app':s['app'],'condition':s['condition'],'soft_budget_s':cap,'requests':len(rr),'known_requests':len(known),
    'complete_within_cap_count':sum(r['complete_receipt_s']<=cap for r in rr)if rr else None,'complete_late_count':sum(r['complete_receipt_s']>cap for r in rr)if rr else None,
    'await_within_cap_count':sum(r['ape_measured_await_s']<=cap for r in known)if known else None,'await_late_count':sum(r['ape_measured_await_s']>cap for r in known)if known else None,
    'network_await_only_excess_s':sum(max(0,r['ape_measured_await_s']-cap)for r in known)if known else None,
    'prepare_budget_excess_s':sum(max(0,r['review_prepare_s']-cap)for r in known)if known else None,
    'prepare_finishes_within_cap_count':sum(r['review_prepare_s']<=cap for r in known)if known else None,
    'state_departure_before_late_reply_count':None,'limitation':'Frozen timeline arithmetic only. prepare includes local work; future navigation, cancellation, reuse and throughput are not simulated.'})
  # All 150 confirmed Probes: independently join Native transition and receipt.
  natives=defaultdict(list)
  for e in be['NATIVE_TRANSITION']:natives[e['transition_id']].append(e)
  execreceipts={}
  for n in src.names:
   if '/jev-receipts/transition-'in n:
    x=src.obj(n);execreceipts[x.get('transition_id')]=(n,x)
  for p in [p for p in probes if p['run_id']==rid]:
   pe=[e for e in be['PROBE_EXECUTED']if e.get('entry_id')==p['entry_id']];assert len(pe)==1
   e=pe[0];nt=natives.get(e['transition_id'],[]);nr=execreceipts.get(e['transition_id']);assert len(nt)==1 and nr is not None
   r={'run_id':rid,'app':s['app'],'provider':p['provider'],'execute_step':p['step'],'control':p['control'],'entry_id':p['entry_id'],'transition_id':e['transition_id'],
    'probe_input_completed_count':sum(x.get('entry_id')==p['entry_id']for x in be['PROBE_INPUT_COMPLETED']),
    'native_transition_count':len(nt),'native_input_confirmed':nt[0].get('input_confirmed'),'native_edge_id':nr[1].get('native_edge_id'),'native_history_position':nr[1].get('native_history_position'),
    'source_gui_id':nr[1].get('source_gui_id'),'target_gui_id':nr[1].get('target_gui_id'),'state_id_at_execution':e['state_id'],'action_id_at_execution':e['model_action_id'],'version_at_execution':e['abstraction_version'],
    'probe_event_ref':e['_ref'],'native_event_ref':nt[0]['_ref'],'native_receipt_ref':src.ref(nr[0])}
   lifecycles.append(r)
   if (s['app'],p['step'])in casekeys and ((s['app']=='activitydiary'and p['provider']=='JEV')or s['app']!='activitydiary'):
    receipt=json.load(open(REVIEW/p['request_ref']))if not p['request_ref'].startswith('/')and '!'not in p['request_ref']else json.load(open(p['request_ref']))if '!'not in p['request_ref']else objref(src,p['request_ref'])
    facts=json.load(open(REVIEW/p['facts_ref']))if not p['facts_ref'].startswith('/')else json.load(open(p['facts_ref']))
    case={'app':p['app'],'run_id':rid,'provider':p['provider'],'step':p['step'],'control':p['control'],'request_id':p['request_id'],'request_step':p['request_step'],'state_id':p['state_id'],'model_action':p['model_action'],
     'request_ref':p['request_ref'],'facts_ref':p['facts_ref'],'lifecycle':r,'previous_equivalent_execution':p['previous_equivalent_execution'],'equivalent_history':p['equivalent_history'],
     'legal_pool_equivalent_ids':p['equivalent_history_in_legal_pool_ids'],'wire_equivalent_ids':p['equivalent_history_visible_ids'],'history_coverage':p['history_coverage'],
     'history_provenance':receipt.get('history_provenance'),'pair_decision':p['pair_decision'],'candidate_decision':p['candidate_decision'],'screenshots':p['screenshots'],'following_actions':p['following_actions'],
     'new_methods_2s':p['new_methods_2s'],'new_methods_10s':p['new_methods_10s'],'new_methods_before_next_action':p['new_methods_before_next_action']}
    cases.append(case)
  checks.append({'run_id':rid,'event_requests':len(rr),'all_requests_one_per_review':all(len(reviewrequests[r['review_id']])==1 for r in rr),
   'raw_receipts_match_previous_latency':all(abs(r['complete_receipt_s']-float(oldprovider[(rid,r['request_id'])]['native_submit_to_complete_s']))<1e-6 for r in rr),
   'no_ordinary_selection_during_wait_envelope':all(r['ordinary_selections_before_consume']==0 for r in rr),
   'probe_count':len([p for p in probes if p['run_id']==rid]),'event_probe_count':len(be['PROBE_EXECUTED']),
   'fail_open_events':[compact(e)for e in be['SEMANTIC_FAIL_OPEN']],
   'late_related_events':dict((k,len(v))for k,v in be.items()if any(w in k for w in ['RETAIN','LATE','ARRIVAL'])),
   'arrival_files':len([n for n in src.names if '/semantic-arrival-'in n]),'history_audit_files':len([n for n in src.names if 'history-audit-'in n])})
  src.close()
 csvout('request-waits.csv',allreq);csvout('wait-distribution.csv',distributions);csvout('soft-timeout-scenarios.csv',scenarios);csvout('run-configs.csv',configs);csvout('probe-native-lifecycle.csv',lifecycles)
 dump(HERE/'evidence/timing-checks.json',checks);dump(HERE/'evidence/history-cases.json',cases)
 print('TOTAL',len(allreq),len(lifecycles),'cases',len(cases),flush=True)
 return summaries,allreq
if __name__=='__main__':main()
