"""Read frozen checked diagnostics only; reuse verified encoder and layout."""
import sys,os,json,copy
from pathlib import Path
sys.path.extend(['/evidence/2026-11-01_contextra-fall-spatial-28clips_r01/helpers','/evidence/2026-10-30_contextra-fight-space-29clips_r01/helpers','/baseline'])
from common import OUT,read,load,evidence,sha
import render_space as encoder
from layout import W,H,IW,IH,IX,IY,text,overlay,MUTED
from contract import EDGES,edge_state
from PIL import Image,ImageDraw
COLORS=encoder.COLORS
def page(e,clip,index,total):
 row=e['row'];r=e['result'];c=r['candidate']
 assert sha(e['path'])==e['rgb_sha256']
 with Image.open(e['path']) as source:image=source.convert('RGB')
 assert image.size==(row['width'],row['height'])
 layer=Image.new('RGBA',image.size);d=ImageDraw.Draw(layer)
 for g in c['colors']:
  color=COLORS[g['state']];d.rectangle(g['bbox'],fill=color+(68,),outline=color+(255,),width=max(2,round(image.width/450)))
 image=Image.alpha_composite(image.convert('RGBA'),layer).convert('RGB');people=[]
 for i,(p,feat) in enumerate(zip(row['persons'],r['people'])):
  if not feat['eligible']:continue
  states=['HIGH' if x=='VALID' else 'BELOW' if x=='LOW_SCORE' else 'INVALID' for x in feat['joint_states']]
  tid=feat['track_id'];people.append(dict(identity='T'+str(tid) if tid is not None else 'q'+str(p['query']),track_id=tid,
   person_score=p['score'],bbox=p['bbox'],joints=p['keypoints'],joint_states=states,
   edge_states=[edge_state(states[a],states[b]) for a,b in EDGES],identity_color=(176,185,194),detail_slot=None))
 panel,meta=overlay(image,people)
 out=Image.new('RGB',(W,H),'#101820');d=ImageDraw.Draw(out);rects=[]
 text(d,rects,(20,16),clip+' | FALL 공간 후보 R1',34)
 text(d,rects,(20,78),f"원본 Frame {row['source_frame']} | PTS {row['seconds']:.3f}s | 관측 {index+1}/{total}",28)
 out.paste(panel,(IX,IY));color=COLORS['RED_CURRENT'] if c['hit'] else COLORS['YELLOW_HOLD'] if c['display'] else COLORS['BLUE_CANDIDATE']
 if c['display']:d.rectangle((IX,IY,IX+IW-1,IY+IH-1),outline=color,width=8)
 x=1950
 for y,label,value,size,ink in [
  (180,'현재 최종 조건','충족' if c['hit'] else '미충족' if c['observable'] else '자료 부족',40,color if c['hit'] else MUTED),
  (365,'표시 유지','현재 충족 표시' if c['hit'] else '노랑 유지 / 현재 충족 아님' if c['display'] else '없음',30,color if c['display'] else MUTED),
  (550,'새 시험 Event','발생' if c['event'] else '없음'+(' / cooldown 차단' if c['blocked'] else ''),30,'#edf1f5'),
  (735,'입력 참고',f"표시 객체 {len(people)}개 / Tracking "+('저장10' if r['tracking_mode']=='stored10' else '파생30'),28,'#edf1f5')]:
  text(d,rects,(x,y),label,34);text(d,rects,(x,y+65),value,size,ink)
 if c['association_uncertain']:text(d,rects,(x,930),'기여 영역 연결 불확실 / 옛 위치 미표시',26,MUTED)
 if r['annotation']['review_frame'] is not None:text(d,rects,(x,1020),f"자체촬영 검토 Frame {r['annotation']['review_frame']}",28,MUTED)
 for y,value in [(1240,'청색: 검토 후보     빨강: 현재 최종 조건 충족     노랑: 마지막 충족 뒤 1초 유지'),
  (1310,'현재 판정과 새 Event는 별개입니다. cooldown 중에도 빨간 판정은 표시합니다.'),
  (1380,'사람 0.25 / 관절 0.10 · bbox와 skeleton은 중립적인 입력 참고 표시입니다.'),
  (1450,'T: 이번 Tracking 결과의 ID · q: 현재 관측의 미추적 칸 · ID 복원을 뜻하지 않습니다.'),
  (1520,'기존 10FPS 관측 / 정상 속도 · 모델 재추론 / 후보 변경 / 운영 적용 없음'),
  (1590,'13·15의 감지 손실과 11·12의 미충족을 포함한 고정 후보입니다. 채택 전 검토용입니다.')]:
  text(d,rects,(20,y),value,28,MUTED if y>1240 else '#edf1f5')
 meta.update(kind='observation',state='RED' if c['hit'] else 'YELLOW' if c['display'] else 'BLUE',hit=c['hit'],
  display=c['display'],event=bool(c['event']),observable=c['observable'],group_states=c['colors'])
 return out,meta

if __name__=='__main__':
 clip=os.environ['CLIP'];rows,selected,spec,cache,maps=load(clip);by={r['sample']:r for r in rows}
 dest=OUT/'download'/clip;v=read(dest/'data/technical_validation.json');trace=dest/'data/diagnostics_checked.jsonl'
 assert sha(trace)==v['diagnostics_sha256']
 hashes=read(evidence(spec['rgb_hashes_remote']));entries=[]
 for line in trace.open():
  r=json.loads(line)
  if not r['displayed']:continue
  r['review_frame']=r['annotation']['review_frame']
  row=by[r['sample']];assert (row['source_frame'],row['seconds'])==(r['source_frame'],r['seconds'])
  entries.append(dict(kind='observation',row=row,result=r,seconds=row['seconds'],part_id='',
   path=cache/'frames'/f"{r['sample']:04d}.png",rgb_sha256=hashes[f"{r['sample']:04d}.png"]))
 assert len(entries)==len(selected)
 encoder.page=page
 encoder.render(clip,entries,dest/'Fall_Spatial_Review.mp4',dest/'data/video_validation.json')
