#!/usr/bin/env python3
from pathlib import Path
import argparse,datetime,hashlib,json,os,subprocess
ROOT=Path('/data/video-pipeline/HaTramAudio/project/028-Nguoi-Duoc-Goi-Ten-Cuoi-Cung');RUN='run-20260722T063856Z-87753fd5';PID=ROOT.name;OWNER='Levy'
def now():return datetime.datetime.now(datetime.timezone.utc).isoformat().replace('+00:00','Z')
def sha(p):
 h=hashlib.sha256()
 with p.open('rb') as f:
  for b in iter(lambda:f.read(8*1024*1024),b''):h.update(b)
 return h.hexdigest()
def guard():
 d=json.loads((ROOT/'.ownership-lock.json').read_text())
 for k,v in {'project_id':PID,'run_id':RUN,'owner':OWNER}.items():
  if d.get(k)!=v:raise RuntimeError('ownership mismatch: '+k)
def probe(p):
 return json.loads(subprocess.check_output(['ffprobe','-v','error','-show_entries','format=duration,size,start_time:stream=index,codec_name,profile,codec_type,width,height,pix_fmt,field_order,r_frame_rate,sample_rate,channels,start_time','-of','json',str(p)]))
def atomic(path,d):
 guard();tmp=path.with_name('.'+path.name+'.tmp');tmp.write_text(json.dumps(d,ensure_ascii=False,indent=2)+'\n');os.replace(tmp,path)
def streams_ok(pr):
 vs=[x for x in pr['streams'] if x.get('codec_type')=='video'];au=[x for x in pr['streams'] if x.get('codec_type')=='audio']
 return len(vs)==1 and len(au)==1 and vs[0].get('codec_name')=='h264' and vs[0].get('width')==1920 and vs[0].get('height')==1080 and vs[0].get('r_frame_rate')=='30/1' and au[0].get('codec_name')=='aac' and au[0].get('sample_rate')=='48000',vs[0],au[0]
