#!/usr/bin/env python3
import json
import re
import time
import urllib.error
import urllib.request
from pathlib import Path

PROJECT = Path('/data/video-pipeline/Channel-Loi-Co-Nhan/project/11062026-001-Biet-Du-Thi-Khong-Nhuc')
RENDER_ENDPOINT = 'http://192.168.1.104:8019/render-story-video'
PROBE_ENDPOINT = 'http://192.168.1.104:8002/probe'
PACKAGE_ENDPOINT = 'http://192.168.1.104:8018/package-audio-safe'
PACKAGE_PROBE_ENDPOINT = 'http://192.168.1.104:8018/probe'
BLACKDETECT_ENDPOINT = 'http://192.168.1.104:8018/blackdetect'
BGM = Path('/data/video-pipeline/Channel-Loi-Co-Nhan/data/bgm/paulyudin-sentimental-story-182512.mp3')
INTRO = PROJECT / 'output/intro/intro.mp4'
INTRO_AUDIO = PROJECT / 'output/intro/intro_audio.wav'
THUMB = PROJECT / 'output/thumbnails/thumb_9x16_final.png'
MAIN = PROJECT / 'output/main_story.mp4'
FINAL = PROJECT / 'output/final/final_video.mp4'
LOG = PROJECT / 'logs/worker_step10_18_rebuild.log'

SUB_WORDS_PER_CUE = 5
BGM_VOLUME = 0.22
INTRO_TAIL_SILENCE_SEC = 2.0


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


def post_json(url, payload, timeout=7200):
    req = urllib.request.Request(
        url,
        data=json.dumps(payload, ensure_ascii=False).encode('utf-8'),
        headers={'Content-Type': 'application/json'},
        method='POST',
    )
    try:
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            return json.loads(resp.read().decode('utf-8'))
    except urllib.error.HTTPError as e:
        body = e.read().decode('utf-8', 'ignore')
        raise RuntimeError(f'{url} HTTP {e.code}: {body}') from e


