from preflight import R,g,HEAD
from pathlib import Path
import csv,json,shutil,time,urllib.request
plan=json.loads((R/'plan.json').read_text());run=json.loads((R/'run.json').read_text());c=g.inspect();built=json.loads((R/'build.json').read_text())
assert run['window_result']=='PASS' and run['cleanup_verified'] and run['final']=='HEALTHY_STOPPED_NEW_IMAGE'
assert c['Image']==built['image_id'] and c['State']['Health']['Status']=='healthy' and c['HostConfig']['RestartPolicy']==plan['normal_restart']
assert g.sha(Path(plan['overlay']))==plan['overlay_hash'] and g.others()==plan['other_containers']
assert g.sha(g.CFG)==plan['config_hash'] and g.sha(g.CFG.parent/'2/model.plan')==plan['plan_hash']
assert all(g.sha(Path(p))==h for p,h in plan['pose_hashes'].items())
model=plan['fire_model'];assert g.sha(Path(model['directory'])/'2/model.plan')==model['plan_sha256'] and g.sha(Path(model['directory'])/'config.pbtxt')==model['config_hash']
svc=g.yaml.safe_load((g.ENV.parent/'metadata-active/compose.yaml').read_text())['services']['analytics']
assert svc['image']==c['Image'] and svc['restart']=='on-failure:3' and g.load_env_file(str(g.ENV))['CONTEXTRA_IMAGE']==built['tag']
assert not any(m['Destination'].startswith('/run/contextra-') for m in c['Mounts'])
env=g.load_env_file(str(g.ENV));op=urllib.request.build_opener(urllib.request.ProxyHandler({}))
with op.open('http://127.0.0.1:'+env['FASTAPI_HOST_PORT']+'/api/v1/streams',timeout=3) as f:streams=json.load(f)['streams']
assert len(streams)==1 and streams[0]['state']=='stopped'
names=g.sh(['docker','ps','-a','--format','{{.Names}}']).splitlines();assert not any(n.startswith('codex-ltb1021-intrusion-preparation-') for n in names)
start,end=run['measurement_start_ns'],run['measurement_end_ns'];seconds=(end-start)/1e9
a=run['measurement']['before']['metadata_path']['performance'];b=run['measurement']['after']['metadata_path']['performance']
counts={k:b['counts'].get(k,0)-a['counts'].get(k,0) for k in ('decoded','selected','preprocess','infer','native','result','publisher_submit')}
rates={k:v/seconds for k,v in counts.items()}
pub0=next(x for x in run['observations'] if x['monotonic_ns']==start)['publisher']['metrics']
pub1=next(x for x in run['observations'] if x['monotonic_ns']==end)['publisher']['metrics']
rates['redis_sent']=(pub1['sent_frames']-pub0['sent_frames'])/seconds
rows=[x for x in run['observations'] if start<=x['monotonic_ns']<=end]
durations=b['durations'];after=run['measurement']['after']['metadata_path']
summary=dict(head=HEAD,source_tests=17,image_tests=17,baseline_frames_each=120,builds=1,starts=1,stops=1,failures=0,image=built,
 changes=['Frame expiration once; idle/stale expiration outside queue lock','only expired IDs copied; full scan remains','engine-local immutable ROI edges; public config unchanged'],
 runtime=dict(status='PASS',measurement_seconds=seconds,counts=counts,fps=rates,latency_ms=durations,redis_frames_observed=run['redis_frames'],same_frame_samples=len(run['same_frame_matches']),events_observed=run['events_observed'],event_delivery='NOT_SUBMITTED/URI null',queue_drop=after.get('queue_drop',0),stale=after.get('stale',0),max_observed_queue=max(x['metadata_path'].get('queued',0) for x in rows),observation_missed=b['observation_missed'],cleanup_s=run['stop_status']['last_cleanup']['elapsed_s'],eof='not reached; explicit Stop',visual='Off; JSON3 during warmup only',control={x['action']:dict(http=x['http'],response_ms=(x['returned_ns']-x['requested_ns'])/1e6) for x in run['calls']}),
 final=dict(analytics='healthy',stream='stopped',restart='on-failure:3 actual/persistent',original_camera='CAS restored: Medium RTSP/display.25/10fps/Anchor',model_mounts='Medium/Pose/Fire retained unchanged',temporary_mounts='removed',detail_capture='Off',non_targets='unchanged',task_containers=0,gpu_mib=g.gpu(),disk_free_gib=shutil.disk_usage(g.sh(['docker','info','--format','{{.DockerRootDir}}']).strip()).free/1024**3),
 limits=['no isolated speedup benchmark; previous source segment differs','result_work contains Tracking/Scenario; stage elapsed includes pacing/wait; do not sum as compute or E2E','per-frame Redis acknowledgement latency and full CPU/GPU time series not measured','no multi-channel/Artifact On/natural EOF acceptance'],next='Event/Artifact semantics and separately scoped RTSP CLI; no required blocker')
assert rates['result']>9 and not summary['runtime']['queue_drop'] and not summary['runtime']['stale']
(R/'summary.json').write_text(json.dumps(summary,indent=2))
with (R/'metrics.csv').open('w') as f:
    w=csv.writer(f);w.writerow(['kind','stage','fps','samples','mean_ms','p95_ms','max_ms'])
    for k,v in rates.items():w.writerow(['throughput',k,f'{v:.4f}','','','',''])
    for k,v in durations.items():w.writerow(['latency',k,'',v['samples'],v['mean_ms'],v['p95_ms'],v['max_ms']])
with (R/'timeline.csv').open('w') as f:
    w=csv.writer(f);w.writerow(['elapsed_s','processed','queued','queue_drop','stale','redis_sent'])
    for x in rows:
        p=x['metadata_path'];w.writerow([(x['monotonic_ns']-start)/1e9,p.get('processed',0),p.get('queued',0),p.get('queue_drop',0),p.get('stale',0),x['publisher']['metrics'].get('sent_frames',0)])
budget=json.loads((R/'budget.json').read_text());budget.update(completed_utc=time.strftime('%Y-%m-%dT%H:%M:%SZ',time.gmtime()),builds=1,starts=1,stops=1,failed_groups=0);(R/'budget.json').write_text(json.dumps(budget,indent=2))
print(json.dumps(dict(fps=rates,measurement_s=seconds,scenario_ms=durations['scenario'],cleanup=summary['runtime']['cleanup_s'],queue=summary['runtime']['max_observed_queue'],final=summary['final']),indent=2))
