"""Independent audit of cache, display conditions, identities and all frame maps."""
import csv
import hashlib
import json
import math
import pathlib
from collections import Counter
import av

ROOT=pathlib.Path('/download');SPECS=json.loads(pathlib.Path('/specs.json').read_text())
RB='/storage/contextra/experiments/action-model-eval/2026'
REFERENCE=json.loads(pathlib.Path('/model_reference.json').read_text())
EDGES=((5,6),(5,7),(7,9),(6,8),(8,10),(5,11),(6,12),(11,12),(11,13),(13,15),(12,14),(14,16))
TOTAL=0;results=[];inventory=[];manifest=[]


def path(remote):return pathlib.Path('/evidence')/pathlib.Path(remote).relative_to(RB)


def hashfile(p):
    h=hashlib.sha256()
    with p.open('rb') as f:
        for b in iter(lambda:f.read(1024**2),b''):h.update(b)
    return h.hexdigest()


def finite(v):return type(v) in (int,float) and math.isfinite(v)


for clip,spec in SPECS.items():
    folder=ROOT/clip;data=folder/'data';cache=path(spec['cache']);v=json.loads((data/'cache_validation.json').read_text())
    report=json.loads((data/'video_validation.json').read_text());summary=json.loads((data/'pose_summary.json').read_text())
    assert v['status']==report['status']==summary['status']=='PASS'
    assert hashfile(cache/'inputs/pose.json')==v['cache_sha256']==report['cache_sha256']
    assert hashfile(path(spec['remote_source']))==spec['sha256']==v['source_sha256']
    for k in ('person_threshold','nms_iou','tracking','model','engine_sha256','source_files','preprocess'):assert summary[k]==REFERENCE['pose'][k]
    assert hashfile(cache/'reports/pose_summary.json')==v['pose_summary_sha256']
    rows=json.loads((cache/'inputs/pose.json').read_text());selected=[rows[i] for i in v['selected_samples']]
    mapping=list(csv.DictReader((data/'frame_map.csv').open()));assert len(selected)==len(mapping)==report['frames']
    palette=json.loads((data/'identity_palette.json').read_text());assert len({tuple(c) for c in palette.values()})==len(palette)
    hashes=json.loads((data/'input_rgb_sha256.json').read_text());assert len(hashes)==len(selected)
    frames_seen=[];visible=hidden=invalid=untracked=omitted=0;joint_totals=Counter()
    with (data/'observation_states.jsonl').open() as f:
        for row,m,line in zip(selected,mapping,f,strict=True):
            fact=json.loads(line);assert (fact['sample'],fact['source_frame'],fact['seconds'])==(row['sample'],row['source_frame'],row['seconds'])
            assert len(fact['persons'])==len(row['persons']);actual_visible=[];colors=[]
            for i,(p,d) in enumerate(zip(row['persons'],fact['persons'])):
                s=p['score'];b=p['bbox'];valid=(finite(s) and 0<=s<=1 and len(b)==4 and all(finite(x) for x in b)
                    and b[0]<b[2] and b[1]<b[3] and b[2]>0 and b[3]>0 and b[0]<row['width'] and b[1]<row['height'])
                state='INVALID' if not valid else 'HIGH' if s>=.5 else 'LOW' if s>=.25 else 'BELOW'
                shown=state in ('HIGH','LOW');assert d['bbox_state']==state and d['visible']==shown
                assert d['raw_order']==i and d['person_score']==s and d['bbox']==b and (d['query'],d['track_id'])==(p['query'],p['track_id'])
                assert d['identity']==('T'+str(p['track_id']) if p['track_id'] is not None else 'q'+str(p['query']))
                expected=[]
                for k in p['keypoints']:
                    good=len(k)==3 and all(finite(x) for x in k) and 0<=k[0]<=row['width'] and 0<=k[1]<=row['height'] and 0<=k[2]<=1
                    expected.append('INVALID' if not good else 'HIGH' if k[2]>=.3 else 'LOW' if k[2]>=.05 else 'BELOW')
                assert d['joint_states']==expected and d['joint_scores']==[k[2] for k in p['keypoints']]
                assert d['counts']=={s:expected.count(s) for s in ('HIGH','LOW','BELOW','INVALID')}
                assert d['core_states']=={str(j):expected[j] for j in (5,6,11,12,7,8,9,10)}
                assert d['extra_states']=={str(j):expected[j] for j in (13,14,15,16)}
                edges=[None if expected[a] not in ('HIGH','LOW') or expected[b] not in ('HIGH','LOW') else
                       'HIGH' if expected[a]==expected[b]=='HIGH' else 'LOW' for a,b in EDGES]
                assert d['edge_states']==edges
                if shown:
                    actual_visible.append(d);colors.append(tuple(d['identity_color']));joint_totals.update(expected)
                    if p['track_id'] is not None:assert d['identity_color']==palette[str(p['track_id'])]
                    else:untracked+=1
                else:hidden+=1;invalid+=state=='INVALID'
            assert len(colors)==len(set(colors))
            for j,d in enumerate(actual_visible):assert d['detail_slot']==(j+1 if j<3 else None)
            n=len(actual_visible);visible+=n;omitted+=max(0,n-3)
            assert int(m['visible'])==n and int(m['detail'])==min(3,n) and int(m['omitted'])==max(0,n-3)
            assert (int(m['sample']),int(m['source_frame']),float(m['source_seconds']))==(row['sample'],row['source_frame'],row['seconds'])
            assert float(m['output_pts_seconds'])==round((row['seconds']-selected[0]['seconds'])*90000)/90000
            boxes=fact['labels']['label_boxes'];assert len(boxes)==n
            for j,a in enumerate(boxes):
                assert 0<=a[0]<a[2]<=1856 and 0<=a[1]<a[3]<=1044
                for b in boxes[j+1:]:assert not(a[0]<b[2] and b[0]<a[2] and a[1]<b[3] and b[1]<a[3])
            filename=f"{row['sample']:04d}.png";assert hashfile(cache/'inputs/frames'/filename)==hashes[filename]
            frames_seen.append(row['source_frame'])
    assert len(frames_seen)==len(set(frames_seen))==len(selected)
    assert report['max_pts_error']<1/90000 and report['all_frame_text_overflow']==report['all_frame_text_overlap']==report['label_overlap']==0
    movie=folder/'Pose_Input_Review.mp4';assert hashfile(movie)==report['sha256']
    with av.open(str(movie)) as container:
        stream=container.streams.video[0];assert (stream.width,stream.height,stream.frames)==(3200,1800,len(selected))
    candidate=None
    if not spec['reused']:
        rawpath=cache/'inputs/pose_candidates_prefilter.jsonl';count=0
        with rawpath.open() as f:
            for row,line in zip(rows,f,strict=True):
                raw=json.loads(line);assert (raw['sample'],raw['source_frame'],raw['seconds'])==(row['sample'],row['source_frame'],row['seconds'])
                byq={p['query']:p for p in raw['candidates']};assert len(byq)==len(raw['candidates'])==100
                expected={q for q,p in byq.items() if p['nms_keep'] and p['score']>=.15}
                assert expected=={p['query'] for p in row['persons']}
                for p in row['persons']:
                    c=byq[p['query']];assert c['score']==p['score'] and c['keypoints']==p['keypoints'] and c['track_id']==p['track_id']
                count+=1
        candidate=dict(observations=count,queries_per_observation=100,sha256=hashfile(rawpath),bytes=rawpath.stat().st_size)
    result=dict(clip=clip,status='PASS',observations=len(selected),reused=spec['reused'],visible_object_observations=visible,
                hidden_object_observations=hidden,invalid_bbox_observations=invalid,untracked_visible=untracked,
                detail_omitted_object_observations=omitted,joint_states_shown=dict(joint_totals),prefilter_cache=candidate)
    results.append(result);TOTAL+=len(selected)
    inventory.append(dict(input_id=clip,source_file=spec['source_file'],source_sha256=spec['sha256'],
        video=clip+'/Pose_Input_Review.mp4',cache='reused' if spec['reused'] else 'new',cache_relative=str(cache.relative_to('/evidence')),observations=len(selected),
        first_source_frame=selected[0]['source_frame'],last_source_frame=selected[-1]['source_frame'],
        first_seconds=selected[0]['seconds'],last_seconds=selected[-1]['seconds'],
        acceptance='USER_UNSET',user_observation=spec['user_observation'],video_sha256=report['sha256']))
    manifest.append(dict(input_id=clip,source_file=spec['source_file'],source_sha256=spec['sha256'],cache_relative=str(cache.relative_to('/evidence')),cache_sha256=v['cache_sha256'],
                         pose_summary_sha256=v['pose_summary_sha256'],display_bounds=spec['display_bounds'],prefilter_cache=candidate))
