#!/usr/bin/env python3
"""Reproduce a labelled static-illustration edit. Does not call any model/API."""
from pathlib import Path
import argparse, datetime, hashlib, json, os, platform, re, shutil, subprocess, tempfile

def sha(path):
    return hashlib.sha256(path.read_bytes()).hexdigest()

def execute(command, root):
    result = subprocess.run(command, capture_output=True, text=True, check=False)
    if result.returncode:
        error = result.stderr.replace(str(root), '<PACKAGE_ROOT>')
        raise RuntimeError('FFmpeg failed: ' + error[-3000:])
    return result

def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--ffmpeg', default=os.environ.get('FFMPEG') or shutil.which('ffmpeg'))
    parser.add_argument('--language', choices=['en', 'zh'], default='en')
    parser.add_argument('--output-dir', default='verified-demo')
    parser.add_argument('--overwrite', action='store_true')
    args = parser.parse_args()
    if not args.ffmpeg:
        parser.error('FFmpeg is required. Pass --ffmpeg /path/to/ffmpeg or put it on PATH.')
    root = Path(__file__).resolve().parent
    output = Path(args.output_dir)
    if not output.is_absolute(): output = root / output
    output.mkdir(parents=True, exist_ok=True)
    final = output / 'assembled-demo.mp4'
    if final.exists() and not args.overwrite:
        parser.error('Output already exists; choose another --output-dir or explicitly pass --overwrite.')
    manifest = json.loads((root / 'manifest.json').read_text())
    shot_names = [shot['sourcePng'] for shot in manifest['shots']]
    inputs = shot_names + [f'end-card.{args.language}.png']
    overlay = root / 'demo-disclosure-overlay.png'
    for name in inputs + [overlay.name]:
        if not (root / name).is_file():
            parser.error(f'Missing {name}; use the supplied PNGs or run render-assets.mjs after editing SVGs.')
    start = datetime.datetime.now(datetime.timezone.utc).isoformat()
    version = execute([args.ffmpeg, '-hide_banner', '-version'], root).stdout.splitlines()[0]
    commands = []
    with tempfile.TemporaryDirectory(prefix='assembly-', dir=output) as temporary:
        temporary = Path(temporary)
        clips = []
        for index, name in enumerate(inputs):
            clip = temporary / f'{index+1:02d}.mp4'
            command = [args.ffmpeg, '-hide_banner', '-nostdin', '-y', '-loop', '1', '-framerate', '24', '-i', str(root/name), '-loop', '1', '-framerate', '24', '-i', str(overlay), '-filter_complex', '[0:v][1:v]overlay=0:0:format=auto,format=yuv420p[v]', '-map', '[v]', '-an', '-t', '3', '-r', '24', '-c:v', 'libx264', '-threads', '1', '-preset', 'veryfast', '-crf', '23', '-movflags', '+faststart', str(clip)]
            execute(command, root)
            commands.append({'step':'encode-static-shot','source':name,'seconds':3,'codec':'libx264','fps':24,'audio':False})
            clips.append(clip)
        concat = temporary / 'concat.txt'
        concat.write_text(''.join("file '" + str(c).replace("'", "'\\''") + "'\n" for c in clips))
        execute([args.ffmpeg, '-hide_banner', '-nostdin', '-y', '-f', 'concat', '-safe', '0', '-i', str(concat), '-c', 'copy', '-an', '-movflags', '+faststart', str(final)], root)
        commands.append({'step':'concat','segments':4,'source':'three still-product shots and one editable end card','copyCodec':True})
    probe = execute([args.ffmpeg, '-hide_banner', '-nostdin', '-i', str(final), '-map', '0:v:0', '-f', 'null', '-'], root)
    report = probe.stderr.replace(str(root), '<PACKAGE_ROOT>')
    frames = re.findall(r'frame=\s*(\d+)', report)
    frame_count = int(frames[-1]) if frames else None
    duration_match = re.search(r'Duration:\s*(\d+):(\d+):(\d+(?:\.\d+)?)', report)
    duration = (int(duration_match[1])*3600+int(duration_match[2])*60+float(duration_match[3])) if duration_match else None
    checks = {
      'fullDecodePassed':probe.returncode==0,
      'expectedFrameCount':frame_count==288,
      'expectedDuration':duration is not None and abs(duration-12)<0.05,
      'expectedResolution':'1280x720' in report,
      'expectedFrameRate':bool(re.search(r'\b24 fps\b',report)),
      'h264Video':'Video: h264' in report,
      'noAudioStream':'Audio:' not in report,
      'persistentDisclosureWasComposited':True,
      'modelGenerationWasNotCalled':True,
      'sourceFramesAreOriginalIllustrations':manifest['demonstration']['notPhotographs'] and manifest['demonstration']['notModelGeneratedVideo']
    }
    if not all(checks.values()):
        raise RuntimeError('Export validation failed: '+json.dumps(checks))
    keyframes=[]
    for label,time in [('01-overview',1.5),('02-detail',4.5),('03-context',7.5),('04-end-card',10.5)]:
        image=output/(label+'.png')
        execute([args.ffmpeg,'-hide_banner','-nostdin','-y','-ss',str(time),'-i',str(final),'-frames:v','1','-update','1',str(image)],root)
        keyframes.append(image)
    (output/'stream-and-decode-report.txt').write_text(report)
    input_names=sorted(set(inputs+[overlay.name,'manifest.json','shot-list.csv','export-checklist.csv','assemble-demo.py','render-assets.mjs']+[f.name for f in root.glob('*.svg')]))
    inputs_receipt=[{'path':name,'sha256':sha(root/name),'bytes':(root/name).stat().st_size} for name in input_names]
    outputs_receipt=[{'path':f.name,'sha256':sha(f),'bytes':f.stat().st_size} for f in [final,*keyframes]]
    receipt={
      'schemaVersion':1,'blueprintId':'blueprint-38','executionType':'local-post-production-fixture',
      'startedAtUtc':start,'finishedAtUtc':datetime.datetime.now(datetime.timezone.utc).isoformat(),
      'runtime':{'python':platform.python_version(),'platform':platform.system(),'architecture':platform.machine(),'ffmpeg':version},
      'modelGeneration':{'stage':'not-executed','productionTaskRun':False,'paidGenerationCalls':0,'modelOutputEvidence':False},
      'visualSource':'self-authored fictional product SVGs rasterized to PNG; static cuts, no simulated model output',
      'language':args.language,'video':{'width':1280,'height':720,'fps':24,'durationSeconds':duration,'decodedFrames':frame_count,'codec':'H.264','audioStreams':0},
      'inputs':inputs_receipt,'outputs':outputs_receipt,'executedSteps':commands,'checks':checks,
      'limits':['This verifies local assembly and decoding, not model quality.','Inputs depict invented practice products, not real products.','Use real authorized assets and approved wording before an actual product release.']
    }
    (output/'execution-receipt.json').write_text(json.dumps(receipt,ensure_ascii=False,indent=2)+'\n')
    print(json.dumps({'output':str(output).replace(str(root),'<PACKAGE_ROOT>'),'checks':checks,'video':receipt['video']},ensure_ascii=False))

if __name__=='__main__':
    main()
