#!/usr/bin/env python3
from pathlib import Path
import datetime,hashlib,json,os,shutil,subprocess
ROOT=Path('/data/video-pipeline/HaTramAudio/project/019-Buc-Tuong-Giua-Hai-Ban-Cong');PID=ROOT.name;RUN='run-20260721T065616Z-a7dbad2f';OWNER='Levy'
WORK=(ROOT/'work/artwork'/RUN).resolve()
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 atomic_json(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 main():
 guard();manifest=json.loads((ROOT/'script/project-manifest.json').read_text());story=manifest.get('gate_events',{}).get('story',{});canon=manifest.get('canon_sha256')
 if manifest.get('steps',{}).get('story')!='completed' or story.get('verified') is not True or story.get('canon_sha256')!=canon:raise RuntimeError('Story Gate is not committed/current')
 qa=json.loads((ROOT/'log/artwork-qa.json').read_text());prompts=json.loads((ROOT/'script/image-prompts.json').read_text());promo=json.loads((ROOT/'script/promotion-report.json').read_text())
 if qa.get('status')!='passed' or qa.get('verified') is not True or qa.get('source_canon_sha256')!=canon or prompts.get('source_canon_sha256')!=canon:raise RuntimeError('artwork QA/authority mismatch')
 if promo.get('verified') is not True or promo.get('canon_sha256')!=canon:raise RuntimeError('promotion authority mismatch')
 mapping={'intro_poster':'intro-poster-provider.png','right_panel':'right-panel-provider.png','left_panel':'left-panel-provider.png'};provider=ROOT/'image/provider';provider.mkdir(parents=True,exist_ok=True)
 for role,name in mapping.items():
  item=qa.get('items',{}).get(role,{})
  src=Path(item.get('artifact_path') or item.get('source_path','')).resolve();dst=provider/name
  if WORK not in src.parents or not src.is_file():raise RuntimeError('provider source outside immutable artwork namespace '+role)
  if item.get('provider_skill')!='tao-anh' or item.get('text_postprocessed') is not False:raise RuntimeError('provider provenance invalid '+role)
  if item.get('prompt_sha256')!=prompts.get('items',{}).get(role,{}).get('prompt_sha256'):raise RuntimeError('provider prompt binding mismatch '+role)
  if sha(src)!=item.get('artifact_sha256'):raise RuntimeError('provider source hash drift '+role)
  tmp=dst.with_name('.'+dst.name+'.tmp');shutil.copyfile(src,tmp);guard();os.replace(tmp,dst)
  if sha(dst)!=qa['items'][role]['artifact_sha256']:raise RuntimeError('provider byte promotion mismatch '+role)
 subprocess.check_call(['python3',str(ROOT/'work/build_layout.py')])
 lm=json.loads((ROOT/'image/layout-manifest.json').read_text());stamp=now();poster_item=lm['items']['intro_poster'];poster={'schema_version':1,'project_id':PID,'run_id':RUN,'status':'completed','verified':True,'source_canon_sha256':canon,'provider_skill':'tao-anh','provider_model':'cx/gpt-5.5-image','text_postprocessed':False,'prompt_sha256':prompts['items']['intro_poster']['prompt_sha256'],'output':poster_item['target_path'],'artifact_path':poster_item['target_path'],'artifact_sha256':poster_item['target_sha256'],'source_intro_poster_path':poster_item['source_path'],'source_intro_poster_sha256':poster_item['source_sha256'],'operation':'full_frame_resample_only','crop':False,'pad':False,'typography_postprocess':False,'completed_at':stamp}
 sources=[]
 for role in ['left_panel','right_panel']:
  x=lm['items'][role];sources.append({'role':role,'path':x['source_path'],'sha256':x['source_sha256'],'provider_skill':'tao-anh','text_postprocessed':False})
 layout={'schema_version':1,'project_id':PID,'run_id':RUN,'status':'completed','verified':True,'source_canon_sha256':canon,'output':lm['layout_path'],'artifact_path':lm['layout_path'],'artifact_sha256':lm['layout_sha256'],'layout_path':lm['layout_path'],'layout_sha256':lm['layout_sha256'],'layout_dimensions':lm['layout_dimensions'],'coordinates':lm['coordinates'],'center_fill':lm['center_fill'],'composition':'deterministic raster placement; no text layer','typography_postprocess':False,'content_qa':'not_run_by_policy','provider_panel_sources':sources,'completed_at':stamp}
 atomic_json(ROOT/'log/poster.json',poster);atomic_json(ROOT/'log/layout.json',layout);print(json.dumps({'status':'completed','verified':True,'poster_sha256':poster['artifact_sha256'],'layout_sha256':layout['artifact_sha256']},ensure_ascii=False))
if __name__=='__main__':main()
