#!/usr/bin/env python3
from pathlib import Path
import datetime,hashlib,json,os
ROOT=Path('/data/video-pipeline/HaTramAudio/project/029-Chiec-O-De-Lai-O-Tram-Xe-Buyt-Cuoi');PID=ROOT.name;RUN='run-20260722T162951Z-87a939ad';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 load(rel):return json.loads((ROOT/rel).read_text())
def atomic(p,d):guard();t=p.with_name('.'+p.name+'.tmp');t.write_text(json.dumps(d,ensure_ascii=False,indent=2)+'\n');os.replace(t,p)
def art(path,expected):
 p=Path(path)
 if not p.is_file() or sha(p)!=expected:raise RuntimeError('artifact hash mismatch '+str(p))
 return {'path':str(p),'sha256':expected,'bytes':p.stat().st_size}
def main():
 guard();m=load('script/project-manifest.json');canon=m['canon_sha256'];events=m.get('gate_events',{});required=['story','tts','poster','layout','intro','footage','final_render','transcode','metadata']
 if any(events.get(x,{}).get('status')!='completed' or events.get(x,{}).get('verified') is not True for x in required):raise RuntimeError('gate events incomplete')
 promo=load('script/promotion-report.json');tts=load('log/tts.json');poster=load('log/poster.json');layout=load('log/layout.json');intro=load('log/intro-render.json');foot=load('log/footage-render.json');final=load('log/final-render.json');trans=load('log/transcode-upload.json');meta=load('log/metadata.json')
 receipts=[promo,tts,poster,layout,intro,foot,final,trans,meta]
 if any(x.get('verified') is not True or x.get('status') not in {'completed','passed'} for x in receipts):raise RuntimeError('upstream receipt not verified')
 if any((x.get('source_canon_sha256') or x.get('canon_sha256'))!=canon for x in receipts):raise RuntimeError('canon lineage mismatch')
 poster_h=events['poster']['artifact_sha256'];layout_h=events['layout']['artifact_sha256']
 if any(x.get('source_poster_sha256')!=poster_h for x in [intro,final,trans]):raise RuntimeError('poster lineage mismatch')
 if any(x.get('source_layout_sha256')!=layout_h for x in [foot,final,trans]):raise RuntimeError('layout lineage mismatch')
 if final.get('source_intro_sha256')!=intro.get('artifact_sha256') or final.get('source_footage_sha256')!=foot.get('artifact_sha256'):raise RuntimeError('final direct upstream mismatch')
 if trans.get('source_final_sha256')!=final.get('artifact_sha256'):raise RuntimeError('transcode direct upstream mismatch')
 artifacts={'canon':art(ROOT/'story/story-canon.txt',canon),'narration':art(ROOT/'story/spoken-narration.txt',promo['spoken_narration_sha256']),'tts':art(tts['artifact_path'],tts['artifact_sha256']),'poster':art(poster['artifact_path'],poster['artifact_sha256']),'layout':art(layout['artifact_path'],layout['artifact_sha256']),'intro':art(intro['artifact_path'],intro['artifact_sha256']),'footage':art(foot['artifact_path'],foot['artifact_sha256']),'final':art(final['artifact_path'],final['artifact_sha256']),'upload_copy':art(trans['artifact_path'],trans['artifact_sha256']),'metadata':art(meta['artifact_path'],meta['artifact_sha256'])}
 checks={'all_upstream_receipts_terminal_verified':True,'canon_bound_end_to_end':True,'all_artifact_hashes_match':True,'direct_upstream_bindings_match':True,'production_audio_duration_40_60_minutes':2400<=tts['duration_seconds']<=3600,'provider_raster_only':poster.get('provider_skill')=='tao-anh' and layout.get('provider_panel_sources') is not None,'text_postprocessing_absent':poster.get('text_postprocessed') is False and layout.get('typography_postprocess') is False,'human_poster_approval_not_required':True,'mirror_applied':foot.get('mirror_applied') is True,'source_audio_discarded':foot.get('source_audio_discarded') is True,'technical_media_qa_verified':all(x.get('technical_qa',{}).get('verified') is True for x in [foot,final,trans]),'footage_content_qa_not_run':foot.get('content_qa')=='not_run_by_policy','downstream_frame_qa_not_run':all(x.get('frame_qa')=='prohibited_not_run' for x in [foot,final,trans]),'upload_copy_under_one_gb':artifacts['upload_copy']['bytes']<1_000_000_000,'voice_test_not_required':True,'listening_test_not_required':True}
 if not all(checks.values()):raise RuntimeError('publish checks failed')
 d={'schema_version':1,'project_id':PID,'run_id':RUN,'status':'completed','verified':True,'readiness':'ready','source_canon_sha256':canon,'source_poster_sha256':poster_h,'source_layout_sha256':layout_h,'artifact_path':artifacts['upload_copy']['path'],'artifact_sha256':artifacts['upload_copy']['sha256'],'artifacts':artifacts,'checks':checks,'postiz_upload_allowed':True,'postiz_schedule_allowed_after_calendar_recheck':True,'content_qa':'not_run_by_policy','frame_qa':'prohibited_not_run','audited_at':now()};atomic(ROOT/'log/publish-ready.json',d);print(json.dumps({'status':'completed','verified':True,'readiness':'ready','checks':len(checks),'upload_bytes':artifacts['upload_copy']['bytes']}))
if __name__=='__main__':main()
