import bootstrap
from bootstrap import PRIOR
import itertools,json,time,resource
from pathlib import Path
from data_robust import OUT,read,sha,paths,lines,reference
from inputs import load
from app.fight_r5.core import R5Core
from app.fight_runtime import FightSettings,FightScenario
from app.contracts.frames import FrameEnvelope,PoseObservation
from app.pose_config import POSE_MODEL

review=Path('/evidence/2026-10-30_contextra-r5-review-12clips_r01/download')
expected=read(PRIOR/'reports/probe_robust_weak.json')['R5'];feedback=read(OUT/'feedback.json')
report={};total=0;all_errors=[]
for clip,spec in read(OUT/'specs.json').items():
 rows,chosen,_,_,_=load(clip);fp,bp,source=paths(clip)
 detail=review/clip/'data/R5_diagnostics.jsonl'
 ref=lines(detail) if detail.exists() else lines(bp)
 engine=R5Core();runtime=FightScenario(FightSettings.parse({'enabled':True,'engine':'r5'}))
 d=dict(labels={},feedback={},new_hit_frames=[],new_negative_events=[],lost_positive_frames=[],remaining_negative_frames=[])
 errors=[];started=time.monotonic();processed=0;timing=[];canonical_clips=[]
 outdir=OUT/'download'/clip;outdir.mkdir(exist_ok=True)
 with (outdir/'runtime_trace.jsonl').open('w') as output:
  for row,base,old in itertools.zip_longest(rows,lines(bp),ref):
   assert row is not None and base is not None and old is not None
   assert all(row[k]==base[k]==old[k] for k in ('sample','source_frame','seconds'))
   began=time.perf_counter();result,view=engine.step(row);timing.append(time.perf_counter()-began)
   ep=result['episode'];tag,ids=reference(clip,base,feedback)
   frame=FrameEnvelope('fixture',clip,'fixture',clip,1,row['sample'],round(row['seconds']*1e9),1,row['width'],row['height'],'R5')
   w,h=row['width'],row['height'];poses=[];tracking=[]
   for p in row['persons']:
    normalized=tuple(v/(w if i%2==0 else h) for i,v in enumerate(p['bbox']))
    canonical=tuple(max(0.,min(1.,v)) for v in normalized)
    if normalized!=canonical:canonical_clips.append(dict(frame=row['source_frame'],person=len(poses),raw_box=p['bbox'],canonical_box=canonical))
    # Existing native pose_decode.hpp clamps bbox; the offline cache retained raw edges.
    poses.append(PoseObservation(frame,POSE_MODEL,p['score'],canonical,
      tuple((x/w,y/h,s) for x,y,s in p['keypoints'])))
    tracking.append(p['track_id'])
   displays,events=runtime.evaluate(frame,poses,tracking);live=runtime.engine.last_result
   for key in ('hit','raw','observable'):
    if (detail.exists() or key=='hit') and result[key]!=old[key]:errors.append(dict(frame=row['source_frame'],kind='CORE_'+key))
    if live[key]!=result[key]:errors.append(dict(frame=row['source_frame'],kind='ADAPTER_'+key))
   if detail.exists() and ep!=old['episode']:errors.append(dict(frame=row['source_frame'],kind='EPISODE'))
   if bool(events)!=ep['event'] or live['episode']['event']!=ep['event']:errors.append(dict(frame=row['source_frame'],kind='ADAPTER_EVENT'))
   if events:
    assert len(events)==1 and 0<=int(events[0].object_ref)<len(poses)
    assert events[0].track_id==tracking[int(events[0].object_ref)]
   output.write(json.dumps(dict(source_frame=row['source_frame'],seconds=row['seconds'],sample=row['sample'],
       displayed=row['sample'] in chosen,hit=result['hit'],event=ep['event'],display_orders=live['display_orders']),separators=(',',':'))+'\n')
   processed+=1
   if row['sample'] not in chosen:continue
   s=d['labels'].setdefault(tag,dict(N=0,observable=0,base_hit=0,new_hit=0,base_events=0,new_events=0))
   s['N']+=1;s['observable']+=result['observable'];s['base_hit']+=base['hit'];s['new_hit']+=result['hit'];s['base_events']+=base['episode']['event'];s['new_events']+=ep['event']
   f=row['source_frame']
   if result['hit'] and not base['hit']:d['new_hit_frames'].append(f)
   if tag=='positive_reference' and base['hit'] and not result['hit']:d['lost_positive_frames'].append(f)
   if tag=='negative_reference' and result['hit']:d['remaining_negative_frames'].append(f)
   if tag=='negative_reference' and ep['event'] and not base['episode']['event']:d['new_negative_events'].append(f)
   for key in ids:
    v=d['feedback'].setdefault(key,dict(N=0,base_hit=0,new_hit=0,base_events=0,new_events=0))
    v['N']+=1;v['base_hit']+=base['hit'];v['new_hit']+=result['hit'];v['base_events']+=base['episode']['event'];v['new_events']+=ep['event']
 if d!=expected['clips'][clip]:errors.append(dict(kind='R5_AGGREGATE'))
 total+=len(chosen);all_errors.extend(dict(clip=clip,**e) for e in errors)
 timing.sort();summary=dict(status='PASS' if not errors else 'FAIL',displayed=len(chosen),processed=processed,errors=errors[:30],
    full_r5_trace=detail.exists(),aggregate=d,elapsed=time.monotonic()-started,p95_ms=timing[int(len(timing)*.95)]*1000,
    source=source,cache_sha256=spec['cache_sha256'],canonical_bbox_clips=canonical_clips)
 (outdir/'validation.json').write_text(json.dumps(summary,indent=2));report[clip]=summary
 print('CACHE',clip,summary['status'],'errors',len(errors),'displayed',len(chosen),flush=True)
assert total==18045 and len(report)==32
for name,digest in read(OUT/'baseline_fingerprints.json').items():assert sha(Path('/evidence')/name)==digest
(OUT/'download/cache_validation.json').write_text(json.dumps(dict(status='PASS' if not all_errors else 'FAIL',inputs=32,displayed=total,errors=all_errors,clips=report,maxrss_kib=resource.getrusage(resource.RUSAGE_SELF).ru_maxrss),indent=2))
raise SystemExit(bool(all_errors))
