import hashlib
import importlib.util
import json
import os
from collections import Counter
from datetime import datetime
from pathlib import Path
from types import SimpleNamespace
from zoneinfo import ZoneInfo

ROOT = Path(__file__).resolve().parents[1]
REPO = Path('/workspace/contextra-analytics-runtime-lifecycle-refactor')
os.umask(0o077)
data = json.loads((ROOT / 'private/snapshot.json').read_text())
web = json.loads((ROOT / 'private/web_events.json').read_text())
assert web['data']['exit_code'] == 0
web_rows = [json.loads(line) for line in web['data']['stdout'].splitlines() if line.startswith('{')]
assert data['before']['run_id'] == data['after']['run_id']
assert data['settings']['effective']['fight']['joint_score'] == .1
assert data['after']['observed_state'].upper() == 'RUNNING'
for name, expected in data['source_sha256'].items():
    assert hashlib.sha256((REPO / name).read_bytes()).hexdigest() == expected

rows = [r['snapshot'] for r in data['frames']]
assert len({r['runId'] for r in rows}) == 1
assert all(b['ptsNs'] > a['ptsNs'] for a,b in zip(rows, rows[1:]))
ids = Counter()
positive = 0
for row in rows:
    red = False
    for obj in row['objects']:
        for scenario in obj['scenarioResults']:
            if scenario.get('scenario') == 'FIGHT' and scenario.get('state') == 'FIGHT':
                assert obj['color'].upper() == '#FF3030'
                ids[scenario.get('eventId')] += 1
                red = True
    positive += red
assert len(ids) == 1 and None not in ids
eid = next(iter(ids))
event = next(x for x in data['recent_events'] if x['event_id'] == eid)
web_event = next(x for x in web_rows if x['external_event_id'] == eid and 'thumbnail_present' in x)
assert event['run_id'] == data['after']['run_id']
assert event['artifact_state'] == 'READY' and event['delivery_state'] == 'SENT' and event['http_status'] == 200
assert web_event['thumbnail_present'] and web_event['clip_present'] and web_event['has_artifacts']
path = data['after']['metadata_path']
assert path['events_created'] == path['event_cooldown']['counts']['FIGHT_admitted'] == 1
assert path['event_cooldown']['counts'].get('FIGHT_suppressed_candidates', 0) == 0
assert path['artifacts']['accepted'] == path['artifacts']['capture_ready'] == 1
assert path['artifacts']['error'] is None and path['artifacts']['pending'] == 0

# Exercise only the unchanged Episode class with synthetic booleans, never live frames or Events.
module_spec = importlib.util.spec_from_file_location('episode_probe', REPO / 'app/fight_r5/episode.py')
episode_module = importlib.util.module_from_spec(module_spec)
module_spec.loader.exec_module(episode_module)
policy = SimpleNamespace(max_step_gap=.15, event_cooldown=2., rearm_quiet=2., pair_retention=5., display_hold=1.)


def probe(sequence):
    episode = episode_module.Episode(policy)
    events = []
    transitions = []
    for t, hit, quiet in sequence:
        result = episode.step(t, hit, quiet)
        if result['event']:
            events.append(t)
        if result['transition']:
            transitions.append([t, result['transition']])
    return dict(events=events, transitions=transitions, latched=episode.latched,
                quiet_seconds=episode.quiet, last_valid=episode.last_valid)


intermittent = [(i/10, i%100 == 0, i%100 != 0 and i%10 in (1,2,3)) for i in range(6001)]
quiet = [(0., True, False)] + [(i/10, False, True) for i in range(1,23)] + [(2.3,True,False)]
missing = [(0.,True,False)] + [(i/10,False,False) for i in range(1,53)] + [(5.3,True,False)]
probes = dict(intermittent_600_seconds=probe(intermittent), observed_quiet=probe(quiet), missing_expiry=probe(missing))
assert probes['intermittent_600_seconds']['events'] == [0.]
assert probes['intermittent_600_seconds']['latched']
assert len(probes['observed_quiet']['events']) == len(probes['missing_expiry']['events']) == 2


def kst(ms):
    return datetime.fromtimestamp(ms/1000, ZoneInfo('Asia/Seoul')).isoformat(timespec='milliseconds')


summary = dict(start_kst=kst(data['started_ms']), end_kst=kst(data['ended_ms']),
    observations=len(rows), red_observations=positive, red_object_event_ids=dict(ids),
    new_event_records=sum(len(r['events']) for r in rows),
    run_id=data['after']['run_id'], run_uptime_seconds=data['after']['uptime_s'],
    processed=path['finalized'], events_created=path['events_created'],
    cooldown=path['event_cooldown'],
    capture={k:path['artifacts'].get(k) for k in ('accepted','capture_ready','error','pending')},
    existing_event=dict(event_id=eid, ai_created_kst=kst(event['created_at']*1000),
        artifact_state=event['artifact_state'],delivery_state=event['delivery_state'],http_status=event['http_status']),
    web_event=web_event,
    tracker=dict(library=data['supervision']['version'], frame_rate=30, lost_buffer=30,
        max_time_lost_live=30, max_time_lost_offline=10,
        formula='int(frame_rate / 30.0 * lost_track_buffer)', activation=.25, new_track_det_thresh=.35),
    episode_probe=probes,
    verified=dict(server_source_hashes_match=True, same_run=True, no_frame_replay=True, no_live_changes=True),
    interpretation='AI episode remains linked to the initial Event; no new Event reaches artifact/Web admission. Per-frame quiet failure reasons are not exposed.')
(ROOT / 'shared/summary.json').write_text(json.dumps(summary,ensure_ascii=False,indent=2)+'\n')
(ROOT / 'shared/episode_probe.json').write_text(json.dumps(probes,indent=2)+'\n')
(ROOT / 'shared/camera24_metadata_20s.json').write_text(json.dumps({k:data[k] for k in ('started_ms','ended_ms','frames')},ensure_ascii=False)+'\n')
print(json.dumps(summary,ensure_ascii=False,indent=2))
