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

PROJECT = Path('/data/video-pipeline/Youtube-Video-Maker/Project/26052026-001-Ngo-Quyen')
TTS_ENDPOINT = 'http://192.168.1.201:3900/generate'
RENDER_ENDPOINT = 'http://192.168.1.104:8015/render-story-video'
PROBE_ENDPOINT = 'http://192.168.1.104:8002/probe'
WORKER_HOST = '192.168.1.104'
WORKER_PASS = '11210119'
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')
LOGO = Path('/data/video-pipeline/Youtube-Video-Maker/data/logo/logoframe.png')
THUMBNAIL = PROJECT / 'output/thumbnails/thumb_16x9_final.png'
SOURCE_STORY = PROJECT / 'script/source_story.txt'
CHECKLIST = PROJECT / 'checklist_19_steps.json'
INTRO_DIR = PROJECT / 'output/intro'
INTRO_TEXT = INTRO_DIR / 'intro_text.txt'
INTRO_TTS_RAW = INTRO_DIR / 'intro_tts_raw.wav'
INTRO_AUDIO = INTRO_DIR / 'intro_audio.wav'
INTRO_VIDEO = INTRO_DIR / 'intro.mp4'
INTRO_PROBE = INTRO_DIR / 'intro_probe.json'
MANIFEST = PROJECT / 'logs/step17_create_intro_manifest.json'


def log(msg):
    print(time.strftime('%Y-%m-%d %H:%M:%S ') + msg, flush=True)


def normalize_spaces(s):
    return re.sub(r'\s+', ' ', s).strip()


def read_metadata():
    raw = SOURCE_STORY.read_text(encoding='utf-8')
    title_match = re.search(r'Tiêu đề truyện:\s*(.+)', raw, re.IGNORECASE)
    series_match = re.search(r'Series:\s*(.+)', raw, re.IGNORECASE)
    if not title_match or not series_match:
        # Fallback only if source file is missing the explicit labels.
        checklist = json.loads(CHECKLIST.read_text(encoding='utf-8'))
        title = checklist.get('project_name')
        series = checklist.get('series')
    else:
        title = title_match.group(1).strip()
        series = series_match.group(1).strip()
    if not title or not series:
        raise RuntimeError('Missing required intro metadata: series/title')
    title = normalize_spaces(title).title().replace('—', '-').replace('Đằng', 'Đằng')
    # Preserve expected Vietnamese display casing for the known metadata source.
    title = title.replace('Ngô Quyền - Tiếng Sóng Bạch Đằng Mở Lại Non Sông', 'Ngô Quyền - Tiếng Sóng Bạch Đằng Mở Lại Non Sông')
    series = normalize_spaces(series)
    return series, title


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 generate_tts(text):
    ref_text = REF_TEXT.read_text(encoding='utf-8') if REF_TEXT.exists() else ''
    fields = {
        'text': text,
        'language': 'vi',
        'ref_text': ref_text,
        'speed': os.environ.get('INTRO_TTS_SPEED', '0.88'),
        'num_step': '16',
        'guidance_scale': '2.0',
        'effect_preset': 'broadcast',
    }
    files = {'ref_audio': REF_AUDIO}
    boundary, body = multipart(fields, files)
    req = urllib.request.Request(
        TTS_ENDPOINT,
        data=body,
        headers={'Content-Type': 'multipart/form-data; boundary=' + boundary},
        method='POST',
    )
    with urllib.request.urlopen(req, timeout=300) as resp:
        INTRO_TTS_RAW.write_bytes(resp.read())


def post_json(url, payload, timeout=3600):
    req = urllib.request.Request(url, data=json.dumps(payload, ensure_ascii=False).encode('utf-8'), headers={'Content-Type': 'application/json'}, method='POST')
    with urllib.request.urlopen(req, timeout=timeout) as resp:
        return json.loads(resp.read().decode('utf-8'))


def probe(path):
    doc = post_json(PROBE_ENDPOINT, {'input_path': str(path)}, timeout=120)
    fmt = doc.get('format', {})
    streams = doc.get('streams', [])
    return {
        'duration_sec': float(fmt.get('duration') or 0),
        'format_name': fmt.get('format_name'),
        'size': int(fmt.get('size') or 0),
        'has_audio': any(s.get('codec_type') == 'audio' for s in streams),
        'has_video': any(s.get('codec_type') == 'video' for s in streams),
        'streams': streams,
    }


def worker(cmd, timeout=600):
    full = ['sshpass', '-p', WORKER_PASS, 'ssh', '-o', 'StrictHostKeyChecking=no', '-o', 'PreferredAuthentications=password', '-o', 'PubkeyAuthentication=no', f'root@{WORKER_HOST}', cmd]
    p = subprocess.run(full, capture_output=True, text=True, timeout=timeout)
    if p.returncode != 0:
        raise RuntimeError(p.stderr[-4000:] or p.stdout[-4000:])
    return p.stdout


