"""Current evidence, display hold, and Event are separate visual channels."""
import bootstrap
import csv
from fractions import Fraction
import hashlib
import json
import os
from pathlib import Path
import time
import av
import numpy as np
from PIL import Image,ImageDraw,ImageOps
from inputs import OUT,load,evidence
from contract import palette,observation,edge_state,EDGES
from layout import W,H,IW,IH,IX,IY,FONT,text,overlay,MUTED
from render_candidates import hull,LABEL

COLORS={'RED_CURRENT':(255,65,65),'YELLOW_HOLD':(255,205,35),'BLUE_CANDIDATE':(40,145,255)}
TEAL='#42f0d0';PURPLE='#df94ff'
RULE_NAMES={'ARM_DIRECT':'팔 직접 접근','ARM_CURVE':'팔 굽힘/곡선','ARM_RETURN':'팔 후속 동작',
            'LEG_DIRECT':'발 직접 접근','LEG_RETURN':'발 후속 동작','CLOSE_REVERSAL':'근접 반전+몸통 반응'}
VETO={'MUTUAL_HAND_CONTACT':'서로 손 근접','LOW_ENDPOINT':'낮은 손발 위치',
      'BODY_DROP_DOMINANT':'몸 전체 하강 우세','HORIZONTAL_BODY_LOW_RESIDUAL':'가로 몸통+약한 상대동작'}


def draw_panel(image,row,view,result,colors):
    by={b['order']:b for b in view['bodies']}
    mask=Image.new('RGBA',image.size);d=ImageDraw.Draw(mask)
    polygons=[]
    for g in result['groups']:
        coords=[]
        for order in g['orders']:
            l,t,r,b=by[order]['bbox'];coords.extend(((l,t),(r,t),(r,b),(l,b)))
        poly=hull(coords);color=COLORS[g['state']]
        if len(poly)>2:
            d.polygon(poly,fill=color+(68,),outline=color+(255,),width=max(3,round(image.width/450)))
            polygons.append((g,poly))
    image=Image.alpha_composite(image.convert('RGBA'),mask).convert('RGB')
    people=[p for p in observation(row,colors) if p['visible']]
    for p in people:
        p['joint_states']=['BELOW' if x=='LOW' else x for x in p['joint_states']]
        p['edge_states']=[edge_state(p['joint_states'][a],p['joint_states'][b]) for a,b in EDGES]
    panel,meta=overlay(image,people);base=panel.copy();pd=ImageDraw.Draw(panel)
    fit=ImageOps.contain(image,(IW,IH));sx=fit.width/image.width;sy=fit.height/image.height
    ox=(IW-fit.width)//2;oy=(IH-fit.height)//2
    def pt(p):return (ox+max(0,min(fit.width-1,p[0]*sx)),oy+max(0,min(fit.height-1,p[1]*sy)))
    rendered=set()
    for p in result['pairs']:
        for f in p['diagnostics']:
            partial=f['tests']['motion'] and f['tests']['target_distance']
            if not (f['rules'] or f['suppression_applied'] or partial):continue
            key=(p['orders'][f['actor']],f['limb'],bool(f['rules']),f['suppression_applied'])
            if key in rendered:continue
            rendered.add(key);color=TEAL if f['rules'] else PURPLE if f['suppression_applied'] else TEAL
            radius=11 if f['rules'] else 7;width=4 if f['rules'] or f['suppression_applied'] else 2
            x,y=pt(f['endpoint']);pd.ellipse((x-radius,y-radius,x+radius,y+radius),outline=color,width=width)
            if f['rules'] and f['suppression_applied']:
                pd.ellipse((x-radius-5,y-radius-5,x+radius+5,y+radius+5),outline=PURPLE,width=2)
            pd.line((*pt(f['root_point']),x,y),fill=color,width=width)
            if f['rules']:pd.line((x,y,*pt(f['target_point'])),fill=color,width=2)
    for box in meta['label_boxes']:
        box=tuple(round(x) for x in box);panel.paste(base.crop(box),box)
    return panel,meta


