"""Render A/B diagnostics; do not evaluate action semantics from RGB."""
import csv
import hashlib
import json
import os
from pathlib import Path
from fractions import Fraction
import time
import uuid
import av
import numpy as np
from PIL import Image,ImageDraw
from contract import observation
from layout import text,overlay,W,H,IW,IH,IX,IY,MUTED

clip=os.environ['CLIP'];cache=Path('/cache');previous=Path('/previous')/clip/'data'
dest=Path('/download')/clip;data=dest/'data'
summary=json.loads((data/'engine_summary.json').read_text());check=json.loads((previous/'cache_validation.json').read_text())
raw=(cache/'inputs/pose.json').read_bytes();assert hashlib.sha256(raw).hexdigest()==check['cache_sha256']
all_rows=json.loads(raw);rows=[all_rows[i] for i in check['selected_samples']]
palette={int(k):tuple(v) for k,v in json.loads((previous/'identity_palette.json').read_text()).items()}
hashes=json.loads((previous/'input_rgb_sha256.json').read_text())
STATUS={'ALERT':'새 시험 이벤트','RULE_HIT':'현재 조건 충족','RULE_HIT_LATCHED':'현재 조건 충족 / 같은 episode',
 'RULE_HIT_COOLDOWN':'현재 조건 충족 / cooldown','DISPLAY_HOLD':'짧은 표시 유지','UNKNOWN_LATCHED':'근거 부족 / episode 보존',
 'QUIET_REARM_WAIT':'관측된 조용한 구간 / 재무장 대기','MONITORING':'현재 규칙 미충족',
 'INSUFFICIENT':'준비 중 또는 근거 부족','WARMUP_OR_INSUFFICIENT':'준비 중 또는 근거 부족','LATCHED_WAIT':'episode 보존 / 재무장 대기'}