def main():
 a=argparse.ArgumentParser();a.add_argument('stage',choices=['footage','final','transcode']);args=a.parse_args();guard()
 work=ROOT/'work'/args.stage/RUN;t=json.loads((work/'terminal-response.json').read_text());b=json.loads((work/'request-bindings.json').read_text());req=json.loads((work/'request.json').read_text());m=json.loads((ROOT/'script/project-manifest.json').read_text());canon=m['canon_sha256']
 if t.get('status')!='completed' or t.get('verification',{}).get('verified') is not True:raise RuntimeError('server terminal verification failed')
 if args.stage=='footage':
  out=Path(req['output']);receipt=ROOT/'log/footage-render.json';layout=m['gate_events']['layout']['artifact_sha256'];tts=json.loads((ROOT/'script/tts-manifest.json').read_text());expected=float(tts['output_duration'])
  command=' '.join(map(str,t.get('command',[])))
  checks={'terminal_completed':True,'server_verified':True,'gpu_nvenc_encoder':'h264_nvenc' in command,'mirror_intent_bound':b.get('mirror_required') is True and b.get('expected_horizontal_transform')=='hflip','mirror_applied':t.get('verification',{}).get('mirror_applied') is True and t.get('horizontal_mirror')=='hflip' and 'hflip' in command,'source_audio_intent_bound':b.get('source_audio_must_be_discarded') is True and b.get('production_narration_must_be_only_audio') is True,'source_audio_discarded':t.get('source_audio_discarded') is True,'bindings_match':b.get('source_canon_sha256')==canon and b.get('source_layout_sha256')==layout and b.get('source_audio_sha256')==tts['output_sha256']}
 elif args.stage=='final':
  out=Path(req['output_video_path']);receipt=ROOT/'log/final-render.json';intro=json.loads((ROOT/'log/intro-render.json').read_text());foot=json.loads((ROOT/'log/footage-render.json').read_text());layout=m['gate_events']['layout']['artifact_sha256'];poster=m['gate_events']['poster']['artifact_sha256'];expected=float(intro['duration_seconds'])+float(foot['duration_seconds'])
  checks={'server_completed_verified':True,'stream_copy_request':req.get('packaging_mode')=='stream_copy','stream_copy_terminal':t.get('packaging_mode')=='stream_copy','input_order_intro_then_footage':req.get('intro_video_path')==intro['artifact_path'] and req.get('footage_video_path')==foot['artifact_path'],'no_main_audio_path':'main_audio_path' not in req,'audio_source_footage':t.get('audio_source')=='footage_video_audio','poster_lineage_current':intro.get('source_poster_sha256')==poster,'layout_lineage_current':foot.get('source_layout_sha256')==layout,'bindings_match':b.get('source_canon_sha256')==canon and b.get('source_intro_sha256')==intro['artifact_sha256'] and b.get('source_footage_sha256')==foot['artifact_sha256']}
 elif args.stage=='transcode':
  out=Path(req['output_video_path']);receipt=ROOT/'log/transcode-upload.json';fin=json.loads((ROOT/'log/final-render.json').read_text());layout=m['gate_events']['layout']['artifact_sha256'];poster=m['gate_events']['poster']['artifact_sha256'];expected=float(fin['duration_seconds'])
  command=' '.join(map(str,t.get('command',[])))
  checks={'server_completed_verified':True,'gpu_nvenc_encoder':'h264_nvenc' in command,'source_final_binding':b.get('source_final_sha256')==fin['artifact_sha256'],'poster_lineage_current':fin.get('source_poster_sha256')==poster,'layout_lineage_current':fin.get('source_layout_sha256')==layout}
 if not out.is_file() or out.stat().st_size<=0:raise RuntimeError('output missing')
 if args.stage=='transcode':checks.update({'under_one_gb':out.stat().st_size<1_000_000_000,'target_near_800mb':650_000_000<=out.stat().st_size<1_000_000_000})
 pr=probe(out);ok,v,audio=streams_ok(pr);duration=float(pr['format']['duration']);checks.update({'single_streams':ok,'video_contract':v.get('pix_fmt')=='yuv420p' and v.get('field_order') in {None,'progressive'},'audio_contract':audio.get('profile')=='LC','duration_preserved':abs(duration-expected)<=0.1,'timestamps_start_zero':abs(float(v.get('start_time') or 0))<0.05 and abs(float(audio.get('start_time') or 0))<0.05})
 if args.stage=='footage':checks.update({'narration_single_audio':ok,'duration_exact':abs(duration-expected)<=0.1})
 if args.stage=='final':checks['duration_sum_exact']=abs(duration-expected)<=0.1
 if not all(checks.values()):raise RuntimeError('technical checks failed: '+','.join(k for k,v in checks.items() if not v))
 d={'schema_version':1,'project_id':PID,'run_id':RUN,'status':'completed','verified':True,'source_canon_sha256':canon,'job_id':t.get('id'),'artifact_path':str(out),'artifact_sha256':sha(out),'artifact_bytes':out.stat().st_size,'duration_seconds':duration,'technical_qa':{'status':'passed','verified':True,'checks':checks,'method':'endpoint terminal verification + independent ffprobe + streaming SHA-256; no frame extraction/viewing'},'content_qa':'not_run_by_policy','frame_qa':'prohibited_not_run','hash_method':'sha256_stream_8MiB','completed_at':now()}
 if args.stage=='footage':d.update({'source_audio_sha256':tts['output_sha256'],'source_layout_sha256':layout,'horizontal_mirror':'hflip','mirror_applied':True,'source_audio_discarded':True})
 if args.stage=='final':d.update({'source_intro_sha256':intro['artifact_sha256'],'source_footage_sha256':foot['artifact_sha256'],'source_poster_sha256':poster,'source_layout_sha256':layout,'intro_duration_seconds':intro['duration_seconds'],'footage_duration_seconds':foot['duration_seconds'],'packaging_mode':t.get('packaging_mode')})
 if args.stage=='transcode':d.update({'source_final_sha256':fin['artifact_sha256'],'source_poster_sha256':poster,'source_layout_sha256':layout,'target_output_bytes':req['target_output_bytes'],'max_output_bytes':req['max_output_bytes'],'under_one_gb':True})
 atomic(receipt,d);print(json.dumps({'stage':args.stage,'status':'completed','verified':True,'sha256':d['artifact_sha256'],'bytes':d['artifact_bytes'],'duration':duration}));
if __name__=='__main__':main()