def page(panel,row,view,result,cfg,clip,index,total):
    out=Image.new('RGB',(W,H),'#101820');d=ImageDraw.Draw(out);rects=[];out.paste(panel,(IX,IY))
    text(d,rects,(20,16),clip+' | B 통합 후보 v1 | 운영 미적용',34)
    text(d,rects,(20,78),f"입력 Frame {row['source_frame']} | PTS {row['seconds']:.3f}s | 관측 {index+1}/{total} | 10FPS",28)
    ep=result['episode'];color='#ff5555' if result['hit'] else '#ffcd23' if ep['display'] else '#65b7ff'
    state='현재 최종 조건 충족' if result['hit'] else '직전 조건 표시 유지 / 현재 충족 아님' if ep['display'] else '현재 최종 조건 미충족' if result['observable'] else '근거 부족 / 준비 중'
    if ep['display']:d.rectangle((IX,IY,IX+IW-1,IY+IH-1),outline=color,width=8)
    text(d,rects,(20,1220),state,32,color)
    text(d,rects,(20,1270),f"공간 {len(result['groups'])}그룹 / {len(view['pairs'])}쌍 | 현재 근거 {int(result['raw'])} | 최종 {int(result['hit'])} | 새 Event {int(ep['event'])}",26)
    ref=LABEL[result['reference']]
    if result['review_frame'] is not None:ref+=f" / 원본01 검토 Frame {result['review_frame']}"
    text(d,rects,(20,1330),ref,26)
    lines=['청색: 공간 후보 / 빨강: 현재 기여 / 노랑: 마지막 충족 뒤 1초',
           '청록 얇은 선: 부분 근거 / 굵은 선: 현재 규칙 근거',
           '보라: 일부 규칙 억제 / 청록+보라: 다른 규칙 근거는 남음',
           '쿨다운 중에도 현재 판정 표시 / Event0 또는1은 이번 관측 값',
           'T는 기존 ID, q는 Frame별 칸 / 싸움 당사자 확정 표기 아님',
           'D/N 세부 구간·새 우선순위 미지정 / 사용자 검토 후 추가']
    for i,line in enumerate(lines):text(d,rects,(20,1390+i*58),line,26,MUTED)
    x=1910
    text(d,rects,(x,150),'손·팔 / 발·다리 / 근접 반전+상대 몸통 반응',28)
    text(d,rects,(x,202),f"bbox0.25 · 관절0.10 / 최근 {cfg['window']:.1f}초 / 같은 주체 {cfg['min_samples']}관측",24)
    text(d,rects,(x,248),f"손 속도>={cfg['hand_speed']:.2f} 거리<={cfg['hand_near']:.2f} 높이<={cfg['hand_vertical']:.2f}",24)
    entries=sorted(result['pairs'],key=lambda p:(not p['hit'],not p['raw'],not p['suppression_applied'],p['orders']))[:3]
    for i,p in enumerate(entries):
        y=310+i*380;lim=(x,y,3180,y+355);d.rectangle(lim,outline='#43515e',width=2)
        group=next((g['id'] for g in result['groups'] if set(p['orders'])<=set(g['orders'])),'?')
        text(d,rects,(x+16,y+12),group+' / '+p['token']+' | '+' + '.join(p['refs']),28,limit=lim)
        text(d,rects,(x+16,y+57),f"근접 {'현재' if p['candidate_state']=='CURRENT_NEAR' else '유지'} | 현재근거 {int(p['raw'])} / 최종 {int(p['hit'])} / 관측수 {p['counts']}",24,limit=lim)
        ds=p['diagnostics']
        f=max(ds,key=lambda f:(bool(f['rules']),f['suppression_applied'],sum(f['tests'].values()),f['speed'])) if ds else None
        if f:
            rules='/'.join(RULE_NAMES[k] for k in f['rules']) or '현재 규칙 근거 없음'
            text(d,rects,(x+16,y+102),p['refs'][f['actor']]+' '+f['limb']+' | '+rules,24,TEAL if f['rules'] else MUTED,lim)
            text(d,rects,(x+16,y+148),f"속도 {f['speed']:.3f} / 거리 {f['near']:.3f} / 높이 {f['vertical']:.3f}",24,limit=lim)
            text(d,rects,(x+16,y+191),f"접근 {f['approach']/f['elapsed']:.3f}>={cfg['approach']:.2f} / 방향 {f['cosine']:.3f}>={cfg['direction']:.2f}",24,limit=lim)
            veto='/'.join(VETO[k] for k in f['suppression_evidence']) or '없음'
            text(d,rects,(x+16,y+234),'억제 근거: '+veto+' / 적용 '+str(int(f['suppression_applied'])),22,PURPLE if f['suppression_applied'] else MUTED,lim)
            text(d,rects,(x+16,y+277),f"실제 입력 PTS {f['input_start']:.3f}~{f['input_end']:.3f} / 근접 반전수 {p['contact_counts']}",22,MUTED,lim)
            text(d,rects,(x+16,y+318),'통과 규칙은 중복 가산 안 함 / 수치 상세는 data 참조',22,MUTED,lim)
        else:text(d,rects,(x+16,y+145),'유효 동작 관측 부족 / '+p['reason'],24,MUTED,lim)
    if not entries:text(d,rects,(x+16,350),'유효 후보쌍 없음 / 정상 행동 확정 아님',26)
    text(d,rects,(x,1490),f"상세 {len(entries)}쌍 / {max(0,len(result['pairs'])-3)}쌍 상세 생략, 전체 data 보존",24,MUTED)
    text(d,rects,(x,1550),'공간 → 현재 근거 → 최근 관측 → 최종 조건 → Event',24)
    text(d,rects,(x,1610),'근거 부족과 억제 적용은 다름 / 현재 최종 조건만 빨강',24,MUTED)
    text(d,rects,(x,1670),'그룹 대응 불확실 시 ROI 상태만 유지 / 옛 위치 재사용 없음',24,MUTED)
    return out