def canvas(image,row,result,mode,index):
    people=observation(row,palette);shown=[p for p in people if p['visible']]
    active_orders={o for p in result['pairs'] if mode=='A' and p['episode']['display'] for o in p['orders']}
    for p in shown:
        p['fight_display']=p['raw_order'] in active_orders
        p['detail_slot']=None  # Right-hand panels describe pairs, not object slots.
    page=Image.new('RGB',(W,H),'#101820');draw=ImageDraw.Draw(page);rects=[]
    name='A | Tracking ID 쌍 개선안' if mode=='A' else 'B | 공간 관계 / ROI 개선안'
    text(draw,rects,(20,16),clip+' | '+name,40)
    text(draw,rects,(20,82),f"원본 Frame {row['source_frame']} | PTS {row['seconds']:.3f}s | 관측 {index+1}/{len(rows)} | 10FPS 정상 속도",28)
    panel,labels=overlay(image,shown);page.paste(panel,(IX,IY))
    if mode=='B' and result['episode']['display']:draw.rectangle((IX,IY,IX+IW-1,IY+IH-1),outline='#ff5555',width=8)
    color='#ff7777' if result['episode']['display'] else '#edf1f5'
    text(draw,rects,(20,1210),'상태: '+STATUS[result['episode']['status']],34,color)
    before=summary['modes'][mode]['test_events_before_display']
    text(draw,rects,(20,1260),f"현재 규칙: {','.join(result['current_rules']) or '없음'} | 새 시험 이벤트 {len(result['events'])} | 누적 {result['event_count']} (선행 {before})",28)
    lines=[
        f"입력 bbox {len(shown)} / 유효 몸통 {result['eligible_bodies']} / 근접 후보쌍 {result['candidate_pairs']}",
        f"창 1.0초 / 과거 비교 약0.2초 / 실제 선행 준비 {summary['preceding_observations']}관측 / 미래 Frame 사용 없음",
        '회색 bbox·skeleton = 입력 참고 / 얇은 링·ID 색 = 객체 구분',
        'A 빨간 bbox = 해당 쌍의 표시 상태 / B 빨간 ROI = 화면 전체 표시 상태',
        'T는 기존 cache ID / q는 Frame별 미추적 출력 칸 / S는 시험 공간상태 번호',
        'STRIKE=타격형 조건 / PUSH_GRAPPLE=접촉·상대 움직임 조건 / 실제 행동 정답 아님',
        '현재 조건·표시 유지·새 이벤트를 구분합니다. 실제 Web 알람·Event 저장 없음',
    ]
    for j,line in enumerate(lines):text(draw,rects,(20,1320+j*57),line,26,MUTED)
    x=1910
    text(draw,rects,(x,150),'판정 근거 요약 (상세 원시 값은 data)',32)
    text(draw,rects,(x,205),f"연결 모호 {result['ambiguous_matches']} / 상한 초과 {result['pairs_over_cap']} / 배정 없음 {result['unassigned_pairs']}",26)
    reason_counts={}
    for p in result['rejected']:reason_counts[p['reason']]=reason_counts.get(p['reason'],0)+1
    reasons=' / '.join(k+':'+str(v) for k,v in reason_counts.items()) or '없음'
    # Keep detailed reason codes in JSON when a crowded frame would overflow one line.
    if len(reasons)>65:reasons=reasons[:62]+'...'
    text(draw,rects,(x,255),'입력 제외: '+reasons,24,MUTED)
    pairs=result['pairs']
    # Decision data is unchanged. This panel prioritizes actual hit/display evidence.
    selected=sorted(enumerate(pairs),key=lambda z:(not z[1]['hit'],not(z[1].get('episode',{}).get('display',False)),z[0]))[:3]
    for n,(_,p) in enumerate(selected):
        yy=330+n*390;lim=(x,yy,3180,yy+370);draw.rectangle(lim,outline='#43515e',width=2)
        refs=' + '.join(p['refs']);pc='#ff7777' if p['hit'] else '#edf1f5'
        text(draw,rects,(x+20,yy+15),p['token']+' | '+refs,30,pc,lim)
        text(draw,rects,(x+20,yy+65),'규칙: '+(','.join(p['rules']) or '미충족')+' / '+p['reason'],24,pc,lim)
        text(draw,rects,(x+20,yy+110),f"근접 관측 {p['close_seconds']:.2f}s | 타격형 pulse {p['pulses']}/2 | 새 pulse {int(p['new_pulse'])}",26,limit=lim)
        text(draw,rects,(x+20,yy+155),f"접촉 {p['contact_seconds']:.2f}/0.40s | 동적 접촉 {p['dynamic_seconds']:.2f}/0.20s ({p['dynamic_samples']}관측)",24,limit=lim)
        f=p['features'];arms=[] if not f else f['arms']
        change='--' if f is None else f"{f['pair_relative_change']:.3f}"
        text(draw,rects,(x+20,yy+200),f"관측 가능 팔 {sum(a['valid'] for a in arms)} | 상대 위치 변화 {change}",26,limit=lim)
        if arms:
            a=max(arms,key=lambda a:a['relative_motion'])
            text(draw,rects,(x+20,yy+245),f"최대 팔 변화 {a['relative_motion']:.3f} | 상대방 방향 {a['toward']:.3f} | 거리 {a['hand_distance']:.3f}",24,limit=lim)
        else:text(draw,rects,(x+20,yy+245),'팔 비교 자료 없음 (0점/정상 판정 아님)',24,MUTED,lim)
        source='--' if p['input_start'] is None else f"{p['input_start']:.3f}"
        text(draw,rects,(x+20,yy+295),f"실제 팔 비교 PTS {source} ~ {p['input_end']:.3f} | 증거창 최대1초",24,MUTED,lim)
    if not pairs:text(draw,rects,(x+20,350),'유효한 근접 쌍 없음 / 정상 행동 확정 아님',28,MUTED)
    text(draw,rects,(x+20,1550),f"상세 {len(selected)}쌍 / {max(0,len(pairs)-3)}쌍 생략 (전체 JSON 보존)",26,MUTED)
    text(draw,rects,(x+20,1610),'상세 순서: 현재 조건 충족 → 표시 유지 → 원래 쌍 순서',24,MUTED)
    text(draw,rects,(x+20,1670),'이 화면은 격리 시험 결과이며 운영 싸움 판정이 아닙니다.',26,MUTED)
    return page,labels


