"""
小星糖自拍錄影 Skill — 一條龍腳本

接受 CLI 參數，跑完印出最終可傳 Discord 的壓縮影片路徑。

流程：
  1. 把輸入圖上傳到 ComfyUI input/
  2. 載入 workflow/starvideo.json，注入 tts_text / videotext / image / resolution
  3. POST /prompt 提交
  4. Poll /history 直到完成（最多 10 分鐘）
  5. 從 /view 下載結果 mp4
  6. ffmpeg CRF 28 壓縮到 < 8 MB（Discord 上限）
  7. stdout 最後一行印出 `READY: <絕對路徑>`

用法：
  python make_video.py \
      --image F:/path/to/selfie.webp \
      --tts-text "主人，今天班上..." \
      --videotext "photorealistic ... talking to camera ..." \
      --output-dir F:/AIStudio/ClaudeCodeLover/Recorder \
      [--resolution 1280] [--comfy http://127.0.0.1:8188]

回傳碼：
  0 = 成功，stdout 最後一行有 `READY: <path>`
  1 = ComfyUI 呼叫失敗
  2 = 參數錯誤
  3 = ffmpeg 壓縮失敗
"""

import argparse
import json
import random
import shutil
import subprocess
import sys
import time
import urllib.parse
import urllib.request
from pathlib import Path

SKILL_ROOT = Path(__file__).resolve().parent.parent
WORKFLOW_PATH = SKILL_ROOT / "workflow" / "starvideo.json"

POLL_INTERVAL = 10
POLL_DEADLINE = 600  # 10 min
DISCORD_LIMIT_BYTES = 8 * 1024 * 1024  # 8 MB


def upload_image(comfy: str, image_path: Path) -> str:
    """Upload to ComfyUI input/ via /upload/image, return server-side filename."""
    if not image_path.exists():
        raise FileNotFoundError(f"input image not found: {image_path}")

    # Use curl for multipart form (avoid stdlib boilerplate)
    result = subprocess.run(
        [
            "curl", "-s", "-X", "POST",
            "-F", f"image=@{image_path}",
            "-F", "type=input",
            "-F", "overwrite=true",
            f"{comfy}/upload/image",
        ],
        capture_output=True, text=True, check=True,
    )
    data = json.loads(result.stdout)
    return data["name"]


def submit_workflow(
    comfy: str, image_name: str, tts_text: str, videotext: str, resolution: int
) -> str:
    """Patch workflow JSON and POST /prompt; return prompt_id."""
    wf = json.loads(WORKFLOW_PATH.read_text(encoding="utf-8"))

    # Node 365 = BreezyVoiceCrossLingualNode
    wf["365"]["inputs"]["tts_text"] = tts_text
    wf["365"]["inputs"]["seed"] = random.randint(1, 2**31 - 1)

    # Node 16 = CLIPTextEncode (videotext motion prompt)
    wf["16"]["inputs"]["text"] = videotext

    # Node 348 = LoadImage
    wf["348"]["inputs"]["image"] = image_name

    # Node 357 = ImageResizeKJv2 (long edge)
    wf["357"]["inputs"]["width"] = resolution
    wf["357"]["inputs"]["height"] = resolution

    # Node 15 = RandomNoise (video seed)
    wf["15"]["inputs"]["noise_seed"] = random.randint(1, 2**31 - 1)

    payload = json.dumps({"prompt": wf}).encode("utf-8")
    req = urllib.request.Request(
        f"{comfy}/prompt",
        data=payload,
        headers={"Content-Type": "application/json"},
    )
    with urllib.request.urlopen(req, timeout=30) as resp:
        result = json.loads(resp.read().decode("utf-8"))

    prompt_id = result.get("prompt_id")
    if not prompt_id:
        raise RuntimeError(f"submit failed: {result}")
    return prompt_id


def poll_until_done(comfy: str, prompt_id: str, output_dir: Path) -> Path:
    """Poll /history until completed, then fetch the video. Return local path."""
    start = time.time()
    while time.time() - start < POLL_DEADLINE:
        try:
            with urllib.request.urlopen(
                f"{comfy}/history/{prompt_id}", timeout=15
            ) as r:
                hist = json.loads(r.read())
        except Exception as e:
            print(f"[{int(time.time()-start)}s] poll err: {e}", flush=True)
            time.sleep(POLL_INTERVAL)
            continue

        entry = hist.get(prompt_id)
        if not entry or not entry.get("status", {}).get("completed"):
            print(
                f"[{int(time.time()-start)}s] running...",
                flush=True,
            )
            time.sleep(POLL_INTERVAL)
            continue

        # Done — extract first video output
        outputs = entry.get("outputs", {})
        for node_id, node_out in outputs.items():
            for key in ("videos", "gifs", "images"):
                for item in node_out.get(key, []) or []:
                    filename = item["filename"]
                    if not filename.lower().endswith(".mp4"):
                        continue
                    subfolder = item.get("subfolder", "")
                    ftype = item.get("type", "output")
                    url = (
                        f"{comfy}/view?"
                        f"filename={urllib.parse.quote(filename)}"
                        f"&subfolder={urllib.parse.quote(subfolder)}"
                        f"&type={ftype}"
                    )
                    local = output_dir / filename
                    with urllib.request.urlopen(url, timeout=120) as r:
                        local.write_bytes(r.read())
                    return local

        raise RuntimeError(
            f"workflow done but no mp4 output found: {list(outputs.keys())}"
        )

    raise TimeoutError(f"poll deadline exceeded ({POLL_DEADLINE}s)")