def ts(sec):
    ms = int(round(max(0.0, 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 clean_words(text):
    return ' '.join(text.replace('\n', ' ').split()).split()


def chunks(words, n=SUB_WORDS_PER_CUE):
    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(words[i:i + take])
        i += take
    return out


def asr_words():
    asr = json.loads((PROJECT / 'asr/master_narration_asr_words.json').read_text(encoding='utf-8'))
    words = []
    for seg in asr.get('segments', []):
        for w in seg.get('words', []):
            if w.get('start') is not None and w.get('end') is not None:
                words.append(w)
    return words


def build_asr_timed_subtitles():
    scenes = json.loads((PROJECT / 'script/scenes.json').read_text(encoding='utf-8'))['scenes']
    aw = asr_words()
    sub_path = PROJECT / 'subtitles/master_subtitles_worker_asr_split.srt'
    debug_path = PROJECT / 'subtitles/subtitle_debug_worker_asr_split.json'
    sub_path.parent.mkdir(parents=True, exist_ok=True)
    blocks = []
    debug = []
    cursor = 0
    idx = 1
    for scene in scenes:
        words = clean_words(scene['narration'])
        for ch in chunks(words):
            start_i = cursor
            end_i = min(cursor + len(ch) - 1, len(aw) - 1)
            if start_i >= len(aw):
                break
            start = float(aw[start_i]['start'])
            end = float(aw[end_i]['end'])
            text = ' '.join(ch)
            blocks.append(f'{idx}\n{ts(start)} --> {ts(end)}\n{text}\n')
            debug.append({'idx': idx, 'scene_id': scene['scene_id'], 'start': start, 'end': end, 'text': text, 'asr_start_word': aw[start_i].get('word'), 'asr_end_word': aw[end_i].get('word')})
            idx += 1
            cursor += len(ch)
    sub_path.write_text('\n'.join(blocks), encoding='utf-8')
    debug_path.write_text(json.dumps(debug, ensure_ascii=False, indent=2), encoding='utf-8')
    return sub_path, debug_path


def duration_from_probe(probe):
    return float(probe.get('format', {}).get('duration') or 0)


def has_stream(probe, typ):
    return any(s.get('codec_type') == typ for s in probe.get('streams', []))


def render_main_worker(sub_path):
    timeline = json.loads((PROJECT / 'audio/master_narration_timeline.json').read_text(encoding='utf-8'))['items']
    master_probe = post_json(PROBE_ENDPOINT, {'input_path': str(PROJECT / 'audio/master_narration.wav')}, timeout=120)
    master_duration = duration_from_probe(master_probe)
    scenes = []
    for i, item in enumerate(timeline):
        image_path = item.get('image_path') or str(PROJECT / 'images/scenes' / f"{item['scene_id']}.png")
        start = float(item.get('start', 0.0))
        next_start = float(timeline[i + 1].get('start', master_duration)) if i + 1 < len(timeline) else master_duration
        # Hold each scene until the next scene starts, and hold the last scene to the true master-audio end.
        # Using only item['duration'] drops tail pauses, making images advance before voice/subtitles finish.
        hold = max(float(item.get('duration', 0.5)), next_start - start)
        # 8019 storybook_smooth uses 0.5s crossfade, so each outgoing image overlaps the next.
        # Add that overlap back to every non-final scene so visual scene changes still align to voice/sub timing.
        if i + 1 < len(timeline):
            hold += 0.5
        scenes.append({'image_path': image_path, 'duration': round(hold, 3)})
    payload = {
        'job_id': f'{PROJECT.name}_step14_worker_onepass',
        'scenes': scenes,
        'audio_path': str(PROJECT / 'audio/master_narration.wav'),
        'subtitle_path': str(sub_path),
        'output_path': str(MAIN),
        'width': 1440,
        'height': 2560,
        'fps': 30,
        'crf': 20,
        'preset': 'veryfast',
        'burn_subtitles': True,
        'ken_burns': True,
        'motion_preset': 'storybook_smooth',
        'transition': 'cut',
        'transition_duration': 0.0,
        'fade_in': 0.0,
        'fade_out': 0.0,
        'timeline_start_sec': 0.0,
        'timeline_end_sec': None,
        'audio_start_sec': None,
        'audio_end_sec': None,
        'subtitle_time_offset_sec': 0.0,
        'slice_audio': False,
        'slice_subtitles': False,
    }
    log('STEP14 worker render main_story.mp4')
    response = post_json(RENDER_ENDPOINT, payload, timeout=7200)
    probe = post_json(PROBE_ENDPOINT, {'input_path': str(MAIN)}, timeout=120)
    report = {'endpoint': RENDER_ENDPOINT, 'payload': payload, 'response': response, 'probe': probe}
    (PROJECT / 'logs/step14_worker_onepass_manifest.json').write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding='utf-8')
    (PROJECT / 'output/main_story_probe.json').write_text(json.dumps(probe, ensure_ascii=False, indent=2), encoding='utf-8')
    return probe


def extend_intro_tail_silence():
    if not INTRO.exists():
        raise SystemExit(f'intro_missing: {INTRO}')
    if not INTRO_AUDIO.exists():
        raise SystemExit(f'intro_audio_missing: {INTRO_AUDIO}')
    tmp = INTRO.with_name('intro_with_2s_tail_tmp.mp4')
    payload_filter = (
        '[0:v]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,'
        'setsar=1,fps=30,format=yuv420p[v];'
        f'[1:a]aresample=44100,aformat=sample_rates=44100:channel_layouts=stereo,'
        f'apad=pad_dur={INTRO_TAIL_SILENCE_SEC}[a]'
    )
    import subprocess
    cmd = [
        'ffmpeg', '-y', '-v', 'error', '-loop', '1', '-i', str(THUMB), '-i', str(INTRO_AUDIO),
        '-filter_complex', payload_filter, '-map', '[v]', '-map', '[a]', '-t', str(duration_from_probe(post_json(PACKAGE_PROBE_ENDPOINT, {'input_path': str(INTRO_AUDIO)}, timeout=120)) + INTRO_TAIL_SILENCE_SEC),
        '-c:v', 'libx264', '-preset', 'veryfast', '-crf', '20', '-pix_fmt', 'yuv420p',
        '-c:a', 'aac', '-b:a', '192k', '-ar', '44100', '-ac', '2', str(tmp)
    ]
    subprocess.run(cmd, check=True)
    tmp.replace(INTRO)
    return post_json(PACKAGE_PROBE_ENDPOINT, {'input_path': str(INTRO)}, timeout=120)


def package_final_worker(main_probe):
    FINAL.parent.mkdir(parents=True, exist_ok=True)
    intro_probe = extend_intro_tail_silence()
    main_probe2 = post_json(PACKAGE_PROBE_ENDPOINT, {'input_path': str(MAIN)}, timeout=120)
    if not has_stream(intro_probe, 'audio') or not has_stream(main_probe2, 'audio'):
        raise SystemExit('intro_or_main_missing_audio')
    payload = {
        'intro_video_path': str(INTRO),
        'main_video_path': str(MAIN),
        'bgm_audio_path': str(BGM),
        'output_video_path': str(FINAL),
        'sample_rate': 44100,
        'channels': 2,
        'width': 1080,
        'height': 1920,
        'bgm_volume': BGM_VOLUME,
        'crf': 20,
        'preset': 'veryfast',
    }
    log('STEP18 worker package intro + main + BGM, no outro')
    response = post_json(PACKAGE_ENDPOINT, payload, timeout=7200)
    probe = response.get('probe') or post_json(PACKAGE_PROBE_ENDPOINT, {'input_path': str(FINAL)}, timeout=120)
    black = post_json(BLACKDETECT_ENDPOINT, {'input_path': str(FINAL), 'log_path': str(FINAL.parent / 'final_blackdetect.log')}, timeout=7200)
    expected = duration_from_probe(intro_probe) + duration_from_probe(main_probe2)
    report = {
        'project_id': PROJECT.name,
        'step': 18,
        'status': 'completed',
        'endpoint': PACKAGE_ENDPOINT,
        'order': ['intro', 'main_story'],
        'no_outro': True,
        'bgm_path': str(BGM),
        'bgm_volume': BGM_VOLUME,
        'output_video_path': str(FINAL),
        'expected_duration_sec': expected,
        'final_duration_sec': duration_from_probe(probe),
        'duration_diff_sec': duration_from_probe(probe) - expected,
        'probe': probe,
        'blackdetect': black,
        'package_response': response,
    }
    (FINAL.parent / 'final_video_probe.json').write_text(json.dumps(probe, ensure_ascii=False, indent=2), encoding='utf-8')
    (FINAL.parent / 'final_package_report.json').write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding='utf-8')
    (PROJECT / 'logs/step18_final_package_manifest.json').write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding='utf-8')
    return report


