#!/usr/bin/env python3
import base64, concurrent.futures, json, mimetypes, os, re, subprocess, time, uuid, urllib.request
from pathlib import Path

PROJECT=Path('/data/video-pipeline/Channel-Loi-Co-Nhan/project/13062026-024-Vo-Chong-Hon-Nhau-O-Chu-Nhuong')
IMG_URL='http://192.168.40.11:20128/v1/images/generations'
TTS_URL='http://192.168.1.201:3900/generate'
REF_AUDIO=Path('/data/video-pipeline/Youtube-Video-Maker/data/voices/template1/refaudio.mp3')
REF_TEXT=Path('/data/video-pipeline/Youtube-Video-Maker/data/voices/template1/reftext.txt')
BGM=Path('/data/video-pipeline/Channel-Loi-Co-Nhan/data/bgm/the_mountain-history-archive-145197/the_mountain-history-archive-145197.mp3')
LOGO=Path('/data/video-pipeline/Channel-Loi-Co-Nhan/data/logo/logoloiconhan.png')
WORKERS=5
TTS_WORKERS=1
TTS_SPEED='0.86'
SUB_FONT_SIZE=11
SCENE_PAUSE_SEC=0.5
INTRO_PAUSE_SEC=4.0
LOG=PROJECT/'logs/complete_short_project.log'
LOG.parent.mkdir(parents=True, exist_ok=True)

def log(s):
    line=time.strftime('%F %T ')+s
    print(line, flush=True)
    with LOG.open('a',encoding='utf-8') as f: f.write(line+'\n')

def load_env():
    p=Path('/home/hermes/.hermes/.env')
    if p.exists():
        for line in p.read_text().splitlines():
            if line.startswith('GEN_IMG_API_KEY=') and not os.environ.get('GEN_IMG_API_KEY'):
                os.environ['GEN_IMG_API_KEY']=line.split('=',1)[1].strip().strip('"').strip("'")

def run(cmd):
    log('RUN '+' '.join(map(str,cmd)))
    subprocess.check_call(list(map(str,cmd)))

def dur(p):
    out=subprocess.check_output(['ffprobe','-v','error','-show_entries','format=duration','-of','default=nw=1:nk=1',str(p)],text=True).strip()
    return float(out)

def gen_image(prompt,out):
    load_env(); key=os.environ['GEN_IMG_API_KEY']; out.parent.mkdir(parents=True,exist_ok=True)
    while not out.exists() or out.stat().st_size==0:
        try:
            body={'model':'cx/gpt-5.5-image','prompt':prompt,'size':'1024x1792','n':1}
            req=urllib.request.Request(IMG_URL,data=json.dumps(body).encode(),headers={'Authorization':'Bearer '+key,'Content-Type':'application/json'},method='POST')
            with urllib.request.urlopen(req,timeout=600) as r: payload=json.loads(r.read().decode('utf-8','ignore'))
            b=payload['data'][0].get('b64_json')
            if b.startswith('data:'): b=b.split(',',1)[1]
            tmp=out.with_suffix('.tmp'); tmp.write_bytes(base64.b64decode(b)); tmp.replace(out)
            log(f'DONE image {out}')
        except Exception as e:
            log(f'RETRY image {out.name}: {type(e).__name__} {e}'); time.sleep(30)

def multipart(fields, files):
    boundary='----OmniVoiceBoundary'+uuid.uuid4().hex; chunks=[]
    for k,v in fields.items(): chunks.append(f'--{boundary}\r\nContent-Disposition: form-data; name="{k}"\r\n\r\n{v}\r\n'.encode())
    for k,path in files.items():
        p=Path(path); mime=mimetypes.guess_type(str(p))[0] or 'application/octet-stream'
        chunks.append(f'--{boundary}\r\nContent-Disposition: form-data; name="{k}"; filename="{p.name}"\r\nContent-Type: {mime}\r\n\r\n'.encode()+p.read_bytes()+b'\r\n')
    chunks.append(f'--{boundary}--\r\n'.encode()); return boundary,b''.join(chunks)

