"""User-approved GOP exclusion; no repair, interpolation, or original mutation."""
import csv
import json
from pathlib import Path
from PIL import Image,ImageFont
import prepare_review as base

ROOT=base.ROOT;REPORT=base.REPORT;SOURCE=base.SOURCE;DOWNLOAD=base.DOWNLOAD
SEGMENTS=((0.,439.516944),(441.775299,445.881399),(448.098693,None))

def packets(path,stage):
    cmd=['ffprobe','-v','error','-select_streams','v:0','-show_packets','-show_data_hash','sha256',
         '-show_entries','packet=pts_time,duration_time,flags,data_hash','-of','json',str(path)]
    return json.loads(base.run(cmd,stage,120).read_text())['packets']

def main():
    assert base.digest(SOURCE)==base.EXPECTED_SHA
    parts=ROOT/'clean_segments';parts.mkdir(exist_ok=False)
    source=packets(SOURCE,'source_packet_fingerprints')
    expected=[];part_info=[];paths=[]
    for part,(start,end) in enumerate(SEGMENTS,1):
        selected=[(i+1,p) for i,p in enumerate(source) if float(p['pts_time'])>=start-1e-7 and (end is None or float(p['pts_time'])<end-1e-7)]
        assert selected and 'K' in selected[0][1]['flags']
        path=parts/f'part{part:02d}.mp4';paths.append(path)
        cmd=['ffmpeg','-nostdin','-hide_banner','-loglevel','error','-n','-ss',f'{start:.6f}','-i',str(SOURCE),'-map','0:v:0','-an']
        if end is not None:cmd+=['-t',f'{end-start:.6f}']
        cmd+=['-c:v','copy','-map_metadata','-1','-avoid_negative_ts','make_zero','-video_track_timescale','1000000',str(path)]
        base.run(cmd,f'part{part:02d}_copy',120)
        actual=packets(path,f'part{part:02d}_packets')
        assert len(actual)==len(selected),(part,len(actual),len(selected))
        assert [p['data_hash'] for p in actual]==[p['data_hash'] for _,p in selected],('Packet content changed',part)
        origin=float(actual[0]['pts_time'])
        assert max(abs((float(a['pts_time'])-origin)-(float(s['pts_time'])-start)) for a,(_,s) in zip(actual,selected))<.000002
        part_info.append(dict(part=part,source_start=start,source_end_exclusive=end,frames=len(actual),first_review_frame=len(expected)+1,last_review_frame=len(expected)+len(actual)))
        expected.extend(dict(part=part,source_packet_1based=i,source_pts=float(p['pts_time']),packet_hash=p['data_hash']) for i,p in selected)
        print('COPY_PASS',part,len(actual),flush=True)
    assert len(expected)==31937
    concat=ROOT/'concat.txt';concat.write_text(''.join(f"file '{p}'\n" for p in paths))
    merged=ROOT/'clean_joined.mp4'
    base.run(['ffmpeg','-nostdin','-hide_banner','-loglevel','error','-n','-f','concat','-safe','0','-i',str(concat),
              '-map','0:v:0','-an','-c:v','copy','-map_metadata','-1','-video_track_timescale','1000000',str(merged)],'clean_concat',180)
    joined=packets(merged,'clean_joined_packets')
    assert len(joined)==len(expected) and [p['data_hash'] for p in joined]==[p['packet_hash'] for p in expected]
    before=base.probe(merged,'clean_joined_decode');frames=before['frames'];assert len(frames)==len(expected)
    times=[float(f['best_effort_timestamp_time']) for f in frames]
    assert all(b>a for a,b in zip(times,times[1:]))
    assert all(abs(float(p['pts_time'])-t)<1e-6 for p,t in zip(joined,times))
    for info in part_info:
        a=info['first_review_frame']-1;b=info['last_review_frame']
        offset=expected[a]['source_pts']-times[a]
        assert max(abs(t+offset-e['source_pts']) for t,e in zip(times[a:b],expected[a:b]))<.000002
        info['source_time_offset']=offset;info['joined_start']=times[a]
    exclusions=[]
    for a,b in ((439.516944,441.775299),(445.881399,448.098693)):
        dropped=[i+1 for i,p in enumerate(source) if a-1e-7<=float(p['pts_time'])<b-1e-7]
        exclusions.append(dict(start_seconds=a,end_exclusive_seconds=b,duration_seconds=b-a,packet_count=len(dropped),first_packet=dropped[0],last_packet=dropped[-1],reason='decoder anomaly within GOP; user-approved exclusion through next keyframe'))
    base.save(REPORT/'exclusion_plan.json',dict(source_sha256=base.EXPECTED_SHA,retained=part_info,excluded=exclusions,original_declared_packets=len(source),original_full_decode_frames=32042,original_decode_errors=(REPORT/'original_probe.stderr').read_text(),numbering='review frames after exclusions, not original full decode ordinal'))
    header=base.LABEL+' (오류 구간 2곳 제외)';(ROOT/'clean_header.txt').write_text(header,encoding='utf-8')
    font=ImageFont.truetype(str(base.FONT),32);box=font.getbbox(header);assert box[2]-box[0]<1240
    vf=f"setpts=PTS-STARTPTS,pad=iw:ih+120:0:120:color=black,drawtext=fontfile='{base.FONT}':textfile='{ROOT/'clean_header.txt'}':expansion=none:fontsize=32:fontcolor=white:x=20:y=15"
    for info in part_info:
        a,b=info['first_review_frame']-1,info['last_review_frame']-1
        label="검토 Frame %{eif\\:n+1\\:d} / 31937 | 원본시간 %{pts\\:hms\\:"+f"{info['source_time_offset']:.6f}"+"}"
        vf+=f",drawtext=fontfile='{base.FONT}':text='{label}':fontsize=32:fontcolor=white:x=20:y=65:enable='between(n,{a},{b})'"
    draft=DOWNLOAD/'review_excluding_errors_incomplete.mp4';output=DOWNLOAD/(base.LABEL+'_Frame검토본_오류구간제외.mp4')
    cmd=['ffmpeg','-nostdin','-hide_banner','-loglevel','error','-n','-threads','2','-i',str(merged),'-map','0:v:0','-an','-map_metadata','-1',
         '-filter_threads','1','-vf',vf,'-c:v','libx264','-preset','veryfast','-crf','20','-threads','2','-pix_fmt','yuvj420p',
         '-color_range','pc','-colorspace','bt709','-color_trc','bt709','-color_primaries','bt709','-fps_mode','passthrough','-enc_time_base','-1',
         '-video_track_timescale','1000000','-movflags','+faststart','-progress',str(REPORT/'progress.txt'),str(draft)]
    print('CLEAN_DECODE_PASS',len(frames),'RENDER_START',flush=True)
    base.run(cmd,'render',1800)
    after=base.probe(draft,'clean_review_probe');out=after['frames'];assert len(out)==len(expected)
    outtimes=[float(f['best_effort_timestamp_time']) for f in out]
    error=max(abs(t-u) for t,u in zip(times,outtimes));assert error<.001
    assert all(b>a for a,b in zip(outtimes,outtimes[1:])) and all((f['width'],f['height'])==(1280,840) for f in out)
    assert base.digest(SOURCE)==base.EXPECTED_SHA
    mapping=DOWNLOAD/'frame_map.csv'
    with mapping.open('x',newline='',encoding='utf-8-sig') as f:
        w=csv.writer(f);w.writerow(['review_frame_1based','review_pts_seconds','retained_segment','source_packet_1based_not_decoded_frame','source_pts_seconds','source_elapsed_seconds'])
        for i,(e,t) in enumerate(zip(expected,outtimes),1):w.writerow([i,f'{t:.6f}',e['part'],e['source_packet_1based'],f"{e['source_pts']:.6f}",f"{e['source_pts']:.6f}"])
    gaps=[dict(previous_review_frame=i,next_review_frame=i+1,previous_source_time=expected[i-1]['source_pts'],next_source_time=expected[i]['source_pts'],source_gap=expected[i]['source_pts']-expected[i-1]['source_pts'],review_gap=b-a) for i,(a,b) in enumerate(zip(outtimes,outtimes[1:]),1) if b-a>.1]
    assert len(gaps)==1 and abs(gaps[0]['source_gap']-1.016668)<.001 and abs(gaps[0]['source_gap']-gaps[0]['review_gap'])<.001
    assert not output.exists();draft.rename(output)
    v=dict(status='PASS_WITH_DISCLOSED_SOURCE_EXCLUSIONS',label=base.LABEL,frame_count=len(out),original_packet_count=len(source),original_full_decode_frames=32042,
        source_sha256=base.EXPECTED_SHA,source_unchanged=True,review_filename=output.name,review_sha256=base.digest(output),
        first_review_pts=outtimes[0],last_review_pts=outtimes[-1],last_source_pts=expected[-1]['source_pts'],max_relative_pts_error_seconds=error,
        original_decode_errors=(REPORT/'original_probe.stderr').read_text(),retained_decode_errors='',review_decode_errors='',
        excluded=exclusions,retained_segments=part_info,preserved_pts_gaps=gaps,numbering='1-based review decoded frames after exclusion; use CSV to find original PTS',
        original_packet_identity_preserved=True,original_picture_size=[1280,720],output_size=[1280,840],header_height=120,
        resampling=False,frame_interpolation=False,retained_speed_unchanged=True,semantic_review=False,
        color_metadata_preserved=all(before['streams'][0][k]==after['streams'][0][k] for k in ('color_range','color_space','color_transfer','color_primaries')),
        review_stream=after['streams'][0],active_child_processes=0)
    assert v['color_metadata_preserved']
    base.save(DOWNLOAD/'validation/video_validation.json',v)
    base.save(DOWNLOAD/'validation/exclusions.json',dict(retained=part_info,excluded=exclusions))
    indices=sorted({0,len(out)-1,*[x['first_review_frame']-1 for x in part_info[1:]],*[x['first_review_frame']-2 for x in part_info[1:]],gaps[0]['previous_review_frame']-1,gaps[0]['next_review_frame']-1})
    band=REPORT/'clean_header_samples';band.mkdir(exist_ok=False)
    select='+'.join(f'eq(n\\,{i})' for i in indices)
    base.run(['ffmpeg','-nostdin','-hide_banner','-loglevel','error','-n','-threads','2','-i',str(output),'-an','-filter_threads','1',
        '-vf',f'select={select},crop=1280:120:0:0','-fps_mode','passthrough','-threads','1',str(band/'header_%02d.png')],'clean_header_samples',600)
    pictures=sorted(band.glob('*.png'));assert len(pictures)==len(indices)
    sheet=Image.new('RGB',(1280,120*len(pictures)))
    for i,p in enumerate(pictures):
        with Image.open(p) as im:sheet.paste(im,(0,i*120))
    sheet.save(DOWNLOAD/'validation/header_samples.png')
    base.save(DOWNLOAD/'validation/header_sample_frames.json',[i+1 for i in indices])
    print(json.dumps(dict(status=v['status'],frames=len(out),excluded_packets=109,excluded_seconds=sum(x['duration_seconds'] for x in exclusions),download=str(DOWNLOAD)),ensure_ascii=False),flush=True)

if __name__=='__main__':main()