def main():
    clip=os.environ['CLIP'];started=time.monotonic();rows,chosen,spec,cache,maps=load(clip)
    rows=[r for r in rows if r['sample'] in chosen];dest=OUT/'download'/clip;data=dest/'data'
    output=dest/'B_Integrated_Review.mp4';report=data/'video_validation.json'
    if report.exists():
        check=json.loads(report.read_text());assert hashlib.sha256(output.read_bytes()).hexdigest()==check['sha256'];print('REUSED',clip);return
    draft=dest/'.B_Integrated_Review.partial.mp4';assert not output.exists() and not draft.exists()
    cfg=json.loads((OUT/'candidates.json').read_text())['candidates'][0]['config']
    views=[json.loads(s) for s in (OUT/'work'/clip/'enriched.jsonl').read_text().splitlines()];views=[r for r in views if r['displayed']]
    results=[json.loads(s) for s in (data/'B_Integrated_diagnostics.jsonl').read_text().splitlines()];results=[r for r in results if r['displayed']]
    assert len(rows)==len(views)==len(results)
    colors=palette(rows);(data/'identity_palette.json').write_text(json.dumps(colors));rgb_hashes=json.loads(evidence(spec['rgb_hashes_remote']).read_text())
    con=av.open(str(draft),'w',options={'movflags':'faststart'});stream=con.add_stream('libx264',rate=10)
    stream.width=W;stream.height=H;stream.pix_fmt='yuv420p';stream.options={'crf':'22','preset':'veryfast'}
    stream.codec_context.thread_count=1;stream.time_base=stream.codec_context.time_base=Fraction(1,90000)
    samples={0,len(rows)-1,max(range(len(rows)),key=lambda i:len(views[i]['pairs']))}
    for pred in (lambda r:r['hit'],lambda r:r['episode']['display'] and not r['hit'],lambda r:any(p['suppression_applied'] for p in r['pairs'])):
        n=next((i for i,r in enumerate(results) if pred(r)),None)
        if n is not None:samples.add(n)
    mapping=[]
    for i,(row,view,r) in enumerate(zip(rows,views,results)):
        assert (row['sample'],row['source_frame'],row['seconds'])==(view['sample'],view['source_frame'],view['seconds'])==(r['sample'],r['source_frame'],r['seconds'])
        path=cache/'frames'/f"{row['sample']:04d}.png";assert hashlib.sha256(path.read_bytes()).hexdigest()==rgb_hashes[path.name]
        with Image.open(path) as im:image=im.convert('RGB')
        assert image.size==(row['width'],row['height'])
        panel,meta=draw_panel(image,row,view,r,colors);out=page(panel,row,view,r,cfg,clip,i,len(rows))
        if i in samples:out.save(data/f'sample_{i:04d}.jpg',quality=90)
        pts=round((row['seconds']-rows[0]['seconds'])*90000)
        frame=av.VideoFrame.from_ndarray(np.asarray(out),format='rgb24');frame.pts=pts;frame.time_base=Fraction(1,90000)
        for packet in stream.encode(frame):con.mux(packet)
        mapping.append(dict(output_frame=i+1,sample=row['sample'],source_frame=row['source_frame'],source_seconds=row['seconds'],review_frame=view['review_frame'],output_seconds=pts/90000))
        if (i+1)%200==0:print('FRAME',clip,i+1,len(rows),flush=True)
    for packet in stream.encode():con.mux(packet)
    con.close();times=[]
    with av.open(str(draft)) as inp:
        inp.streams.video[0].codec_context.thread_count=1
        for f in inp.decode(video=0):assert not f.is_corrupt and (f.width,f.height)==(W,H);times.append(float(f.pts*f.time_base))
    assert len(times)==len(rows) and all(b>a for a,b in zip(times,times[1:]))
    error=max(abs(t-r['output_seconds']) for t,r in zip(times,mapping));assert error<1/90000
    draft.rename(output)
    with (data/'frame_map.csv').open('w',newline='') as f:
        w=csv.DictWriter(f,fieldnames=mapping[0]);w.writeheader();w.writerows(mapping)
    result=dict(status='PASS',frames=len(rows),resolution=[W,H],nominal_fps=10,max_pts_error=error,text_overflow=0,text_overlap=0,
        source_rgb_hashes_verified=len(rows),sha256=hashlib.sha256(output.read_bytes()).hexdigest(),elapsed_seconds=time.monotonic()-started)
    report.write_text(json.dumps(result,indent=2));print('VIDEO_PASS',clip,json.dumps(result),flush=True)


if __name__=='__main__':main()
