"""One original, no resampling: decoded-frame labels with original relative PTS."""
import csv
import datetime
import hashlib
import json
import os
from pathlib import Path
import resource
import shutil
import signal
import statistics
import subprocess
import sys
import time
from zoneinfo import ZoneInfo

ROOT=Path(__file__).resolve().parent.parent
REPORT=ROOT/'reports'
SOURCE=Path('/data/playlist/uploads/261028_아이서티_컨텍스트라_싸움_쓰러짐_자체제작_클립영상/2026-10-28 15_48_17 이동형 #3.mp4')
LABEL='아이서티_컨텍스트라_싸움_쓰러짐_자체촬영_원본01'
DOWNLOAD=ROOT/(LABEL+'_검토자료')
OUTPUT=DOWNLOAD/(LABEL+'_Frame검토본.mp4')
FONT=Path('/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc')
CPUS=sorted(os.sched_getaffinity(0))[-2:]
EXPECTED_SHA='e26ff19c6c4d5e97b308a383c8f511c79dea08ff9bc2d6a9eada8451f59c46bf'

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

def save(p,value):
    p.write_text(json.dumps(value,ensure_ascii=False,indent=2),encoding='utf-8')

def resources():
    m={s.split(':')[0]:int(s.split()[1])*1024 for s in Path('/proc/meminfo').read_text().splitlines() if s.startswith('MemAvailable:')}
    r=dict(root_free=shutil.disk_usage('/').free,data_free=shutil.disk_usage(ROOT).free,
           ram_available=m['MemAvailable'],load=os.getloadavg(),cpus=CPUS)
    assert r['root_free']>=15*2**30 and r['data_free']>=5*2**30 and r['ram_available']>=4*2**30,r
    return r

def child_limits():
    os.sched_setaffinity(0,CPUS)
    os.nice(10)

def run(cmd,stage,timeout):
    before=resources();started=time.monotonic();samples=[]
    stdout=REPORT/(stage+'.stdout');stderr=REPORT/(stage+'.stderr')
    save(REPORT/(stage+'_command.json'),cmd)
    with stdout.open('wb') as out,stderr.open('wb') as err:
        p=subprocess.Popen(cmd,stdout=out,stderr=err,start_new_session=True,preexec_fn=child_limits)
        try:
            while True:
                try:code=p.wait(timeout=min(30,max(.1,timeout-(time.monotonic()-started))));break
                except subprocess.TimeoutExpired:
                    if time.monotonic()-started>=timeout:raise TimeoutError(stage)
                    current=resources();status=Path('/proc')/str(p.pid)/'status'
                    rss=next((int(s.split()[1])*1024 for s in status.read_text().splitlines() if s.startswith('VmRSS:')),0) if status.exists() else 0
                    assert rss<2*2**30,('Own process RSS limit',rss)
                    current['process_rss']=rss;samples.append(current)
                    progress=''
                    if stage=='render' and (REPORT/'progress.txt').exists():
                        progress=' '.join((REPORT/'progress.txt').read_text().splitlines()[-6:])
                    print(stage,round(time.monotonic()-started,1),'seconds',progress,flush=True)
        except BaseException:
            if p.poll() is None:
                os.killpg(p.pid,signal.SIGTERM)
                try:p.wait(timeout=10)
                except subprocess.TimeoutExpired:os.killpg(p.pid,signal.SIGKILL);p.wait()
            raise
    save(REPORT/(stage+'_resources.json'),dict(before=before,samples=samples,after=resources(),
        elapsed_seconds=round(time.monotonic()-started,3),returncode=code,max_child_rss_kib=resource.getrusage(resource.RUSAGE_CHILDREN).ru_maxrss))
    if code:raise RuntimeError(stage+': '+stderr.read_text(errors='replace')[-2500:])
    assert not stderr.read_text().strip(),(stage,stderr.read_text()[-1500:])
    return stdout

def probe(path,stage):
    cmd=['ffprobe','-v','error','-threads','2','-select_streams','v:0','-show_streams','-show_frames','-show_entries',
         'stream=codec_name,width,height,pix_fmt,color_range,color_space,color_transfer,color_primaries,time_base,r_frame_rate,avg_frame_rate,nb_frames,duration:frame=pts_time,best_effort_timestamp_time,key_frame,pict_type,width,height',
         '-of','json',str(path)]
    return json.loads(run(cmd,stage,600).read_text())