assert len(results)==15 and sum(r['reused'] for r in results)==11
layout=json.loads((ROOT/'validation/layout_review.json').read_text());assert layout['status']=='PASS'
with (ROOT/'clip_list.csv').open('w',newline='',encoding='utf-8-sig') as f:
    writer=csv.DictWriter(f,fieldnames=inventory[0]);writer.writeheader();writer.writerows(inventory)
lines=['# 입력 목록과 사용자 검토 대기','',
       '원본Frame은1-based이며 아래 범위는 실제 표시된10FPS 표본이다. 필수 감지/비감지·중요도는 모두 미정이다.','',
       '| 입력 ID | 원본 파일 | 실제 표시Frame | 관측 수 | cache |','| --- | --- | --- | ---: | --- |']
for r in inventory:lines.append(f"| {r['input_id']} | {r['source_file']} | {r['first_source_frame']}~{r['last_source_frame']} | {r['observations']} | {r['cache']} |")
lines+=['','## 기존 사용자 설명','']
for r in inventory:lines.append(f"- {r['input_id']}: {r['user_observation']}")
lines+=['','이 설명을 정확한Frame별 정답 또는 시연 필수 통과 조건으로 확대하지 않는다.','']
(ROOT/'clip_list.ko.md').write_text('\n'.join(lines))
(ROOT/'input_manifest.json').write_text(json.dumps(manifest,indent=2))
(ROOT/'model_reference.json').write_text(json.dumps(REFERENCE,indent=2))
report=dict(status='PASS',inputs=15,videos=15,reused=11,new=4,frames=TOTAL,clips=results,
    bbox_thresholds=[.25,.5],joint_thresholds=[.05,.3],checks=['same source/cache hashes and Frame/PTS',
    'independent score bands and edge conditions','raw-order first three details','real T identity and per-frame q',
    'fixed T palette and distinct co-visible colors','all RGB hashes','renderer full decode/PTS/text geometry',
    'new prefilter/derived query agreement','minimal human layout samples'],semantic_review=False)
(ROOT/'validation/technical_audit.json').write_text(json.dumps(report,indent=2))
hashes={str(p.relative_to(ROOT)):hashfile(p) for p in sorted(ROOT.rglob('*')) if p.is_file() and p.name!='SHA256SUMS.json'}
(ROOT/'SHA256SUMS.json').write_text(json.dumps(hashes,indent=2))
print(json.dumps(dict(status='PASS',inputs=15,videos=15,frames=TOTAL,files=len(hashes),reused=11,new=4)),flush=True)
