import hashlib
import json
import math
import os
import statistics
from collections import Counter
from datetime import datetime
from pathlib import Path
from zoneinfo import ZoneInfo

ROOT=Path(__file__).resolve().parents[1]
os.umask(0o077)
def load(name):return json.loads((ROOT/'private'/name).read_text())
def kst(ms):return datetime.fromtimestamp(ms/1000,ZoneInfo('Asia/Seoul')).isoformat(timespec='milliseconds')
before=load('settings_before.json');after=load('followup.json');raw=load('monitor.json')
frames=raw['frames'];snapshots=[x['snapshot'] for x in frames]
assert 30<=raw['elapsed_seconds']<=60 and frames
assert all(str(x['cameraId'])=='24' for x in snapshots)
assert len({(x['runId'],x['runGeneration'],x['configVersion']) for x in snapshots})==1
assert all(b['ptsNs']>a['ptsNs'] and b['frameSeq']>a['frameSeq'] for a,b in zip(snapshots,snapshots[1:]))
assert before['settings']==after['settings']
assert before['runtime']['runId']==after['runtime']['runId']
assert before['container_evidence']==after['container_evidence']==raw['container_evidence']

def fight(obj):return any(x.get('scenario')=='FIGHT' and x.get('state')=='FIGHT' for x in obj.get('scenarioResults',[]))
flags=[any(fight(o) for o in s['objects']) for s in snapshots]
all_objects=[o for s in snapshots for o in s['objects']]
mismatch=sum((o['color'].upper()=='#FF3030')!=fight(o) for o in all_objects)
assert mismatch==0
assert all(len(o['bbox'])==4 and all(math.isfinite(x) and 0<=x<=1 for x in o['bbox']) for o in all_objects)
intervals=[];start=None
for i,flag in enumerate(flags+[False]):
    if flag and start is None:start=i
    if not flag and start is not None:
        end=i-1
        intervals.append({'first_frame_seq':snapshots[start]['frameSeq'],'last_frame_seq':snapshots[end]['frameSeq'],
                          'first_observed_kst':kst(frames[start]['observed_at_ms']),
                          'last_observed_kst':kst(frames[end]['observed_at_ms']),
                          'observations':end-start+1,'right_censored_by_capture_end':end==len(flags)-1})
        start=None
events={e['eventId']:e for s in snapshots for e in s.get('events',[]) if e.get('scenario')=='FIGHT'}
ledger={e['event_id']:e for e in after['events']}
new_events=[]
for eid,event in events.items():
    row=ledger[eid]
    new_events.append({'event_id':eid,'frame_seq':event['frameSeq'],'pts_ns':event['ptsNs'],
                       'ai_recorded_kst':kst(event['aiRecordedAtWallMs']),
                       'artifact_state':row['artifact_state'],'delivery_state':row['delivery_state'],
                       'http_status':row['http_status'],'media':row['media']})
lines=load('web_fight_db.json')['data']['stdout'].splitlines()
rows=[json.loads(x) for x in lines if x.startswith('{')]
scenarios=[x for x in rows if x.get('code')=='FIGHT']
assert len(scenarios)==1 and scenarios[0]['enabled']==1
request={'scenarioCode':'FIGHT','enabled':True,'rois':[], 'parameters':scenarios[0]['parameters']}
assert not any('roi_id' in x for x in rows)
assert set(request['parameters'])=={'person_confidence','pose_confidence','cooldown_seconds','pair_distance_ratio','pair_close_hold_seconds','arm_motion_ratio','required_bursts'}

pts_gaps=[(b['ptsNs']-a['ptsNs'])/1e9 for a,b in zip(snapshots,snapshots[1:])]
lag=[x['observed_at_ms']-x['snapshot']['ingestTimeWallMs'] for x in frames if x['snapshot'].get('ingestTimeWallMs')]
source_lag=[x['observed_at_ms']-x['snapshot']['frameTime']['detectedAtWallMs'] for x in frames if (x['snapshot'].get('frameTime') or {}).get('detectedAtWallMs')]
rawjoints=[k for o in all_objects for k in o.get('keypoints',[])]
priority=[k for o in all_objects for i,k in enumerate(o.get('keypoints',[])) if i in (5,6,7,8,9,10,11,12)]
stats={
 'camera':24,'start_kst':kst(raw['started_ms']),'end_kst':kst(raw['ended_ms']),
 'duration_seconds':raw['elapsed_seconds'],'metadata_observations':len(frames),
 'observed_metadata_rate':len(frames)/raw['elapsed_seconds'],
 'provider_models':dict(Counter(s.get('providerModel') for s in snapshots)),
 'run_count':len({s['runId'] for s in snapshots}),'run_generation':snapshots[0]['runGeneration'],
 'settings_unchanged':True,'containers_unchanged':True,'read_errors':raw['read_errors'],
 'display_fight_observations':sum(flags),'display_fight_percent':100*sum(flags)/len(flags),
 'display_intervals':intervals,'new_events':new_events,'color_state_mismatches':mismatch,
 'objects_per_observation':dict(Counter(len(s['objects']) for s in snapshots)),
 'fight_objects_per_positive_observation':dict(Counter(sum(fight(o) for o in s['objects']) for s in snapshots if any(fight(o) for o in s['objects']))),
 'pts_gap_seconds':{'min':min(pts_gaps),'median':statistics.median(pts_gaps),'max':max(pts_gaps)},
 'ingest_to_observe_ms':{'median':statistics.median(lag),'max':max(lag)},
 'source_time_to_observe_ms':{'median':statistics.median(source_lag),'max':max(source_lag),'interpretation':'External source anchor is ESTIMATED; not proven processing latency.'},
 'time_sync_states':dict(Counter(s['frameTime']['syncStatus'] for s in snapshots)),
 'priority_joint_slots':len(priority),'priority_joint_score_ge030':sum(k[2]>=.3 for k in priority),
 'priority_joint_score_ge010':sum(k[2]>=.1 for k in priority),
 'joint_score_counts_note':'Available published object slots only; not person recall or a rerun of R5 at another threshold.',
 'effective':before['settings']['effective'],'saved_event_cooldowns':after['saved_event_cooldowns'],
 'web_fight_item':request,
 'request_evidence':'Reconstructed from current Web DB and source assembly/catalog; no raw HTTP request packet captured.',
 'limits':['FIGHT color/state includes display hold; not exact current-rule-positive count.',
           'Only existing event clip/thumbnail samples inspected, not a full 45-second RGB recording.',
           'No model/Pose/Tracking/rule rerun or config changes; no browser pixel capture.',
           'New event is at observation end; initial ledger SELECT was before asynchronous acceptance. Follow-up confirms READY/SENT.']}
(ROOT/'shared/summary.json').write_text(json.dumps(stats,ensure_ascii=False,indent=2)+'\n')
(ROOT/'shared/web_fight_start_item.json').write_text(json.dumps(request,indent=2)+'\n')
(ROOT/'shared/camera24_metadata_45s.json').write_text(json.dumps({k:raw[k] for k in ('started_ms','ended_ms','elapsed_seconds','frames')},ensure_ascii=False)+'\n')
print(json.dumps({k:stats[k] for k in ('start_kst','end_kst','metadata_observations','observed_metadata_rate','display_fight_observations','display_fight_percent','display_intervals','new_events','objects_per_observation','pts_gap_seconds','ingest_to_observe_ms','source_time_to_observe_ms','priority_joint_slots','priority_joint_score_ge030','priority_joint_score_ge010')},ensure_ascii=False,indent=2))
