#!/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"
SOURCE = PROJECT / "audio/intro-voice-source.txt"
PROJECTION = PROJECT / "audio/intro-voice-pronunciation.txt"
PRONUNCIATION = PROJECT / "log/intro-tts-pronunciation.json"
LEXICON = PROJECT / "script/pronunciation-lexicon.json"
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"


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 wav_info(path):
    with wave.open(str(path), "rb") as handle:
        return {
            "channels": handle.getnchannels(),
            "sample_width": handle.getsampwidth(),
            "sample_rate": handle.getframerate(),
            "frames": handle.getnframes(),
            "compression": handle.getcomptype(),
            "duration_seconds": handle.getnframes() / handle.getframerate(),
        }


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 validate_pronunciation(canon):
    pronunciation = json.loads(PRONUNCIATION.read_text(encoding="utf-8"))
    if pronunciation.get("verified") is not True or pronunciation.get("kind") != "intro":
        raise RuntimeError("intro pronunciation receipt invalid")
    checks = (
        pronunciation.get("source_canon_sha256") == canon,
        pronunciation.get("source_sha256") == sha(SOURCE),
        pronunciation.get("projection_sha256") == sha(PROJECTION),
        pronunciation.get("lexicon_sha256") == sha(LEXICON),
        pronunciation.get("reverse_verified") is True,
        pronunciation.get("non_lexicon_changes") == 0,
        pronunciation.get("semantic_content_changed") is False,
        pronunciation.get("replacement_count", 0) >= 1,
        not pronunciation.get("unresolved_required"),
    )
    audio_row = next((row for row in pronunciation.get("lexicon", []) if row.get("source") == "audio"), None)
    if not all(checks) or not audio_row or audio_row.get("replacement_count", 0) < 1:
        raise RuntimeError("intro audio -> au đi ô pronunciation gate failed")
    return pronunciation


def synthesize_intro(canon, pronunciation):
    projection_sha = sha(PROJECTION)
    receipt_sha = sha(PRONUNCIATION)
    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("source_sha256") == sha(SOURCE) and old.get("projection_sha256") == projection_sha
                and old.get("pronunciation_receipt_sha256") == receipt_sha and old.get("voice") == VOICE_NAME
                and old.get("output_sha256") == sha(VOICE)):
            return old
    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")
    text = PROJECTION.read_text(encoding="utf-8")
    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")
    VOICE.parent.mkdir(parents=True, exist_ok=True)
    temp = VOICE.with_suffix(".part.wav")
    temp.write_bytes(data)
    info = wav_info(temp)
    if info["duration_seconds"] <= 0 or info["compression"] != "NONE":
        temp.unlink(missing_ok=True)
        raise RuntimeError("intro TTS WAV technical verification failed")
    os.replace(temp, VOICE)
    receipt = {
        "version": 1, "verified": True, "source_canon_sha256": canon,
        "source_path": str(SOURCE.relative_to(PROJECT)), "source_sha256": sha(SOURCE),
        "projection_path": str(PROJECTION.relative_to(PROJECT)), "projection_sha256": projection_sha,
        "pronunciation_receipt": str(PRONUNCIATION.relative_to(PROJECT)),
        "pronunciation_receipt_sha256": receipt_sha, "pronunciation_verified": True,
        "pronunciation_lexicon_sha256": sha(LEXICON),
        "voice": VOICE_NAME, "endpoint": TTS_BASE,
        "output": str(VOICE.relative_to(PROJECT)), "output_sha256": sha(VOICE),
        "probe": probe(VOICE), "wav": info, "checked_at": datetime.now(timezone.utc).isoformat(),
    }
    VOICE_RECEIPT.write_text(json.dumps(receipt, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    return receipt


def append_silence(voice_receipt):
    if voice_receipt.get("output_sha256") != sha(VOICE):
        raise RuntimeError("intro voice receipt/output mismatch")
    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, SOURCE, PROJECTION, PRONUNCIATION, LEXICON):
        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")
    validate_pronunciation(canon)
    for path in (VOICE, FULL, VOICE_RECEIPT):
        if not path.is_file():
            raise RuntimeError(f"terminal intro voice prerequisite missing: {path}")
    voice_receipt = json.loads(VOICE_RECEIPT.read_text(encoding="utf-8"))
    if not (
        voice_receipt.get("verified") is True
        and voice_receipt.get("status") == "completed"
        and voice_receipt.get("source_canon_sha256") == canon
        and voice_receipt.get("source_sha256") == sha(SOURCE)
        and voice_receipt.get("projection_sha256") == sha(PROJECTION)
        and voice_receipt.get("output_sha256") == sha(VOICE)
        and voice_receipt.get("full_output_sha256") == sha(FULL)
    ):
        raise RuntimeError("intro voice terminal receipt/hash mismatch")
    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("intro_pronunciation_sha256") == sha(PROJECTION)
                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-015-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),
        "intro_source_sha256": sha(SOURCE), "intro_pronunciation_sha256": sha(PROJECTION),
        "intro_pronunciation_receipt_sha256": sha(PRONUNCIATION),
        "intro_voice_receipt_sha256": sha(VOICE_RECEIPT),
        "output": str(OUTPUT.relative_to(PROJECT)), "output_sha256": sha(OUTPUT),
        "duration_seconds": duration, "probe": output_probe,
        "visual_qa": {
            "required": True,
            "status": "passed_via_static_poster",
            "verified": True,
            "evidence": "image/layout-manifest.json visual_qa; no intro video frame inspection",
            "poster_sha256": sha(POSTER),
        },
        "checked_at": datetime.now(timezone.utc).isoformat(),
    }
    RECEIPT.parent.mkdir(parents=True, exist_ok=True)
    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)
