import csv
import hashlib
import json
from pathlib import Path
import statistics
import subprocess

R=Path(__file__).parent
rows=[];details={}
for name in ['two','one']:
    d=json.loads((R/(name+'.json')).read_text())
    ra,rb=d['resources'];resource_dt=rb['at']-ra['at']
    thread_rows=[]
    for tid,b in rb['threads'].items():
        a=ra['threads'].get(tid)
        if a and a['start']==b['start']:
            thread_rows.append(dict(tid=tid,pid=b['pid'],comm=b['comm'],
                cpu_percent=(b['ticks']-a['ticks'])/rb['ticks_per_second']/resource_dt*100,
                runqueue_wait_ms=(b['runqueue_wait_ns']-a['runqueue_wait_ns'])/1e6))
    details[name]=dict(resources=dict(cpu_cores=(rb['cpu']['usage_usec']-ra['cpu']['usage_usec'])/1e6/resource_dt,
        cpu_throttle_delta=rb['cpu'].get('nr_throttled',0)-ra['cpu'].get('nr_throttled',0),
        memory_mib=[r['memory']/2**20 for r in [ra,rb]],
        gpu_samples=[r['gpu'] for r in [ra,rb]],
        top_threads=sorted(thread_rows,key=lambda x:x['cpu_percent'],reverse=True)[:12]),channels={})
    for src in d['samples'][0]['channels']:
        allq=[s['channels'][src] for s in d['samples']];a,b=allq[0],allq[-1];dt=b['at']-a['at']
        am,bm=a['state']['metadata_path'],b['state']['metadata_path']
        ac,bc=am['performance']['counts'],bm['performance']['counts']
        du=bm['performance']['durations'];queues=[q['state']['metadata_path']['queued'] for q in allq]
        frames=[q['redis']['frame'] for q in allq]
        objects=[len(f['objects']) for f in frames]
        observed_intervals=[]
        for previous,current in zip(frames,frames[1:]):
            seq=current['frameSeq']-previous['frameSeq']
            if seq>0:
                observed_intervals.append(dict(pts_ms_per_seq=(int(current['ptsNs'])-int(previous['ptsNs']))/seq/1e6,
                    rtp_ms_per_seq=((current['frameTime']['rtpTimestamp']-previous['frameTime']['rtpTimestamp'])%2**32)/seq/90))
        r=dict(condition=name,channel=src,seconds=dt)
        for stage in ['decoded','selected','preprocess','infer','native','result','publisher_submit']:
            r[stage+'_fps']=(bc.get(stage,0)-ac.get(stage,0))/dt
        r['redis_fps']=(b['redis']['entries']-a['redis']['entries'])/(b['redis']['at']-a['redis']['at'])
        r.update(queue_start=queues[0],queue_end=queues[-1],queue_min=min(queues),queue_max=max(queues))
        for field in ['stale','queue_drop','publisher_rejected','out_of_order']:
            r[field+'_delta']=bm.get(field,0)-am.get(field,0)
        for field in ['sampling_drop','invalid_pts','duplicate_pts','pts_reset']:
            r[field+'_delta']=bc.get(field,0)-ac.get(field,0)
        r['observation_missed_delta']=bm['performance']['observation_missed']-am['performance']['observation_missed']
        for field in ['queue','tracking','tracking_cpu','scenario','scenario_cpu','result_work','preprocess_to_infer','infer_to_native','native_to_result','result_to_publisher_submit']:
            for stat in ['mean_ms','p95_ms']:
                r[field+'_'+stat]=du.get(field,{}).get(stat)
        r.update(objects_min=min(objects),objects_max=max(objects),objects_mean=statistics.mean(objects),
            publisher_queue_max=max(s['publisher']['metrics']['queued'] for s in d['samples']),
            redis_length_min=min(q['redis']['length'] for q in allq),redis_length_max=max(q['redis']['length'] for q in allq),
            redis_pttl_min=min(q['redis']['pttl'] for q in allq),redis_pttl_max=max(q['redis']['pttl'] for q in allq),
            analytics_cpu_cores=details[name]['resources']['cpu_cores'])
        rows.append(r)
        details[name]['channels'][src]=dict(summary=r,durations=du,sparse_frame_intervals=observed_intervals,
            selected_interval_counts=[current['state']['metadata_path']['performance']['counts']['selected']-previous['state']['metadata_path']['performance']['counts']['selected'] for previous,current in zip(allq,allq[1:])],
            run_id_stable=len({q['state']['run_id'] for q in allq})==1)
summary=dict(rows=rows,details=details,limits=[
    'Decoded is the existing AI ingress counter, not a separately measured RTSP source FPS.',
    'Redis XINFO entries-added is per-channel; publisher_submit is enqueue acceptance, not Redis ACK latency.',
    'Sparse frame samples cannot resolve sub-second network bursts or full tracker candidate/history size.',
    'Wall-minus-thread CPU is not a direct GIL measurement; runnable scheduler wait excludes blocking/GIL waits.',
    'GPU is device-global and only sampled at endpoints. cgroup CPU includes Python and Triton, not only Tracking.',
    'Different video phases and internal tracker history may affect cost. No fair universal speedup claimed.',
    'Queue durations describe processed frames only. Age-discarded frames are counted in stale.',
    'Paired durations and result_work overlap; do not add them or interpret as source-to-Web delay.'
])
(R/'summary.json').write_text(json.dumps(summary,ensure_ascii=False,indent=2))
with (R/'comparison.csv').open('w',newline='') as f:
    w=csv.DictWriter(f,fieldnames=list(rows[0]));w.writeheader();w.writerows(rows)
repo=Path('/workspace/contextra-analytics-runtime-lifecycle-refactor')
files=['app/det_result_path.py','app/analysis_metrics.py','app/byte_tracking.py']
image=subprocess.check_output(['docker','exec','contextra-analytics-1','sha256sum',*[str(Path('/opt/contextra')/p) for p in files]],text=True)
hashes={p:hashlib.sha256((repo/p).read_bytes()).hexdigest() for p in files}
assert all(value in image for value in hashes.values())
(R/'source-identity.json').write_text(json.dumps(dict(local=hashes,image=image,match=True),indent=2))
for p in [R/'summary.json',R/'comparison.csv',R/'source-identity.json']:p.chmod(0o600)
print('Summary/CSV/source identity written from preserved measurements; no new runtime window')