def tts_one(item):
    out=Path(item['audio_path']); out.parent.mkdir(parents=True,exist_ok=True)
    if out.exists() and out.stat().st_size>1000:
        log(f'SKIP existing tts {out}')
        return
    ref_text=REF_TEXT.read_text(encoding='utf-8') if REF_TEXT.exists() else ''
    text=re.sub(r'\s*[:;\-–—]\s*', ', ', item['text']).strip()
    attempt=0
    while not out.exists() or out.stat().st_size<1000:
        attempt+=1
        try:
            fields={'text':text,'language':'vi','ref_text':ref_text,'speed':TTS_SPEED,'num_step':'24','guidance_scale':'2.0','effect_preset':'broadcast'}
            boundary,body=multipart(fields, {'ref_audio':REF_AUDIO})
            req=urllib.request.Request(TTS_URL,data=body,headers={'Content-Type':'multipart/form-data; boundary='+boundary},method='POST')
            with urllib.request.urlopen(req,timeout=180) as r: blob=r.read()
            tmp=out.with_suffix('.tmp'); tmp.write_bytes(blob)
            if not (blob[:4]==b'RIFF' and b'WAVE' in blob[:12]): raise RuntimeError(blob[:80])
            tmp.replace(out)
            log(f'DONE tts {out}')
        except Exception as e:
            log(f'RETRY tts {item["id"]} attempt {attempt}: {type(e).__name__} {e}'); time.sleep(min(30*attempt,180))

def srt_time(sec):
    ms=int(round(sec*1000)); h=ms//3600000; ms%=3600000; m=ms//60000; ms%=60000; s=ms//1000; ms%=1000
    return f'{h:02}:{m:02}:{s:02},{ms:03}'

