"""Copy representative immutable media into a numbered human review tree."""
import collections
import json
from pathlib import Path
import shutil
import subprocess

ROOT=Path(__file__).resolve().parent
PRIVATE=ROOT/'private';REVIEW=ROOT/'review'
order={'INTRUSION':0,'LOITERING':1,'FALL':2,'FIGHT':3,'CROWD':4,'FIRE':5}
source=[]
for path in (PRIVATE/'artifacts').glob('cameras/*/*/*/event.json'):
    doc=json.loads(path.read_text());source.append((doc['event'],path,False))
recovered=PRIVATE/'artifacts/recovery-fire/ready/event.json'
source.append((json.loads(recovered.read_text())['event'],recovered,True))
chosen={}
for event,path,recovery in sorted(source,key=lambda row:row[0]['aiRecordedAtWallMs']):
    chosen.setdefault((event['cameraId'],event['scenario'],event['ruleId']), (event,path,recovery))
index=[]
for number,(key,(event,path,recovery)) in enumerate(sorted(chosen.items(),key=lambda x:(x[0][0],order[x[0][1]],x[0][2])),1):
    camera,scenario,rule=key;area=event['ruleContext']['area']
    region='FULL' if area['reserved'] else 'OUTSIDE_LEFT' if area['outside'] else 'LEFT'
    name=f'R{number:02d}_{scenario}_rule-{rule:02d}_roi-{event["roiId"]}_{region}'+('_RECOVERED' if recovery else '')
    dest=REVIEW/'01_real_events'/f'camera-{camera}'/name;dest.mkdir(parents=True,exist_ok=False)
    for member in ('thumbnail.jpg','clip.mp4'):shutil.copy2(path.parent/member,dest/member)
    media=json.loads(path.read_text())['media']
    summary={k:event[k] for k in ('cameraId','eventId','scenario','ruleId','roiId','runId','frameSeq','ptsNs','ruleContext')}
    summary.update(scope='REAL_INPUT_EVENT',recovered_from_saved_cache=recovery,media=media,absolute_time_validated=False)
    (dest/'event.summary.json').write_text(json.dumps(summary,ensure_ascii=False,indent=2))
    probe=json.loads(subprocess.check_output(['ffprobe','-v','error','-select_streams','v:0','-show_entries',
        'stream=width,height,codec_name:format=duration','-of','json',str(dest/'clip.mp4')],timeout=10))
    assert probe['streams'][0]['width']==1280 and probe['streams'][0]['height']==720
    assert float(probe['format']['duration'])>0
    index.append(dict(id=f'R{number:02d}',camera=camera,scenario=scenario,rule=rule,roi=event['roiId'],region=region,
                      recovered=recovery,path=dest.relative_to(REVIEW).as_posix(),duration_seconds=float(probe['format']['duration'])))
(REVIEW/'samples.json').write_text(json.dumps(index,ensure_ascii=False,indent=2))
actual=json.loads((PRIVATE/'runtime/acceptance/result.json').read_text())
counts=collections.Counter((str(e['cameraId']),e['scenario']) for e,p,recovery in source if not recovery)
summary=dict(scope='ONE_SIMULTANEOUS_TWO_CAMERA_WINDOW',image='4f660ae3b484',
    live_result={k:actual.get(k) for k in ('error','checks','frames_observed','after_stop','finishedAt')},
    original_completed_artifacts=13,original_renderer_failures=1,cached_recovery_successes=1,
    live_counts=[dict(camera=c,scenario=s,count=n) for (c,s),n in sorted(counts.items())],
    original_sink_delivery='FAILED_TEST_RECEIVER_PERMISSION',offline_local_delivery_recheck='2_ACK_PASS',
    native_decoder_risk='ONE_LIBAV_ASSERTION_NOT_REPRODUCED_ON_SAVED_CACHE_RERENDER',
    samples=len(index),clips_probe='PASS',mock_checks=15,operational_changes=False)