def main():
    LOG.write_text('', encoding='utf-8')
    sub_path, debug_path = build_asr_timed_subtitles()
    log(f'ASR subtitle written {sub_path}')
    main_probe_path = PROJECT / 'output/main_story_probe.json'
    if MAIN.exists() and main_probe_path.exists():
        probe = json.loads(main_probe_path.read_text(encoding='utf-8'))
        log('SKIP STEP14 using existing main_story.mp4')
    else:
        probe = render_main_worker(sub_path)
    log(f'MAIN duration={duration_from_probe(probe):.3f} audio={has_stream(probe, "audio")} video={has_stream(probe, "video")}')
    report = package_final_worker(probe)
    log(f'FINAL duration={report["final_duration_sec"]:.3f} bgm_volume={BGM_VOLUME}')
    info_path = PROJECT / 'output/info.json'
    info = json.loads(info_path.read_text(encoding='utf-8')) if info_path.exists() else {}
    info.update({
        'status': 'worker_rebuilt_needs_final_qa',
        'main_story': str(MAIN),
        'final_video': str(FINAL),
        'subtitles': str(sub_path),
        'subtitle_source': 'asr_word_timestamps_worker',
        'render_endpoint': RENDER_ENDPOINT,
        'package_endpoint': PACKAGE_ENDPOINT,
        'bgm_volume': BGM_VOLUME,
        'intro_tail_silence_sec': INTRO_TAIL_SILENCE_SEC,
    })
    info_path.write_text(json.dumps(info, ensure_ascii=False, indent=2), encoding='utf-8')
    log('COMPLETE worker rebuild')


if __name__ == '__main__':
    main()