def make_padded_audio():
    cmd = (
        f"ffmpeg -y -hide_banner -loglevel error -i '{INTRO_TTS_RAW}' "
        f"-f lavfi -t 3 -i anullsrc=r=22050:cl=mono "
        f"-filter_complex '[0:a]aformat=sample_rates=22050:channel_layouts=mono[a0];[a0][1:a]concat=n=2:v=0:a=1[a]' "
        f"-map '[a]' -acodec pcm_s16le '{INTRO_AUDIO}'"
    )
    worker(cmd, timeout=300)


def render_intro(audio_duration):
    if not LOGO.exists():
        raise FileNotFoundError(LOGO)
    if not THUMBNAIL.exists():
        raise FileNotFoundError(THUMBNAIL)
    scene1 = 6.0
    scene2 = max(1.0, audio_duration - scene1)
    payload = {
        'job_id': f'{PROJECT.name}_step17_intro',
        'scenes': [
            {'image_path': str(LOGO), 'duration': scene1},
            {'image_path': str(THUMBNAIL), 'duration': scene2},
        ],
        'audio_path': str(INTRO_AUDIO),
        'subtitle_path': None,
        'output_path': str(INTRO_VIDEO),
        'width': 1920,
        'height': 1080,
        'fps': 30,
        'crf': 20,
        'preset': 'veryfast',
        'burn_subtitles': False,
        'ken_burns': True,
        'motion_preset': 'none',
        'transition': 'cut',
        'transition_duration': 0.0,
        'fade_in': 0.0,
        'fade_out': 0.0,
        'slice_audio': False,
        'slice_subtitles': False,
    }
    return post_json(RENDER_ENDPOINT, payload, timeout=3600), {'scene1_duration': scene1, 'scene2_duration': scene2}


def main():
    INTRO_DIR.mkdir(parents=True, exist_ok=True)
    MANIFEST.parent.mkdir(parents=True, exist_ok=True)
    series, title = read_metadata()
    locked_intro = f'Chào mừng quý vị trở lại với Vết Mực Thời Gian - Nơi quá khứ được kể lại bằng màu mực và ký ức. Hôm nay trong series {series}, chúng ta cùng đến với {title}.'
    intro = os.environ.get('INTRO_TEXT_OVERRIDE', locked_intro)
    INTRO_TEXT.write_text(intro + '\n', encoding='utf-8')
    log('TTS intro')
    generate_tts(intro)
    tts_probe = probe(INTRO_TTS_RAW)
    make_padded_audio()
    audio_probe = probe(INTRO_AUDIO)
    log(f'RENDER intro duration={audio_probe["duration_sec"]}')
    render_response, scene_plan = render_intro(audio_probe['duration_sec'])
    video_probe = probe(INTRO_VIDEO)
    override_used = intro != locked_intro
    title_spoken_variant = title.replace(' - ', ' ...')
    title_present = title in intro or title_spoken_variant in intro or ('Ngô Quyền' in intro and 'Tiếng Sóng Bạch Đằng Mở Lại Non Sông' in intro)
    valid = (
        INTRO_TEXT.exists()
        and INTRO_AUDIO.exists()
        and INTRO_VIDEO.exists()
        and audio_probe['has_audio']
        and video_probe['has_audio']
        and video_probe['has_video']
        and abs(video_probe['duration_sec'] - audio_probe['duration_sec']) <= 1.0
        and series in intro
        and title_present
    )
    doc = {
        'project_id': PROJECT.name,
        'step': 17,
        'status': 'completed' if valid else 'failed_validation',
        'series': series,
        'title': title,
        'intro_text': intro,
        'locked_intro_text': locked_intro,
        'override_used': override_used,
        'intro_text_path': str(INTRO_TEXT),
        'intro_tts_raw_path': str(INTRO_TTS_RAW),
        'intro_audio_path': str(INTRO_AUDIO),
        'intro_video_path': str(INTRO_VIDEO),
        'intro_audio_probe': audio_probe,
        'intro_tts_raw_probe': tts_probe,
        'intro_video_probe': video_probe,
        'render_response': render_response,
        'scene_plan': scene_plan,
        'validation': {
            'locked_form_used': intro == locked_intro,
            'user_override_used': override_used,
            'metadata_series_used': series in intro,
            'metadata_title_used': title_present,
            'audio_exists': INTRO_AUDIO.exists(),
            'video_exists': INTRO_VIDEO.exists(),
            'has_audio': video_probe['has_audio'],
            'has_video': video_probe['has_video'],
            'valid': valid,
        },
    }
    INTRO_PROBE.write_text(json.dumps(doc, ensure_ascii=False, indent=2), encoding='utf-8')
    MANIFEST.write_text(json.dumps({
        'project_id': PROJECT.name,
        'step': 17,
        'status': doc['status'],
        'series': series,
        'title': title,
        'intro_text_path': str(INTRO_TEXT),
        'intro_audio_path': str(INTRO_AUDIO),
        'intro_video_path': str(INTRO_VIDEO),
        'intro_probe_path': str(INTRO_PROBE),
        'duration_sec': video_probe['duration_sec'],
    }, ensure_ascii=False, indent=2), encoding='utf-8')
    if not valid:
        raise SystemExit('failed_validation')
    log('DONE ' + json.dumps({'status': doc['status'], 'duration': video_probe['duration_sec'], 'series': series, 'title': title}, ensure_ascii=False))


if __name__ == '__main__':
    main()