(REVIEW/'RESULTS.json').write_text(json.dumps(summary,ensure_ascii=False,indent=2))
lines=['# 다중 ROI 시각 검토 시작점','',
 '## 먼저 볼 자료','',
 '- `02_same_frame_camera5/`: 동일한 실제 프레임의 원본·실시간 합집합·규칙별 Event 표시 비교. 새 추론 없이 저장된 압축 영상과 정확한 PTS metadata로 만들었다.',
 '- `01_real_events/`: 실제 RTSP 시험에서 생성된 대표 Event 썸네일·클립. 폴더의 R번호로 피드백한다.',
 '- `03_mock_diagrams/`: 가상 탐지 결과로 만든 그림. 실제 CCTV 분석 결과와 혼동하지 않는다.','',
 '## 확인 방법','',
 '1. 같은 프레임 비교에서 02_live_union.png는 박스2개, 03/04 Event는 각각 해당 영역1개, 전체화면05는2개인지 본다.',
 '2. 아래 표의 thumbnail.jpg를 먼저 보고 필요할 때 clip.mp4를 재생한다.',
 '3. Event는 해당 rule의 표시만 그리지만 영상 자체는 전체 화각을 유지한다. ROI는 영상 가림/박스 자르기 기능이 아니다.',
 '4. `R번호 / 이미지 또는 클립 시각 / 기대한 표시 / 실제 표시` 형태로 피드백한다.',
 '5. 이벤트가 생성됐다는 사실과 실제 행동이 그 시나리오에 해당하는지는 별개다. 과탐·누락은 사람 관측으로 추가 평가한다.','',
 '## 대표 자료','',
 '| ID | 카메라 | 시나리오 | rule / ROI | 영역 | 클립 |',
 '| --- | --- | --- | --- | --- | --- |']
for row in index:
    lines.append(f'| {row["id"]} | {row["camera"]} | {row["scenario"]} | {row["rule"]} / {row["roi"]} | {row["region"]} | [{row["duration_seconds"]:.2f}초]({row["path"]}/clip.mp4) |')
lines += ['', '## 결과와 제한','',
 '- 3·5 동시 실행1회, 동일 Start의 RUN_REUSED, 잘못된 참조422 및 기존 run 유지, 두 채널 Stop·회수 확인.',
 '- 실제 생성: 3번 INTRUSION3/LOITERING1/FALL3/FIGHT3/CROWD1, 5번 FIRE2묶음. 모두 image·clip이 있다.',
 '- 추가 FIRE 전체화면1건은 최초 libav decoder assertion으로 실패했으나 같은 압축 캐시를 변경 없이 재생성해 성공했다. RECOVERED 표시는 이 경우다. 재발 가능성은 아직 미확인이다.',
 '- 시험 수신기 권한 오류로 원래 HTTP 전달은 실패했다. 권한 보정 후 저장된 Event2건의 로컬 수신·응답만 재확인했다. 원래 원장을 성공으로 바꾸지 않았다.',
 '- mock15항목 PASS. 모든 합성 그림 상단에 SYNTHETIC MOCK ONLY를 표시했다.',
 '- 실시간 자동 JPEG capture는 이번 다중 provider 경로에서 생성되지 않았다. 대신 실제 Redis metadata와 위 동일 프레임 재구성 비교를 보존했다. 이를 브라우저 수용으로 확대하지 않는다.',
 '- 시험용 시간 anchor를 사용했으므로 절대시각·main/sub 동기화·Web 연동 성공을 검증한 것이 아니다.',
 '- 모델/precision/threshold는 유지했다. 침입·배회 대기시간과 군중 reference_count=5는 분리된 시험 규칙이며 운영 설정이 아니다.',
 '- 실제 Web/운영 Redis로 Event·metadata를 보내지 않았고 로그인·CSR·재부팅을 하지 않았다.',
 '- 운영 환경·설정은 변경하지 않았다. 시험 서비스는 정지하고 자료·구성은 보존했다.',
 '- private 폴더에는 RTSP 등 비공개 실행 정보가 있으므로 전체 반출하지 않는다. 검토는 review 폴더만 사용한다.','']
(REVIEW/'README.ko.md').write_text('\n'.join(lines))
print(json.dumps({'samples':len(index),'clips_checked':len(index),'original_event_bundles':13,'cached_recovery':1,'review':str(REVIEW)}))

if __name__=='__main__':pass