def compress_for_discord(raw_mp4: Path) -> Path:
    """ffmpeg CRF 28 → < 8 MB. Skip if already small enough."""
    if raw_mp4.stat().st_size <= DISCORD_LIMIT_BYTES:
        return raw_mp4

    compressed = raw_mp4.with_name(raw_mp4.stem + "-compressed.mp4")
    cmd = [
        "ffmpeg", "-y", "-i", str(raw_mp4),
        "-c:v", "libx264", "-preset", "medium", "-crf", "28",
        "-c:a", "aac", "-b:a", "96k",
        "-movflags", "+faststart",
        str(compressed),
    ]
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0:
        raise RuntimeError(
            f"ffmpeg failed (exit {result.returncode}): {result.stderr[-500:]}"
        )

    # Sanity check
    if compressed.stat().st_size > DISCORD_LIMIT_BYTES:
        # Fallback: try CRF 32 + 720p downscale for very long videos
        compressed.unlink(missing_ok=True)
        cmd[cmd.index("28")] = "32"
        cmd.insert(cmd.index("-c:a"), "-vf")
        cmd.insert(cmd.index("-c:a"), "scale=540:960:force_original_aspect_ratio=decrease")
        result = subprocess.run(cmd, capture_output=True, text=True)
        if result.returncode != 0:
            raise RuntimeError(f"ffmpeg fallback failed: {result.stderr[-500:]}")

    return compressed


def main() -> int:
    parser = argparse.ArgumentParser(
        description="小星糖自拍錄影一條龍腳本"
    )
    parser.add_argument("--image", required=True, type=Path,
                        help="輸入圖路徑（會被上傳到 ComfyUI input/）")
    parser.add_argument("--tts-text", required=True,
                        help="小星糖要說的話（中文，先用 tts-prep 規則優化）")
    parser.add_argument("--videotext", required=True,
                        help="LTX-2 motion prompt（英文，talking/lips moving 主導）")
    parser.add_argument("--output-dir", required=True, type=Path,
                        help="輸出資料夾（影片 + 壓縮版會放這裡）")
    parser.add_argument("--resolution", type=int, default=1280,
                        help="長邊解析度（預設 1280；30 秒以上建議 960）")
    parser.add_argument("--comfy", default="http://127.0.0.1:8188",
                        help="ComfyUI 端點（預設 127.0.0.1:8188）")
    args = parser.parse_args()

    args.output_dir.mkdir(parents=True, exist_ok=True)

    # Pre-flight: ensure ffmpeg is on PATH
    if not shutil.which("ffmpeg"):
        print("ERROR: ffmpeg not found on PATH", file=sys.stderr)
        return 2

    try:
        print(f"[step 1/4] upload image: {args.image.name}", flush=True)
        image_name = upload_image(args.comfy, args.image)

        print(f"[step 2/4] submit workflow ({len(args.tts_text)} chars, "
              f"long_edge={args.resolution})", flush=True)
        prompt_id = submit_workflow(
            args.comfy, image_name,
            args.tts_text, args.videotext, args.resolution,
        )
        print(f"           prompt_id={prompt_id}", flush=True)

        print(f"[step 3/4] poll until done (deadline {POLL_DEADLINE}s)",
              flush=True)
        raw_mp4 = poll_until_done(args.comfy, prompt_id, args.output_dir)
        size_mb = raw_mp4.stat().st_size / 1024 / 1024
        print(f"           raw: {raw_mp4.name} ({size_mb:.1f} MB)", flush=True)

        print(f"[step 4/4] compress for Discord (limit 8 MB)", flush=True)
        final = compress_for_discord(raw_mp4)
        size_mb = final.stat().st_size / 1024 / 1024
        print(f"           final: {final.name} ({size_mb:.1f} MB)", flush=True)

        print(f"READY: {final}", flush=True)
        return 0

    except Exception as e:
        print(f"FAILED: {type(e).__name__}: {e}", file=sys.stderr, flush=True)
        return 1


if __name__ == "__main__":
    sys.exit(main())
