"""Small reproducible review package; no RGB cache or weights copied."""
import bootstrap
from collections import Counter,defaultdict
import csv
import datetime
import hashlib
import json
from pathlib import Path
import shutil
from inputs import OUT


def sha(path):return hashlib.sha256(path.read_bytes()).hexdigest()


def main():
    target=OUT/'download';validation=target/'validation';metadata=target/'metadata';metadata.mkdir(exist_ok=True)
    c=json.loads((OUT/'candidates.json').read_text());specs=json.loads((OUT/'specs.json').read_text())
    summaries={k:json.loads((target/k/'data/summary.json').read_text()) for k in specs}
    shutil.copy2(OUT/'00_README_ko.txt',target/'00_README_ko.txt')
    for name in ('candidates.json','execution_contract.json','specs.json','cases.json','self_maps.json'):
        shutil.copy2(OUT/name,metadata/name)
    helper_dir=target/'reproducibility';helper_dir.mkdir(exist_ok=True)
    for p in (OUT/'helpers').glob('*.py'):shutil.copy2(p,helper_dir/p.name)
    for name in ('unit_tests.json','probe.json','probe_refinement.json','probe_final.json'):
        shutil.copy2(OUT/'reports'/name,validation/name)
    table=['# 37개 입력 수치 비교','',
        '현재 최종 조건 충족 관측수다. 노랑 표시 유지, 실제 정확도/검출률, 운영 Event 횟수가 아니다.',
        'B1/B2와 동일 표시 Frame을 사용하며 기존 두 영상의 노랑0.4초와 새 영상의 노랑1초를 충족 관측수에 합산하지 않는다.','',
        '| 입력 | 전체 | 동작 관측 가능 | B1 | B2 | 통합 현재 | 통합 새 시험 Event |',
        '| --- | ---: | ---: | ---: | ---: | ---: | ---: |']
    inventory=[];unique={};duplicates=Counter();whole=Counter();rule_counts=Counter();veto_counts=Counter()
    for clip,s in summaries.items():
        v=s['stats'];old=s['baseline'];b1=old['B1_Directed']['condition_frames'];b2=old['B2_SeedFollow']['condition_frames']
        table.append(f"| {clip} | {v['displayed']} | {v['observable']} | {b1} | {b2} | {v['condition_frames']} | {len(v['test_events'])} |")
        rule_counts.update(s['rule_observation_counts']);veto_counts.update(s['applied_exclusion_observation_counts'])
        for line in (target/clip/'data/B_Integrated_diagnostics.jsonl').open():
            r=json.loads(line)
            if not r['displayed']:continue
            key=r['dedupe_key'];whole[r['reference']]+=1
            if key in unique:duplicates['observations']+=1;continue
            unique[key]=dict(reference=r['reference'],hit=r['hit'],observable=r['observable'],clip=clip,source_frame=r['source_frame'])
        if clip in c['representative_clips']:
            vv=json.loads((target/clip/'data/video_validation.json').read_text())
            inventory.append(dict(input=clip,file=clip+'/B_Integrated_Review.mp4',frames=vv['frames'],fps=10,sha256=vv['sha256']))
    counts={}
    for v in unique.values():
        x=counts.setdefault(v['reference'],dict(N=0,observable=0,hit=0));x['N']+=1;x['observable']+=v['observable'];x['hit']+=v['hit']
    table+=['','## 참고 구간과 중첩','',
        '중첩 원본 Frame은 입력 정렬순 첫 occurrence만 대표로 선택했다. 준비/context 차이가 있을 수 있어 합의된 정답으로 해석하지 않는다.',
        '| 사용자 참고 | 중복제거 관측 | 동작 관측 가능 | 현재 충족 |','| --- | ---: | ---: | ---: |']
    for label,v in counts.items():table.append(f"| {label} | {v['N']} | {v['observable']} | {v['hit']} |")
    table+=['','## 해석 제한','',
        '- 전체 조건 미충족과 입력/동작 관측 부족을 구분한다. 1인 입력의 부족은 싸움 오판0이라는 사실과 별개다.',
        '- 공간 후보의 실제 대응이 없는 노랑 구간은 ROI 상태만 유지한다.',
        '- 개발13입력에서 조건을 고정한 뒤08/09와 나머지 자료를 확인했다. 이 자료는 독립 일반화 검증셋이 아니다.',
        '- C13의 특정 하이파이브 Frame은 새로 지정하지 않았다. 기존 case reference만 사용했다.',
        '- 현재 충족0이라도 부분 근거가 있을 수 있다. data의 raw/diagnostics와 현재 hit를 구분한다.',
        '- 새 Event 수는 cooldown/episode/선행 준비 구간에 영향을 받는다. preceding_events도 확인한다.',
        '- 오류 억제와 감지 보완을 묶어 변경했으므로 개별 변경 효과를 분리 입증하지 않았다.']
    (target/'37_Input_Comparison.ko.md').write_text('\n'.join(table)+'\n')
    dn=['# 피드백 행동 키워드','', '설명용 ID이며 모델 행동 클래스/확정 규칙이 아니다. 같은 장면의 대상/기대/우선순위를 함께 적는다.','', '| ID | 감지 희망 |','| --- | --- |']
    ds=['상대를 향한 직선 주먹 뻗기','옆으로 휘두르는 주먹질','아래에서 위로 주먹질','위에서 아래로 내려치기','상대를 향한 앞차기','옆차기/회전 발차기','상대 몸 밀치기','상대 어깨/몸 붙잡고 흔들기','몸으로 밀고 들어가기/태클','여러 동작이 섞인 몸싸움']
    ns=['가까이 서기/대화','보행 중 팔 흔들림','비틀거리기/균형잡기','숙이기/앉기/일어나기','쓰러지기/누워있기','악수','하이파이브','어깨동무/팔짱/손잡고 이동','혼자 주먹질/발차기','상대와 무관한 허공 밀치기','운동/스트레칭','손짓/인사']
    dn += [f'| D{i:02d} | {x} |' for i,x in enumerate(ds,1)]+['','| ID | 비감지 희망 |','| --- | --- |']+[f'| N{i:02d} | {x} |' for i,x in enumerate(ns,1)]
    dn += ['','새 지정의 상은 지정 사례 모두 충족 목표, 중은 대다수 목표, 하는 trade-off 허용이다. 달성 가능성을 보장하지 않는다.',
        '기존 높은 우선도는 참고 상태다. 대략적 Frame은 정확한 매-Frame GT가 아니며 엔진 강제 발동/차단에 사용하지 않는다.',
        '구간 번호는 화면 상단 입력 Frame이 기본이다. 자체촬영 원본01 검토 Frame을 쓰면 그 기준을 명시한다.']
    (target/'D_N_feedback_guide.md').write_text('\n'.join(dn)+'\n')
    with (target/'video_list.csv').open('w',encoding='utf-8-sig',newline='') as f:
        w=csv.DictWriter(f,fieldnames=inventory[0]);w.writeheader();w.writerows(inventory)
    summary=dict(inputs=37,videos=9,displayed=sum(v['stats']['displayed'] for v in summaries.values()),
        unique_source_frames=len(unique),duplicate_observations=duplicates['observations'],reference=counts,
        rules=dict(rule_counts),local_exclusions=dict(veto_counts),
        decision_p95_ms_max=max(v['decision_p95_ms'] for v in summaries.values()),
        decision_max_ms=max(v['decision_max_ms'] for v in summaries.values()),
        performance_scope='cached CPU decision only; excludes Pose/decode/network/render; not operational FPS',
        candidate_sha256=sha(OUT/'candidates.json'))
    (target/'summary.json').write_text(json.dumps(summary,indent=2))
    manifest=dict(schema='contextra.fight-integrated-review.v1',profile='ltb1021',lifecycle='durable-large',retention='manual-review',
        generated_at=datetime.datetime.now(datetime.timezone.utc).isoformat(),candidate_sha256=summary['candidate_sha256'],files=[])
    for path in sorted(target.rglob('*')):
        if path.is_file() and path.name!='manifest.json':manifest['files'].append(dict(path=str(path.relative_to(target)),bytes=path.stat().st_size,sha256=sha(path)))
    (target/'manifest.json').write_text(json.dumps(manifest,indent=2))
    print('PACKAGE_PASS',json.dumps(summary),len(manifest['files']),flush=True)


if __name__=='__main__':main()