def main():
    LOG.write_text('',encoding='utf-8')
    data=json.loads((PROJECT/'script/scenes.json').read_text(encoding='utf-8'))
    # Hard rule: generate scene images with WORKERS=5; never run this step sequentially.
    image_jobs=[]
    for sc in data['scenes']:
        out=PROJECT/'images/scenes'/(sc['scene_id']+'.png')
        image_jobs.append((sc.get('visual_prompt') or sc.get('visual_beat') or sc['narration'], out))
    with concurrent.futures.ThreadPoolExecutor(max_workers=WORKERS) as ex:
        list(ex.map(lambda job: gen_image(*job), image_jobs))
    thumb=PROJECT/'output/thumbnails/thumb_9x16_final.png'
    title=data.get('title') or data.get('metadata',{}).get('title','Cổ nhân dạy: Người khôn nói ít, kẻ dại nói nhiều')
    thumb_prompt=f"Vertical 9:16 thumbnail, cổ phong giấy vàng, aged yellow parchment, ink-wash cinematic Vietnamese moral story poster. Large readable Vietnamese brush title: {title}. Symbolic overflowing tea cup and calm ancient scholar, sepia bronze gold accents, mobile-safe composition, dramatic but dignified, no modern neon, no horizontal layout."
    gen_image(thumb_prompt, thumb)
    # TTS items: keep existing good segments on rerun; tts_one skips files >1000 bytes.
    items=[]
    for sc in data['scenes']:
        sid=sc['scene_id']; text=sc['narration']
        items.append({'id':sid,'scene_id':sid,'text':text,'audio_path':str(PROJECT/'audio/voice_segments'/sid/'seg_001.wav')})
    (PROJECT/'audio/voice_segments').mkdir(parents=True,exist_ok=True)
    (PROJECT/'audio/voice_segments/voice_segments_manifest.json').write_text(json.dumps({'items':items},ensure_ascii=False,indent=2),encoding='utf-8')
    with concurrent.futures.ThreadPoolExecutor(max_workers=TTS_WORKERS) as ex: list(ex.map(tts_one, items))
    # concatenate audio and make timeline/srt with short subtitle cues only.
    concat=PROJECT/'audio/concat.txt'; lines=[]; timeline=[]; t=0.0; srt=[]; cue_debug=[]; cue_idx=1
    silence=PROJECT/'audio/silence_scene_pause.wav'
    run(['ffmpeg','-y','-v','error','-f','lavfi','-i',f'anullsrc=r=44100:cl=mono','-t',SCENE_PAUSE_SEC,'-c:a','pcm_s16le',silence])
    def word_chunks(text, n=5):
        words=text.replace('\n',' ').split(); out=[]; i=0
        while i < len(words):
            remain=len(words)-i; take=n if remain>n else remain
            if remain==n+1: take=3
            out.append(' '.join(words[i:i+take])); i+=take
        return out
    for idx,it in enumerate(items,1):
        ap=Path(it['audio_path'])
        d=dur(ap); visual_d=d+SCENE_PAUSE_SEC
        lines.append(f"file '{ap}'\n")
        lines.append(f"file '{silence}'\n")
        timeline.append({'scene_id':it['scene_id'],'start':t,'end':t+visual_d,'speech_end':t+d,'duration':visual_d,'audio_duration':d,'pause_sec':SCENE_PAUSE_SEC,'audio_path':str(ap),'image_path':str(PROJECT/'images/scenes'/f"{it['scene_id']}.png")})
        chunks=word_chunks(it['text'],5)
        if chunks:
            step=d/len(chunks)
            for ci,txt in enumerate(chunks):
                cs=t+ci*step; ce=t+(ci+1)*step
                srt.append(f"{cue_idx}\n{srt_time(cs)} --> {srt_time(ce)}\n{txt}\n")
                cue_debug.append({'idx':cue_idx,'scene_id':it['scene_id'],'start':cs,'end':ce,'text':txt})
                cue_idx+=1
        t+=visual_d
    concat.write_text(''.join(lines),encoding='utf-8')
    master=PROJECT/'audio/master_narration.wav'; master.parent.mkdir(exist_ok=True)
    run(['ffmpeg','-y','-v','error','-f','concat','-safe','0','-i',concat,'-ar','44100','-ac','1','-c:a','pcm_s16le',master])
    (PROJECT/'subtitles').mkdir(parents=True, exist_ok=True)
    (PROJECT/'audio/master_narration_timeline.json').write_text(json.dumps({'duration_sec': dur(master), 'items': timeline}, ensure_ascii=False, indent=2), encoding='utf-8')
    (PROJECT/'subtitles/subtitle_cue_debug_short.json').write_text(json.dumps(cue_debug,ensure_ascii=False,indent=2),encoding='utf-8')
    (PROJECT/'subtitles/master_subtitles_clamped.srt').write_text('\n'.join(srt), encoding='utf-8')
    intro_text = (data.get('metadata', {}).get('topic_sentence') or title).strip()
    intro_audio = PROJECT / 'output/intro/intro_audio.wav'
    intro_audio.parent.mkdir(parents=True, exist_ok=True)
    intro_audio.unlink(missing_ok=True)
    tts_one({'id': 'intro', 'text': intro_text, 'audio_path': str(intro_audio)})
    log('STOP after audio/subtitle rebuild; production render/package must use worker-only script.')
    return
    # render main one-pass
    seglist=PROJECT/'render_onepass/segments.txt'; seglist.parent.mkdir(exist_ok=True)
    segtxt=[]
    for it in timeline:
        segtxt.append(f"file '{PROJECT/'images/scenes'/(it['scene_id']+'.png')}'\n")
        segtxt.append(f"duration {it['duration']:.3f}\n")
    segtxt.append(f"file '{PROJECT/'images/scenes'/(timeline[-1]['scene_id']+'.png')}'\n")
    seglist.write_text(''.join(segtxt),encoding='utf-8')
    mainv=PROJECT/'output/main_story.mp4'; mainv.parent.mkdir(parents=True,exist_ok=True)
    # Use filter_complex for the watermark; `-vf` cannot safely mix movie labels with the implicit input label.
    fc=(
        f"[0:v]scale=1080:1920:force_original_aspect_ratio=increase,"
        f"crop=1080:1920,subtitles='{sub}':force_style='FontSize={SUB_FONT_SIZE},Outline=1,Alignment=2,MarginV=80'[base];"
        f"movie='{LOGO}',scale=160:-1,format=rgba,colorchannelmixer=aa=0.55[wm];"
        f"[base][wm]overlay=W-w-35:35[v]"
    )
    run(['ffmpeg','-y','-v','error','-f','concat','-safe','0','-i',seglist,'-i',master,'-filter_complex',fc,'-map','[v]','-map','1:a','-r','30','-c:v','libx264','-pix_fmt','yuv420p','-c:a','aac','-shortest',mainv])
    # intro one thumbnail frame + topic sentence audio
    intro_text=(data.get('metadata',{}).get('topic_sentence') or 'Cổ nhân dạy: Người khôn nói ít, kẻ dại nói nhiều.'); intro_audio=PROJECT/'output/intro/intro_audio.wav'; intro_audio.parent.mkdir(parents=True,exist_ok=True)
    tts_one({'id':'intro','text':intro_text,'audio_path':str(intro_audio)})
    intro=PROJECT/'output/intro/intro.mp4'
    run(['ffmpeg','-y','-v','error','-loop','1','-i',thumb,'-i',intro_audio,'-vf','scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920','-r','30','-c:v','libx264','-pix_fmt','yuv420p','-c:a','aac','-shortest',intro])
    pause=PROJECT/'output/intro/intro_pause_4s.mp4'
    run(['ffmpeg','-y','-v','error','-loop','1','-i',thumb,'-f','lavfi','-i','anullsrc=r=44100:cl=stereo','-t',INTRO_PAUSE_SEC,'-vf','scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920','-r','30','-c:v','libx264','-pix_fmt','yuv420p','-c:a','aac','-shortest',pause])
    # final concat intro + 4s pause + main and bgm
    listp=PROJECT/'output/final/final_concat.txt'; listp.parent.mkdir(parents=True,exist_ok=True)
    listp.write_text(f"file '{intro}'\nfile '{pause}'\nfile '{mainv}'\n",encoding='utf-8')
    tmp=PROJECT/'output/final/final_no_bgm.mp4'; final=PROJECT/'output/final/final_video.mp4'
    concat_fc='[0:v]scale=1080:1920,setsar=1[v0];[0:a]aformat=sample_rates=44100:channel_layouts=stereo[a0];[1:v]scale=1080:1920,setsar=1[v1];[1:a]aformat=sample_rates=44100:channel_layouts=stereo[a1];[2:v]scale=1080:1920,setsar=1[v2];[2:a]aformat=sample_rates=44100:channel_layouts=stereo[a2];[v0][a0][v1][a1][v2][a2]concat=n=3:v=1:a=1[v][voice]'
    run(['ffmpeg','-y','-v','error','-i',intro,'-i',pause,'-i',mainv,'-filter_complex',concat_fc,'-map','[v]','-map','[voice]','-r','30','-c:v','libx264','-pix_fmt','yuv420p','-c:a','aac',tmp])
    mix_fc='[0:a]aformat=sample_fmts=fltp:sample_rates=44100:channel_layouts=stereo[voice];[1:a]aformat=sample_fmts=fltp:sample_rates=44100:channel_layouts=stereo,volume=0.18[bgm];[voice][bgm]amix=inputs=2:duration=first:dropout_transition=2[a]'
    run(['ffmpeg','-y','-v','error','-i',tmp,'-stream_loop','-1','-i',BGM,'-filter_complex',mix_fc,'-map','0:v','-map','[a]','-c:v','copy','-c:a','aac','-shortest',final])
    info={'project_id':PROJECT.name,'status':'completed','thumbnail':str(thumb),'main_story':str(mainv),'final_video':str(final),'duration_sec':dur(final),'scenes':len(data['scenes']),'format':'9:16','tts_speed':TTS_SPEED,'subtitle_font_size':SUB_FONT_SIZE,'scene_pause_sec':SCENE_PAUSE_SEC,'intro_pause_sec':INTRO_PAUSE_SEC}
    (PROJECT/'output/info.json').write_text(json.dumps(info,ensure_ascii=False,indent=2),encoding='utf-8')
    log('COMPLETE '+str(final))
if __name__=='__main__': main()