for mode,filename in [('A','A_IDPair_Review.mp4'),('B','B_ROISpatial_Review.mp4')]:
    out=dest/(Path(filename).stem+'_v2.mp4');report_path=data/(mode+'_video_validation_v2.json')
    if out.exists() and report_path.exists():
        old=json.loads(report_path.read_text());assert old['sha256']==hashlib.sha256(out.read_bytes()).hexdigest();continue
    assert not out.exists()
    results=[json.loads(s) for s in (data/(mode+'_diagnostics.jsonl')).read_text().splitlines()]
    results=[r for r in results if r['displayed']];assert len(results)==len(rows)
    samples={0,len(rows)-1,max(range(len(rows)),key=lambda i:len(results[i]['pairs']))}
    if any(r['current_rules'] for r in results):samples.add(next(i for i,r in enumerate(results) if r['current_rules']))
    draft=dest/('.'+mode+'-'+uuid.uuid4().hex+'.mp4');started=time.monotonic()
    video=av.open(str(draft),'w',options={'movflags':'+faststart'});stream=video.add_stream('libx264',rate=10)
    stream.width=W;stream.height=H;stream.pix_fmt='yuv420p';stream.options={'crf':'21','preset':'veryfast'}
    stream.codec_context.thread_count=2;stream.time_base=stream.codec_context.time_base=Fraction(1,90000)
    mapping=[];rgbhash={};max_labels=0
    for i,(row,result) in enumerate(zip(rows,results)):
        assert (row['sample'],row['source_frame'],row['seconds'])==(result['sample'],result['source_frame'],result['seconds'])
        p=cache/'inputs/frames'/f"{row['sample']:04d}.png";digest=hashlib.sha256(p.read_bytes()).hexdigest();assert digest==hashes[p.name];rgbhash[p.name]=digest
        with Image.open(p) as im:image=im.convert('RGB')
        assert image.size==(row['width'],row['height'])
        page,labels=canvas(image,row,result,mode,i);max_labels=max(max_labels,labels['label_count'])
        if i in samples:page.save(data/(mode+f'_v2_sample_{i:04d}.jpg'),quality=88)
        vf=av.VideoFrame.from_ndarray(np.asarray(page),format='rgb24');vf.pts=round((row['seconds']-rows[0]['seconds'])*90000);vf.time_base=Fraction(1,90000)
        for packet in stream.encode(vf):video.mux(packet)
        mapping.append(dict(output_frame=i+1,sample=row['sample'],source_frame=row['source_frame'],source_seconds=row['seconds'],output_pts_seconds=vf.pts/90000))
    for packet in stream.encode():video.mux(packet)
    video.close()
    with av.open(str(draft)) as inp:
        inp.streams.video[0].codec_context.thread_count=2;times=[]
        for frame in inp.decode(video=0):
            assert not frame.is_corrupt and (frame.width,frame.height)==(W,H);times.append(float(frame.pts*frame.time_base))
    assert len(times)==len(rows) and all(b>a for a,b in zip(times,times[1:]))
    error=max(abs(a-b['output_pts_seconds']) for a,b in zip(times,mapping));assert error<1/90000
    draft.rename(out)
    with (data/(mode+'_frame_map_v2.csv')).open('w',newline='') as f:
        writer=csv.DictWriter(f,fieldnames=mapping[0]);writer.writeheader();writer.writerows(mapping)
    (data/'input_rgb_sha256.json').write_text(json.dumps(rgbhash,indent=2))
    report=dict(status='PASS',mode=mode,frames=len(rows),resolution=[W,H],nominal_fps=10,max_pts_error=error,
        text_overflow=0,text_overlap=0,label_overlap=0,max_input_labels=max_labels,sample_indices=sorted(samples),
        sha256=hashlib.sha256(out.read_bytes()).hexdigest(),elapsed_seconds=time.monotonic()-started,cpu_only=True)
    report_path.write_text(json.dumps(report,indent=2));print(json.dumps(report),flush=True)