def prepare():
    assert digest(SOURCE)==EXPECTED_SHA,'Source fingerprint changed'
    assert FONT.is_file()
    DOWNLOAD.mkdir(exist_ok=False);(DOWNLOAD/'validation').mkdir()
    before=probe(SOURCE,'original_probe');frames=before['frames']
    assert len(frames)==32046
    pts=[float(f['best_effort_timestamp_time']) for f in frames]
    assert all(b>a for a,b in zip(pts,pts[1:])), 'Original PTS is not strictly increasing'
    assert all((f['width'],f['height'])==(1280,720) for f in frames)
    save(REPORT/'source_identity.json',dict(file=str(SOURCE),sha256=EXPECTED_SHA,bytes=SOURCE.stat().st_size,frames=len(frames)))
    (ROOT/'header.txt').write_text(LABEL,encoding='utf-8')
    from PIL import ImageFont
    font=ImageFont.truetype(str(FONT),32)
    for value,y in [(LABEL,15),(f'원본 Frame {len(frames)} / {len(frames)} | 경과시간 00:22:07.035',65)]:
        box=font.getbbox(value);assert box[2]-box[0]<=1240 and y+box[3]<=120,(value,box)
    label="원본 Frame %{eif\\:n+1\\:d} / 32046 | 경과시간 %{pts\\:hms}"
    vf=("setpts=PTS-STARTPTS,pad=iw:ih+120:0:120:color=black,"
        f"drawtext=fontfile='{FONT}':textfile='{ROOT/'header.txt'}':expansion=none:fontsize=32:fontcolor=white:x=20:y=15,"
        f"drawtext=fontfile='{FONT}':text='{label}':fontsize=32:fontcolor=white:x=20:y=65")
    draft=DOWNLOAD/'review_incomplete.mp4'
    cmd=['ffmpeg','-nostdin','-hide_banner','-loglevel','error','-n','-threads','2','-i',str(SOURCE),
         '-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('SOURCE_DECODE_PASS',len(frames),'RENDER_START',flush=True)
    run(cmd,'render',1800)
    after=probe(draft,'review_probe');new=after['frames'];assert len(new)==len(frames)
    outpts=[float(f['best_effort_timestamp_time']) for f in new]
    assert all(b>a for a,b in zip(outpts,outpts[1:]))
    error=max(abs((a-pts[0])-(b-outpts[0])) for a,b in zip(pts,outpts))
    assert error<.001,error
    assert all((f['width'],f['height'])==(1280,840) for f in new)
    for k in ('color_range','color_space','color_transfer','color_primaries'):assert before['streams'][0][k]==after['streams'][0][k]
    assert digest(SOURCE)==EXPECTED_SHA,'Source changed during processing'
    with (DOWNLOAD/'frame_map.csv').open('x',newline='',encoding='utf-8-sig') as f:
        w=csv.writer(f);w.writerow(['source_frame_1based','source_pts_seconds','elapsed_seconds','review_frame_1based','review_pts_seconds','source_key_frame','source_pict_type'])
        for i,(a,b) in enumerate(zip(frames,new),1):
            w.writerow([i,a['best_effort_timestamp_time'],f'{pts[i-1]-pts[0]:.6f}',i,b['best_effort_timestamp_time'],a['key_frame'],a.get('pict_type')])
    gaps=[dict(previous_frame=i,next_frame=i+1,source_previous_pts=a,source_next_pts=b,gap_seconds=b-a,
               review_gap_seconds=outpts[i]-outpts[i-1]) for i,(a,b) in enumerate(zip(pts,pts[1:]),1) if b-a>.1]
    assert len(gaps)==1 and abs(gaps[0]['gap_seconds']-gaps[0]['review_gap_seconds'])<.001
    assert abs(gaps[0]['source_previous_pts']-599.983332)<.001 and abs(gaps[0]['source_next_pts']-601)<.001
    assert not OUTPUT.exists();draft.rename(OUTPUT)
    validation=dict(status='PASS',label=LABEL,frame_count=len(frames),first_source_pts=pts[0],last_source_pts=pts[-1],
        first_review_pts=outpts[0],last_review_pts=outpts[-1],max_relative_pts_error_seconds=error,
        original_stream=before['streams'][0],review_stream=after['streams'][0],large_pts_gaps=gaps,
        original_decode_errors='',review_decode_errors='',render_errors='',frame_number_expression='decoded n+1',
        header_height=120,original_picture_size=[1280,720],output_size=[1280,840],
        source_sha256=EXPECTED_SHA,review_sha256=digest(OUTPUT),source_unchanged=True,
        resampling=False,semantic_review=False,source_frame_loss='Not established from PTS gaps alone',
        active_child_processes=0,timestamp=datetime.datetime.now(ZoneInfo('Asia/Seoul')).isoformat())
    save(DOWNLOAD/'validation/video_validation.json',validation)
    # Extract only the information band; no scene interpretation is performed.
    indices=[0,gaps[0]['previous_frame']-1,gaps[0]['next_frame']-1,len(frames)-1]
    select='+'.join(f'eq(n\\,{i})' for i in indices)
    band=REPORT/'header_samples';band.mkdir(exist_ok=False)
    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')],'header_samples',600)
    from PIL import Image
    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:assert im.size==(1280,120);sheet.paste(im,(0,i*120))
    sheet.save(DOWNLOAD/'validation/header_samples.png')
    save(DOWNLOAD/'validation/header_sample_frames.json',[i+1 for i in indices])
    print(json.dumps(dict(status='RENDER_AND_TECH_VERIFY_PASS',frames=len(frames),pts_error=error,gaps=gaps,download=str(DOWNLOAD)),ensure_ascii=False),flush=True)

if __name__=='__main__':prepare()
