#!/usr/bin/env python3
import hashlib
import json
import os
import subprocess
import sys
import urllib.parse
import urllib.request
import uuid
import wave
from datetime import datetime, timezone
from pathlib import Path

PROJECT = Path(__file__).resolve().parents[1]
PROMOTION = PROJECT / "story/promotion-report.json"
LAYOUT = PROJECT / "image/layout-manifest.json"
POSTER = PROJECT / "image/intro-poster-1920x1080.png"
VOICE = PROJECT / "audio/intro-voice.wav"
FULL = PROJECT / "audio/intro-full.wav"
OUTPUT = PROJECT / "output/intro/intro.mp4"
RECEIPT = PROJECT / "log/intro-render.json"
VOICE_RECEIPT = PROJECT / "log/intro-voice.json"
TTS_BASE = "http://192.168.40.33:7862"
GPU_BASE = "http://192.168.1.104:8021"
VOICE_NAME = "ngoc-huyen-vbee"
TEXT = "Các bạn đang nghe truyện được phát từ Huyền An Audio. Chúc các bạn có những giây phút nghe truyện vui vẻ. Hãy ủng hộ chúng tôi bằng cách thích video và đăng ký kênh."


def sha(path):
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for block in iter(lambda: handle.read(8 * 1024 * 1024), b""):
            digest.update(block)
    return digest.hexdigest()


def probe(path):
    result = subprocess.run([
        "ffprobe", "-v", "error", "-show_entries", "format=duration,size",
        "-show_entries", "stream=index,codec_name,width,height,sample_rate,channels:stream_tags=encoder",
        "-of", "json", str(path),
    ], capture_output=True, text=True, check=True)
    return json.loads(result.stdout)


def request_json(url, payload=None, timeout=30):
    body = json.dumps(payload).encode("utf-8") if payload is not None else None
    request = urllib.request.Request(url, data=body, headers={"Content-Type": "application/json"} if body else {}, method="POST" if body else "GET")
    with urllib.request.urlopen(request, timeout=timeout) as response:
        return json.load(response)


