#!/usr/bin/env python3
from pathlib import Path
import datetime,fcntl,hashlib,json,os,urllib.parse,urllib.request

ROOT=Path('/data/video-pipeline/HaTramAudio/project/020-Chiec-Nhan-Chi-Deo-Khi-Co-Nguoi-Nhin').resolve()
RUN='run-20260721T124541Z-88551d8a'; OWNER='Levy'
CFG=Path.home()/'.config/ha-tram-audio/postiz.env'
TARGETS=(('final','output/final.mp4'),('footage','output/footage/footage.mp4'))

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 atomic(p,d):
 t=p.with_name('.'+p.name+'.tmp');t.write_text(json.dumps(d,ensure_ascii=False,indent=2)+'\n');os.replace(t,p)
def env():
 d={}
 for raw in CFG.read_text().splitlines():
  s=raw.strip()
  if s and not s.startswith('#') and '=' in s:
   k,v=s.split('=',1);d[k.strip()]=v.strip().strip("\"'")
 return d
def dt(v):
 x=datetime.datetime.fromisoformat(v.replace('Z','+00:00'))
 if x.tzinfo is None:x=x.replace(tzinfo=datetime.timezone.utc)
 return x.astimezone(datetime.timezone.utc).replace(microsecond=0)
def guard():
 lock=json.loads((ROOT/'.ownership-lock.json').read_text())
 if lock.get('run_id')!=RUN or lock.get('owner')!=OWNER:raise RuntimeError('ownership mismatch')
 lease=json.loads((ROOT/'script/gate-leases/cleanup.json').read_text());expiry=dt(lease['expires_at'])
 if lease.get('status')!='active' or lease.get('run_id')!=RUN or lease.get('owner')!=OWNER or expiry<=datetime.datetime.now(datetime.timezone.utc).replace(microsecond=0):raise RuntimeError('active cleanup lease required')
def safe_target(rel):
 p=ROOT/rel
 current=ROOT
 for part in Path(rel).parts:
  current=current/part
  if current.is_symlink():raise RuntimeError('symlink component prohibited: '+str(current))
 resolved=p.resolve(strict=False)
 if ROOT not in resolved.parents:raise RuntimeError('target escapes project: '+rel)
 return p
def live_queue(schedule):
 e=env();slot=dt(schedule['slot_utc']);expected={x['integration_id']:x['post_id'] for x in schedule['calendar_verification']}
 q=urllib.parse.urlencode({'startDate':(slot-datetime.timedelta(minutes=10)).isoformat().replace('+00:00','Z'),'endDate':(slot+datetime.timedelta(minutes=10)).isoformat().replace('+00:00','Z')})
 req=urllib.request.Request(e['POSTIZ_BASE_URL'].rstrip('/')+'/posts?'+q,headers={'Authorization':e['POSTIZ_API_KEY'],'Accept':'application/json'})
 with urllib.request.urlopen(req,timeout=60) as r:raw=json.load(r)
 posts=raw if isinstance(raw,list) else raw.get('posts') or raw.get('data') or []
 found={}
 for p in posts:
  integ=p.get('integration') or {};iid=integ.get('id') if isinstance(integ,dict) else p.get('integrationId');pid=p.get('id') or p.get('postId');when=p.get('publishDate') or p.get('date')
  if iid in expected and pid==expected[iid] and when and dt(when)==slot and str(p.get('state','')).upper()=='QUEUE':found[iid]=pid
 if found!=expected:raise RuntimeError('live calendar does not prove exact two QUEUE post IDs')
 return [{'integration_id':i,'post_id':p,'state':'QUEUE'} for i,p in found.items()]
def main():
 guard();lockfile=(ROOT/'script/gate-leases/.cleanup-execution.lock').open('a+')
 try:fcntl.flock(lockfile,fcntl.LOCK_EX|fcntl.LOCK_NB)
 except BlockingIOError:raise RuntimeError('another cleanup process is active')
 manifest=json.loads((ROOT/'script/project-manifest.json').read_text());audit=json.loads((ROOT/'log/completion-audit.json').read_text());upload=json.loads((ROOT/'log/upload.json').read_text());trans=json.loads((ROOT/'log/transcode-upload.json').read_text());schedule=json.loads((ROOT/'log/schedule.json').read_text())
 if manifest.get('status')!='completed' or manifest.get('verified') is not True or audit.get('phase')!='pre_cleanup' or audit.get('verified') is not True:raise RuntimeError('pre-cleanup authority invalid')
 retained=safe_target('output/final-upload.mp4')
 if not retained.is_file() or retained.stat().st_size>=1_000_000_000 or sha(retained)!=upload.get('artifact_sha256') or sha(retained)!=trans.get('artifact_sha256'):raise RuntimeError('retained upload mismatch')
 receipt_path=ROOT/'log/cleanup.json';old=json.loads(receipt_path.read_text()) if receipt_path.exists() else {};deleted=list(old.get('deleted',[])) if old.get('run_id')==RUN else [];done={x.get('relative_path') for x in deleted}
 locked=[]
 for key,rel in TARGETS:
  p=safe_target(rel);record=audit['artifacts'][key]
  if rel in done:
   if p.exists():raise RuntimeError('previously deleted target reappeared: '+rel)
   continue
  if not p.is_file() or p.stat().st_size!=record['bytes'] or sha(p)!=record['sha256']:raise RuntimeError('cleanup target drift: '+rel)
  locked.append({'path':str(p),'relative_path':rel,'bytes':record['bytes'],'sha256':record['sha256']})
 receipt={'schema_version':1,'project_id':ROOT.name,'gate':'cleanup','run_id':RUN,'status':'running','verified':False,'source_canon_sha256':manifest['canon_sha256'],'source_poster_sha256':manifest['gate_events']['poster']['artifact_sha256'],'source_layout_sha256':manifest['gate_events']['layout']['artifact_sha256'],'deleted':deleted,'retained':{'path':str(retained),'bytes':retained.stat().st_size,'sha256':sha(retained)},'started_at':old.get('started_at',now())};atomic(receipt_path,receipt)
 for target in locked:
  guard();calendar=live_queue(schedule);p=safe_target(target['relative_path'])
  if not p.is_file() or sha(p)!=target['sha256']:raise RuntimeError('target changed immediately before delete')
  p.unlink();target.update({'deleted_at':now(),'live_calendar':calendar});deleted.append(target);receipt['deleted']=deleted;atomic(receipt_path,receipt)
 if any(safe_target(rel).exists() for _,rel in TARGETS) or not retained.is_file() or sha(retained)!=receipt['retained']['sha256']:raise RuntimeError('cleanup postcondition failed')
 receipt.update({'status':'completed','verified':True,'completed_at':now(),'post_delete':{rel:True for _,rel in TARGETS}});atomic(receipt_path,receipt);print(json.dumps({'status':'completed','verified':True,'deleted':len(deleted),'retained_sha256':receipt['retained']['sha256']}))
if __name__=='__main__':main()
