"""Independent bookkeeping/causality audit and comparison packaging; no engine run."""
import bootstrap
from collections import Counter
import gzip
import itertools
import json
from pathlib import Path
import shutil
from data_robust import OUT,read,sha,paths,lines,reference

cfg=read(OUT/'candidates.json');specs=read(OUT/'specs.json');feedback=read(OUT/'feedback.json')
assert len(specs)==32 and read(OUT/'reports/unit_tests.json')['status']=='PASS'
assert not list((OUT/'download').rglob('*.mp4'))
tables=[];windows={};audits=[];new_frames=[];new_negative=[];states_count=Counter();anchor_missing=Counter()
for clip,spec in specs.items():
    dest=OUT/'download'/clip/'data';s=read(dest/'summary.json');fp,bp,source=paths(clip)
    assert s['candidate_sha256']==sha(OUT/'candidates.json') and s['trace_sha256']==sha(dest/s['trace'])
    assert all(sha(OUT/'helpers'/k)==v for k,v in s['code_sha256'].items())
    counts=Counter();history={};last=None;last_event=None;group_hits={};first=None
    with gzip.open(dest/s['trace'],'rt') as f:
        for text,old in itertools.zip_longest(f,lines(bp)):
            assert text is not None and old is not None
            r=json.loads(text);t=r['seconds'];first=t if first is None else first
            assert last is None or t>last;last=t
            assert all(r[k]==old[k] for k in ('sample','source_frame','seconds','displayed'))
            tag,ids=reference(clip,old,feedback);assert r['reference']==tag and r['feedback_ids']==ids
            assert r['hit']==any(p['hit'] for p in r['pairs'])
            assert r['raw']==any(p['raw'] for p in r['pairs'])
            if r['hit']:assert r['episode']['display']
            if r['episode']['event']:
                assert r['hit'] and (last_event is None or t-last_event>=2.-1e-8)
                last_event=t
            by={p['token']:p for p in r['pairs']}
            for p in r['pairs']:
                token=p['token'];rows=history.setdefault(token,[])
                if p['reset']:rows.clear();group_hits.pop(token,None)
                rows[:]=[x for x in rows if t-x[0]<=cfg['config']['window']+1e-8]
                flags=[any(d['actor']==i for d in p['passing']) for i in (0,1)]
                rows.append((t,flags))
                assert p['counts']==[sum(x[1][i] for x in rows) for i in (0,1)]
                assert p['hit']==bool(p['actors'])
                if p['hit']:group_hits[token]=t
                for d in p['diagnostics']:
                    assert first<=d['input_start']<d['input_end']==t
                    seed=d['active_first_approach']
                    if seed:
                        assert first<=seed['start_time']<seed['time']<t
                        assert t-seed['time']<=cfg['config']['seed_seconds']+1e-8
                    if {'ARM_RETURN','LEG_RETURN'}&set(d['rules']):
                        link=d['follow_link'];assert seed and link['accepted']
                        assert link['anchor']==seed['anchor'] and d['input_start']>=seed['time']-1e-8
                    if d['seed']:assert d['first_approach']['status'] in ('SINGLE_OBSERVED_APPROACH','CORROBORATED_APPROACH')
            for g in r['groups']:
                if g['state']=='RED_CURRENT':assert g['contributing_pairs'] and all(by[k]['hit'] for k in g['contributing_pairs'])
                if g['state']=='YELLOW_HOLD':
                    assert not g['contributing_pairs'] and g['held_pairs']
                    for k in g['held_pairs']:
                        assert k in by and not by[k]['reset'] and not by[k]['hit']
                        assert abs(g['hold_last_hit'][k]-group_hits[k])<1e-8 and t-group_hits[k]<=1.+1e-8
            counts['processed']+=1
            if r['displayed']:
                counts['displayed']+=1;counts[tag+'_hit']+=r['hit'];counts[tag+'_event']+=r['episode']['event']
                states_count[r['episode']['status']]+=1
            if clip=='SELF_L02':
                assert r['part_id']=='L01_P02' and r['l02_frame']==r['source_frame']-2542
                assert abs(r['l02_seconds']-(t-105.352669))<1e-8
    assert counts['processed']==s['processed'] and counts['displayed']==len(spec['selected_samples'])==s['displayed']
    for tag,v in s['candidate']['reference'].items():
        assert counts[tag+'_hit']==v['hit'] and counts[tag+'_event']==v['events']
    base=s['baseline'];cur=s['candidate'];variants={}
    for name in ('R4','R5'):
        x=s[name]['labels'];variants[name]={tag:x.get(tag,{}).get('new_hit',0) for tag in base['reference']}
    row=dict(clip=clip,displayed=s['displayed'],observable=cur['observable'],
        positive=dict(baseline=base['reference']['positive_reference']['hit'],R4=variants['R4']['positive_reference'],
            R5=variants['R5']['positive_reference'],candidate=cur['reference']['positive_reference']['hit']),
        negative=dict(baseline=base['reference']['negative_reference']['hit'],R4=variants['R4']['negative_reference'],
            R5=variants['R5']['negative_reference'],candidate=cur['reference']['negative_reference']['hit']),
        events=dict(baseline=len(base['test_events']),R4=sum(x['new_events'] for x in s['R4']['labels'].values()),
            R5=sum(x['new_events'] for x in s['R5']['labels'].values()),candidate=len(cur['test_events'])),
        gained_current=len(s['newly_current']),new_negative=len(s['new_negative_current']),lost_positive=len(s['lost_positive']),
        baseline_gap_seconds=base['positive_longest_observed_no_hit_span_seconds'],candidate_gap_seconds=cur['positive_longest_observed_no_hit_span_seconds'],
        timing=s['timing'])
    tables.append(row);new_frames.extend([dict(clip=clip,**x) for x in s['newly_current']]);new_negative.extend([dict(clip=clip,**x) for x in s['new_negative_current']])
    anchor_missing.update(s['feature_diagnostics']['anchor_missing'])
    for key,w in s['feedback'].items():
        windows.setdefault(clip,{})[key]=dict(N=w['N'],baseline=len(w['baseline_hit_frames']),
            R4=s['R4']['feedback'][key]['new_hit'],R5=s['R5']['feedback'][key]['new_hit'],candidate=len(w['candidate_hit_frames']),
            baseline_event_frames=w['baseline_event_frames'],candidate_event_frames=w['candidate_event_frames'],
            baseline_hit_frames=w['baseline_hit_frames'],candidate_hit_frames=w['candidate_hit_frames'])
    audits.append(dict(clip=clip,processed=s['processed'],displayed=s['displayed'],trace_sha256=s['trace_sha256'],
        cache_sha256=s['cache_sha256'],baseline_sha256=s['baseline_sha256'],feature_sha256=s['feature_sha256'],causal=True))
    print('AUDIT_PASS',clip,s['displayed'],flush=True)
