#!/usr/bin/env python3
from __future__ import annotations
import datetime as dt
import hashlib,json,os,time,urllib.request
from pathlib import Path
ROOT=Path('/data/video-pipeline/HaTramAudio/project/016-Nguoi-Giu-Am-Thanh-Cuoi-Cung');RUN='run-20260720T233725Z-24504f17';CANON='fae6fce617a14dc4f8b3bd52968cc688a456c45a60bc2ed7701ccd8164875e02'
BASE='http://192.168.1.104:8022';AUDIO=ROOT/'audio/story-full.wav';LAYOUT=ROOT/'image/normalized/layout-1920x1080.png';OUTPUT=ROOT/'output/footage/footage.mp4';INTENT=ROOT/'log/footage-intent.json';RECEIPT=ROOT/'log/footage-render.json'
def now():return dt.datetime.now(dt.timezone.utc).isoformat().replace('+00:00','Z')
def sha(p):
 h=hashlib.sha256()
 with p.open('rb') as f:
  while b:=f.read(8*1024*1024):h.update(b)
 return h.hexdigest()
def atomic(p,o):p.parent.mkdir(parents=True,exist_ok=True);t=p.with_suffix(p.suffix+'.tmp');t.write_text(json.dumps(o,ensure_ascii=False,indent=2)+'\n');os.replace(t,p)
def get(path):
 with urllib.request.urlopen(BASE+path,timeout=30) as r:return json.load(r)
def post(path,obj):
 req=urllib.request.Request(BASE+path,data=json.dumps(obj).encode(),headers={'Content-Type':'application/json'},method='POST')
 with urllib.request.urlopen(req,timeout=60) as r:return json.load(r)
def guard():
 l=json.loads((ROOT/'.ownership-lock.json').read_text());assert (l['project_id'],l['run_id'],l['owner'],l['status'])==(ROOT.name,RUN,'zoro','main_session_pipeline')
 assert sha(ROOT/'story/story-canon.txt')==CANON
 t=json.loads((ROOT/'script/tts-manifest.json').read_text());assert t['status']=='completed' and t['verified'] and t['output_sha256']==sha(AUDIO)
 lm=json.loads((ROOT/'image/layout-manifest.json').read_text());assert lm['status']=='completed' and lm['verified'] and lm['layout']['sha256']==sha(LAYOUT)
def main():
 guard();OUTPUT.parent.mkdir(parents=True,exist_ok=True)
 payload={'layout':str(LAYOUT),'footage_dir':'/data/video-pipeline/GacMaiAudio/footage','pattern':'*.mp4','recursive':True,'audio':str(AUDIO),'output':str(OUTPUT),'seed':0,'overwrite':False,'width':1920,'height':1080,'fps':30,'center_x':656,'center_y':0,'center_width':608,'center_height':1080,'video_encoder':'h264_nvenc'}
 request_sha=hashlib.sha256(json.dumps(payload,sort_keys=True,separators=(',',':')).encode()).hexdigest();job_id=None
 if RECEIPT.exists():
  old=json.loads(RECEIPT.read_text());
  if old.get('status')=='completed' and old.get('verified') and OUTPUT.exists() and old.get('output_sha256')==sha(OUTPUT):print(json.dumps({'status':'completed_reused','job_id':old['job_id']}));return
  job_id=old.get('job_id');assert old.get('request_sha256')==request_sha
 if not job_id:
  if OUTPUT.exists():raise RuntimeError('unreceipted footage output exists')
  if INTENT.exists():
   intent=json.loads(INTENT.read_text());
   if intent.get('status')=='submitting':raise RuntimeError('ambiguous prior submit intent; fail closed')
  start=time.time()
  while True:
   h=get('/health')
   if h.get('status')=='ok' and h.get('h264_nvenc') is True and not h.get('running'):break
   if time.time()-start>14400:raise RuntimeError('worker remained busy for four hours; no job submitted')
   time.sleep(15)
  intent={'schema_version':1,'project_id':ROOT.name,'run_id':RUN,'status':'submitting','verified':False,'request_sha256':request_sha,'payload':payload,'source_canon_sha256':CANON,'source_audio_sha256':sha(AUDIO),'source_layout_sha256':sha(LAYOUT),'created_at':now()};atomic(INTENT,intent)
  response=post('/v1/footage-render-huyenan/jobs',payload);job_id=response.get('job_id') or response.get('id');assert job_id
  receipt={**intent,'status':'submitted','job_id':job_id,'submitted_response':response,'submitted_at':now()};atomic(RECEIPT,receipt);intent.update(status='submitted',job_id=job_id,updated_at=now());atomic(INTENT,intent)
 while True:
  guard();j=get('/v1/footage-render-huyenan/jobs/'+job_id);status=j.get('status')
  receipt=json.loads(RECEIPT.read_text());receipt.update(last_response=j,status=status or receipt['status'],updated_at=now());atomic(RECEIPT,receipt)
  if status in ('failed','cancelled'):raise RuntimeError(f'footage terminal failure: {status}')
  if status=='completed':
   v=j.get('verification') or {};assert v.get('verified') is True;assert OUTPUT.exists() and OUTPUT.stat().st_size>0
   assert v.get('mirror_applied') is True or v.get('horizontal_mirror')=='hflip'
   assert v.get('source_audio_discarded') is True
   receipt.update(status='completed',verified=True,technical_verified=True,source_canon_sha256=CANON,source_audio_sha256=sha(AUDIO),source_layout_sha256=sha(LAYOUT),output_path=str(OUTPUT),output_sha256=sha(OUTPUT),output_bytes=OUTPUT.stat().st_size,completed_at=now());atomic(RECEIPT,receipt)
   print(json.dumps({'status':'completed','job_id':job_id,'output_sha256':receipt['output_sha256'],'bytes':receipt['output_bytes']}));return
  time.sleep(5)
if __name__=='__main__':main()