def synthesize_intro(canon):
    text_sha = hashlib.sha256(TEXT.encode("utf-8")).hexdigest()
    if VOICE_RECEIPT.exists() and VOICE.exists():
        old = json.loads(VOICE_RECEIPT.read_text(encoding="utf-8"))
        if old.get("verified") is True and old.get("source_canon_sha256") == canon and old.get("text_sha256") == text_sha and old.get("voice") == VOICE_NAME and old.get("output_sha256") == sha(VOICE):
            return
    health = request_json(TTS_BASE + "/health")
    voices = request_json(TTS_BASE + "/voices").get("voices", [])
    row = next((item for item in voices if item.get("id") == VOICE_NAME), None)
    if health.get("status") != "ok" or not row or row.get("model_exists") is not True or row.get("config_exists") is not True:
        raise RuntimeError("intro TTS preflight failed")
    body = urllib.parse.urlencode({"text": TEXT, "voice": VOICE_NAME}).encode("utf-8")
    req = urllib.request.Request(TTS_BASE + "/tts", data=body, headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
    with urllib.request.urlopen(req, timeout=300) as response:
        data = response.read()
    if len(data) < 44 or data[:4] != b"RIFF" or data[8:12] != b"WAVE":
        raise RuntimeError("intro TTS response is not WAV")
    temp = VOICE.with_suffix(".part.wav")
    temp.write_bytes(data)
    os.replace(temp, VOICE)
    VOICE_RECEIPT.write_text(json.dumps({"version": 1, "verified": True, "source_canon_sha256": canon, "text_sha256": text_sha, "voice": VOICE_NAME, "endpoint": TTS_BASE, "output": str(VOICE.relative_to(PROJECT)), "output_sha256": sha(VOICE), "checked_at": datetime.now(timezone.utc).isoformat()}, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")


def append_silence():
    temp = FULL.with_suffix(".part.wav")
    with wave.open(str(VOICE), "rb") as source:
        params = source.getparams()
        if source.getcomptype() != "NONE":
            raise RuntimeError("intro voice WAV is compressed")
        frames = source.readframes(source.getnframes())
    silence_frames = int(params.framerate * 2.0)
    silence = b"\x00" * silence_frames * params.nchannels * params.sampwidth
    with wave.open(str(temp), "wb") as target:
        target.setparams(params)
        target.writeframes(frames)
        target.writeframes(silence)
    os.replace(temp, FULL)


def main():
    for path in (PROMOTION, LAYOUT, POSTER):
        if not path.is_file():
            raise RuntimeError(f"missing {path}")
    promotion = json.loads(PROMOTION.read_text(encoding="utf-8"))
    layout = json.loads(LAYOUT.read_text(encoding="utf-8"))
    canon = promotion.get("spoken_sha256")
    if promotion.get("verified") is not True or layout.get("verified") is not True or layout.get("visual_qa", {}).get("verified") is not True or layout.get("source_canon_sha256") != canon:
        raise RuntimeError("intro authority gate mismatch")
    synthesize_intro(canon)
    append_silence()
    full_duration = float(probe(FULL)["format"]["duration"])
    health = request_json(GPU_BASE + "/health")
    if health.get("status") != "ok" or health.get("encoder") != "h264_nvenc" or health.get("nvenc_available") is not True:
        raise RuntimeError("intro GPU endpoint does not verify h264_nvenc")
    if RECEIPT.exists() and OUTPUT.exists():
        old = json.loads(RECEIPT.read_text(encoding="utf-8"))
        if old.get("verified") is True and old.get("source_canon_sha256") == canon and old.get("poster_sha256") == sha(POSTER) and old.get("audio_sha256") == sha(FULL) and old.get("output_sha256") == sha(OUTPUT):
            print(json.dumps({"verified": True, "reused": True, "output": str(OUTPUT)}, ensure_ascii=False))
            return 0
    OUTPUT.parent.mkdir(parents=True, exist_ok=True)
    job_id = "huyenan-011-intro-" + uuid.uuid4().hex[:12]
    payload = {"job_id": job_id, "scenes": [{"image_path": str(POSTER), "duration": full_duration}], "audio_path": str(FULL), "subtitle_path": None, "output_path": str(OUTPUT), "width": 1920, "height": 1080, "fps": 30, "crf": 18, "preset": "p4", "burn_subtitles": False, "ken_burns": False, "motion_preset": "none", "transition": "cut", "transition_duration": 0.0, "fade_in": 0.0, "fade_out": 0.0, "slice_audio": False, "slice_subtitles": False}
    result = request_json(GPU_BASE + "/stickman-render-gpu", payload, timeout=900)
    if not OUTPUT.is_file():
        raise RuntimeError("intro GPU endpoint returned without output")
    output_probe = probe(OUTPUT)
    streams = output_probe.get("streams", [])
    duration = float(output_probe["format"]["duration"])
    verified = len(streams) == 2 and any(row.get("codec_name") == "h264" for row in streams) and any(row.get("codec_name") == "aac" for row in streams) and abs(duration - full_duration) <= 0.1
    receipt = {"version": 1, "verified": verified, "status": "completed" if verified else "failed", "source_canon_sha256": canon, "job_id": job_id, "endpoint": GPU_BASE, "encoder_required": "h264_nvenc", "server_health": health, "server_result": result, "poster_sha256": sha(POSTER), "audio_sha256": sha(FULL), "output": str(OUTPUT.relative_to(PROJECT)), "output_sha256": sha(OUTPUT), "duration_seconds": duration, "probe": output_probe, "visual_qa": {"required": True, "status": "pending", "verified": False}, "checked_at": datetime.now(timezone.utc).isoformat()}
    RECEIPT.write_text(json.dumps(receipt, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    print(json.dumps({"verified": verified, "job_id": job_id, "duration": duration}, ensure_ascii=False))
    return 0 if verified else 1


if __name__ == "__main__":
    try:
        sys.exit(main())
    except Exception as exc:
        print(f"Intro blocked: {exc}", file=sys.stderr)
        sys.exit(1)