assert sum(x['displayed'] for x in tables)==18045
for document in ('baseline_fingerprints.json','comparison_fingerprints.json'):
    assert all(sha(Path('/evidence')/k)==v for k,v in read(OUT/document).items())
totals={};names=('baseline','R4','R5','candidate')
short=[r for r in tables if r['clip']!='SELF_L02'];long=next(r for r in tables if r['clip']=='SELF_L02')
totals['positive31']={k:sum(r['positive'][k] for r in short) for k in names}
totals['positiveL02']=long['positive']
totals['feedbackCxx']={k:sum(w[k] for c,vs in windows.items() if c!='SELF_L02' for w in vs.values()) for k in names}
totals['feedbackL02']={k:sum(w[k] for w in windows['SELF_L02'].values()) for k in names}
report=dict(status='COMPARISON_COMPLETE_NOT_ADOPTED',tables=tables,totals=totals,feedback=windows,new_current=new_frames,
    new_negative_current=new_negative,anchor_missing=dict(anchor_missing),videos=0,
    interpretation='Counts on approximate reference ranges, not accuracy/recall; Cxx/L02 overlap, do not sum',
    limitation='R4/R5 full Event timelines not retained; only saved counts and changes compared')
data=OUT/'download/data';data.mkdir(exist_ok=True)
(data/'comparison.json').write_text(json.dumps(report,ensure_ascii=False,indent=2))
for name in ('candidates.json','sources.json','feedback.json','baseline_fingerprints.json','comparison_fingerprints.json','design.ko.md','execution_contract.json'):
    shutil.copy2(OUT/name,data/name)
shutil.copy2(bootstrap.PRIOR/'download/data/L02_observation_map.csv',data/'L02_observation_map.csv')
validation=dict(status='PASS',unit=read(OUT/'reports/unit_tests.json'),inputs=32,displayed=18045,
    processed=sum(r['processed'] for r in audits),per_input=audits,source_hashes_preserved=True,
    input_alignment=True,counts_independently_recomputed=True,causal=True,display_event_separated=True,
    L02_mapping=7261,anchor_missing=dict(anchor_missing),videos=0,rendering='NOT_REQUESTED')
(OUT/'download/validation/technical_audit.json').write_text(json.dumps(validation,indent=2))
(OUT/'reports/comparison_brief.json').write_text(json.dumps(dict(totals=totals,new_current=len(new_frames),new_negative=len(new_negative),anchor_missing=dict(anchor_missing)),indent=2))
md=['# 첫 접근·후속 연결 후보 비교','',
    '상태: CPU 비교 완료, 후보 미채택. 영상/클립은 만들지 않았으며 운영 적용도 없다.',
    '기존32입력18,045표시 관측과 저장 특징을 재사용했다. 기준 엔진 및 R4/R5는 재실행하지 않았다.',
    '', '## 변경 원리',
    '같은 관절·상대 부위를 기준으로 실제 접근량과 자기 root 대비 이동을 확인한 뒤 첫 근거를 승인한다. 후속 이동은 그 승인 이후의 실제 접근 지속 또는 회수인지 확인한다.',
    '직접/회전/발/근접 규칙의 기존 수치는 유지했다. R4/R5의 자기 어깨 근접 억제는 사용하지 않았다. 따라서 직접 규칙으로 통과하는 일상 동작을 모두 억제하는 후보는 아니다.',
    '첫 접근 조건은 거리 감소0.025·관절 변위0.06이다. 단일 구간 또는0.30초 이내 별도 관측2개 이상의 순 이동으로 확인한다. 기존0.6초 이내 후속 연결에 접근 지속0.01/관측 최소거리 뒤 회수0.015를 확인한다.',
    '두 lag를 무조건 AND로 묶거나 겹치는 이동량을 더하지 않는다. 구체식과 한계는 data/design.ko.md, 고정값은 data/candidates.json을 따른다.',
    '', '## 핵심 비교','아래는 빨간 판정 관측 수다. 정확도·재현율이 아니며 Cxx/L02 중첩을 독립 사례로 합산하지 않는다.',
    '| 범위 | 기준 | R4 | R5 | 새 후보 |','| --- | ---: | ---: | ---: | ---: |']
for key,title in [('positive31','기존31입력 감지 희망'),('positiveL02','L02 감지 희망'),('feedbackCxx','최신 지정 Cxx 비감지'),('feedbackL02','최신 지정 L02 비감지')]:
    v=totals[key];md.append('| '+title+' | '+' | '.join(str(v[k]) for k in names)+' |')
md += ['',f"새 빨간 관측 {len(new_frames)}개, 그중 비감지 참고 구간의 신규 빨강 {len(new_negative)}개다. Event 추가/제거 시각은 입력별 summary.json에 별도로 보존했다.",
    'C17/C19 등 같은 장면의 개별 클립·연속본 결과와 잔여 구간은 data/comparison.json의 feedback에 나란히 둔다. 서로 다른 관측을 억지로 맞추지 않았다.',
    '', '## 입력별 비교','감지 희망과 비감지 열은 기준/R4/R5/새 후보 순서다. 기존 action_cases의 negative label을 이번에 새 오판으로 확정하지 않는다.',
    '| 입력 | 관측 | 감지 희망 | 비감지 참고 | 새 시험 Event |','| --- | ---: | --- | --- | --- |']
for r in tables:
    md.append('| '+r['clip']+' | '+str(r['displayed'])+' | '+' / '.join(str(r['positive'][k]) for k in names)+' | '+' / '.join(str(r['negative'][k]) for k in names)+' | '+' / '.join(str(r['events'][k]) for k in names)+' |')
md += ['', '## 검증과 해석 경계',
    f"합성 검사{validation['unit']['tests']}개와32입력의 지문·Frame/PTS·누적 개수·첫 근거 인과성·표시/Event 정합을 확인했다.",
    'Event 수는 표시 구간 내 새 시험 Event다. 준비 구간의 기존 episode, 현재 빨간 판정과 표시 유지는 별개다.',
    '새 후보와 기준의 전체 Event 시각은 입력별 summary에 있다. R4/R5의 전체 Event 시각은 과거에 보존되지 않은 항목이 있어 저장된 수치만 비교했다.',
    '전체 원시 진단은 입력별 data/ApproachLink_diagnostics.jsonl.gz에 있다. first_approach는 승인 근거, follow_link는 후속 연결 확인, seed_eligible은 기존 규칙 자격이고 seed는 새 첫 근거 승인 후보다.',
    'L02는 기존 L01_P02 cache의 논리 입력이다. 원시 source_frame/seconds는 L01을 유지하며 l02_frame/l02_seconds와 대응표를 함께 제공한다. 새 물리 클립은 없다.',
    '입력 부족·기대 미정·근사 구간을 정상 또는 정확한GT로 취급하지 않는다. 형상이 비슷한 일상 동작과 공격의 분리, 운영 FPS·실제 오알람률·모델 우열은 검증하지 않았다.',
    '조건을 바꿔 반복 탐색하거나 영상 제작을 자동 진행하지 않는다. 사용자 검토 전 후보 채택·운영 적용은 보류한다.']
(OUT/'download/00_README_ko.md').write_text('\n'.join(md)+'\n')
manifest={str(p.relative_to(OUT/'download')):sha(p) for p in sorted((OUT/'download').rglob('*')) if p.is_file() and p.name!='manifest.json'}
(OUT/'download/manifest.json').write_text(json.dumps(manifest,indent=2))
print('PACKAGE_PASS',json.dumps(dict(totals=totals,files=len(manifest),new_current=len(new_frames),new_negative=len(new_negative))),flush=True)
