"""ffmpeg 기반 MP4 렌더링 모듈.

IDS Phase 5 — 모션 카드뉴스 (HTML→MP4)
- 5가지 모션 효과 지원 (fade, slide, zoom, dissolve, sequence)
- libx264 / yuv420p — SNS 호환 출력
- BGM 믹싱 지원
"""
from __future__ import annotations

import os
import shutil
import subprocess
import tempfile
from pathlib import Path
from typing import Optional


def _get_ffmpeg_bin() -> str:
    """ffmpeg 실행 파일 경로를 반환합니다.

    우선순위: 환경변수 FFMPEG_BIN → /home/jay/.local/bin/ffmpeg → PATH lookup
    """
    env_bin = os.environ.get("FFMPEG_BIN", "")
    if env_bin and Path(env_bin).exists():
        return str(env_bin)

    local_bin = "/home/jay/.local/bin/ffmpeg"
    if Path(local_bin).exists():
        return local_bin

    path_bin = shutil.which("ffmpeg")
    if path_bin:
        return path_bin

    raise FileNotFoundError(
        "ffmpeg를 찾을 수 없습니다. FFMPEG_BIN 환경변수를 설정하거나 PATH에 ffmpeg를 추가하세요."
    )


def _build_fade_cmd(
    frame_paths: list[Path],
    output_path: Path,
    size: tuple[int, int],
    fps: int,
    duration_per_frame: float,
    bgm_path: Optional[Path],
    ffmpeg_bin: str,
) -> list[str]:
    """fade 효과 ffmpeg 명령어를 구성합니다."""
    w, h = size
    n = len(frame_paths)
    fade_d = min(0.5, duration_per_frame * 0.3)
    fade_out_st = duration_per_frame - fade_d

    cmd: list[str] = [ffmpeg_bin, "-y"]

    for fp in frame_paths:
        cmd += ["-loop", "1", "-t", str(duration_per_frame), "-i", str(fp)]

    if bgm_path:
        cmd += ["-i", str(bgm_path)]

    filter_parts: list[str] = []
    for i in range(n):
        filter_parts.append(
            f"[{i}:v]scale={w}:{h}:force_original_aspect_ratio=decrease,"
            f"pad={w}:{h}:(ow-iw)/2:(oh-ih)/2,setsar=1,"
            f"fade=t=in:st=0:d={fade_d},"
            f"fade=t=out:st={fade_out_st}:d={fade_d}[v{i}]"
        )

    inputs = "".join(f"[v{i}]" for i in range(n))
    filter_parts.append(f"{inputs}concat=n={n}:v=1:a=0[vout]")
    filter_str = ";".join(filter_parts)

    cmd += ["-filter_complex", filter_str, "-map", "[vout]"]

    if bgm_path:
        cmd += ["-map", f"{n}:a", "-shortest"]
        cmd += ["-af", f"afade=t=out:st={duration_per_frame * n - 1}:d=1"]

    cmd += [
        "-c:v", "libx264",
        "-pix_fmt", "yuv420p",
        "-r", str(fps),
        "-movflags", "+faststart",
        str(output_path),
    ]
    return cmd


def _build_slide_cmd(
    frame_paths: list[Path],
    output_path: Path,
    size: tuple[int, int],
    fps: int,
    duration_per_frame: float,
    bgm_path: Optional[Path],
    ffmpeg_bin: str,
) -> list[str]:
    """slide 효과 ffmpeg 명령어를 구성합니다."""
    w, h = size
    n = len(frame_paths)

    cmd: list[str] = [ffmpeg_bin, "-y"]

    for fp in frame_paths:
        cmd += ["-loop", "1", "-t", str(duration_per_frame), "-i", str(fp)]

    if bgm_path:
        cmd += ["-i", str(bgm_path)]

    filter_parts: list[str] = []
    for i in range(n):
        filter_parts.append(
            f"[{i}:v]scale={w}:{h}:force_original_aspect_ratio=decrease,"
            f"pad={w}:{h}:(ow-iw)/2:(oh-ih)/2,setsar=1[v{i}]"
        )

    inputs = "".join(f"[v{i}]" for i in range(n))
    filter_parts.append(f"{inputs}concat=n={n}:v=1:a=0[vout]")
    filter_str = ";".join(filter_parts)

    cmd += ["-filter_complex", filter_str, "-map", "[vout]"]

    if bgm_path:
        cmd += ["-map", f"{n}:a", "-shortest"]

    cmd += [
        "-c:v", "libx264",
        "-pix_fmt", "yuv420p",
        "-r", str(fps),
        "-movflags", "+faststart",
        str(output_path),
    ]
    return cmd


def _build_zoom_cmd(
    frame_paths: list[Path],
    output_path: Path,
    size: tuple[int, int],
    fps: int,
    duration_per_frame: float,
    bgm_path: Optional[Path],
    ffmpeg_bin: str,
) -> list[str]:
    """zoom (Ken Burns) 효과 ffmpeg 명령어를 구성합니다."""
    w, h = size
    n = len(frame_paths)
    total_frames = int(fps * duration_per_frame)

    cmd: list[str] = [ffmpeg_bin, "-y"]

    for fp in frame_paths:
        cmd += ["-loop", "1", "-t", str(duration_per_frame), "-i", str(fp)]

    if bgm_path:
        cmd += ["-i", str(bgm_path)]

    filter_parts: list[str] = []
    for i in range(n):
        zoom_w = int(w * 1.2)
        zoom_h = int(h * 1.2)
        filter_parts.append(
            f"[{i}:v]scale={zoom_w}:{zoom_h}:force_original_aspect_ratio=increase,"
            f"crop={w}:{h},"
            f"zoompan=z='min(zoom+0.0005,1.1)':d={total_frames}:s={w}x{h}:fps={fps}[v{i}]"
        )

    inputs = "".join(f"[v{i}]" for i in range(n))
    filter_parts.append(f"{inputs}concat=n={n}:v=1:a=0[vout]")
    filter_str = ";".join(filter_parts)

    cmd += ["-filter_complex", filter_str, "-map", "[vout]"]

    if bgm_path:
        cmd += ["-map", f"{n}:a", "-shortest"]

    cmd += [
        "-c:v", "libx264",
        "-pix_fmt", "yuv420p",
        "-r", str(fps),
        "-movflags", "+faststart",
        str(output_path),
    ]
    return cmd


def _build_dissolve_cmd(
    frame_paths: list[Path],
    output_path: Path,
    size: tuple[int, int],
    fps: int,
    duration_per_frame: float,
    bgm_path: Optional[Path],
    ffmpeg_bin: str,
) -> tuple[list[str], Optional[str]]:
    """dissolve (cross-dissolve) 효과 ffmpeg 명령어를 구성합니다."""
    w, h = size
    n = len(frame_paths)
    xfade_d = min(0.5, duration_per_frame * 0.3)

    cmd: list[str] = [ffmpeg_bin, "-y"]

    for fp in frame_paths:
        cmd += ["-loop", "1", "-t", str(duration_per_frame), "-i", str(fp)]

    if bgm_path:
        cmd += ["-i", str(bgm_path)]

    filter_parts: list[str] = []

    for i in range(n):
        filter_parts.append(
            f"[{i}:v]scale={w}:{h}:force_original_aspect_ratio=decrease,"
            f"pad={w}:{h}:(ow-iw)/2:(oh-ih)/2,setsar=1[scaled{i}]"
        )

    if n == 1:
        filter_parts.append("[scaled0]copy[vout]")
    else:
        prev_label = "scaled0"
        for i in range(1, n):
            offset = max(0.0, (i * duration_per_frame) - xfade_d)
            out_label = "vout" if i == n - 1 else f"xf{i}"
            filter_parts.append(
                f"[{prev_label}][scaled{i}]xfade=transition=dissolve:"
                f"duration={xfade_d}:offset={offset}[{out_label}]"
            )
            prev_label = out_label

    filter_str = ";".join(filter_parts)

    cmd += ["-filter_complex", filter_str, "-map", "[vout]"]

    if bgm_path:
        cmd += ["-map", f"{n}:a", "-shortest"]

    cmd += [
        "-c:v", "libx264",
        "-pix_fmt", "yuv420p",
        "-r", str(fps),
        "-movflags", "+faststart",
        str(output_path),
    ]
    return cmd, None


def _build_sequence_cmd(
    frame_paths: list[Path],
    output_path: Path,
    size: tuple[int, int],
    fps: int,
    duration_per_frame: float,
    bgm_path: Optional[Path],
    ffmpeg_bin: str,
) -> tuple[list[str], Optional[str]]:
    """sequence 효과 (concat demuxer) ffmpeg 명령어를 구성합니다.

    Returns:
        (cmd 리스트, concat_list_path 또는 None)
        호출자가 임시 파일을 정리해야 합니다.
    """
    w, h = size

    concat_file = tempfile.NamedTemporaryFile(
        mode="w", suffix=".txt", delete=False, prefix="motion_concat_"
    )
    for fp in frame_paths:
        concat_file.write(f"file '{Path(fp).absolute()}'\n")
        concat_file.write(f"duration {duration_per_frame}\n")
    if frame_paths:
        concat_file.write(f"file '{Path(frame_paths[-1]).absolute()}'\n")
    concat_file.close()
    concat_list_path = concat_file.name

    cmd: list[str] = [ffmpeg_bin, "-y"]
    cmd += ["-f", "concat", "-safe", "0", "-i", concat_list_path]

    if bgm_path:
        cmd += ["-i", str(bgm_path)]

    filter_str = (
        f"[0:v]scale={w}:{h}:force_original_aspect_ratio=decrease,"
        f"pad={w}:{h}:(ow-iw)/2:(oh-ih)/2,setsar=1[vout]"
    )
    cmd += ["-filter_complex", filter_str, "-map", "[vout]"]

    if bgm_path:
        cmd += ["-map", "1:a", "-shortest"]

    cmd += [
        "-c:v", "libx264",
        "-pix_fmt", "yuv420p",
        "-r", str(fps),
        "-movflags", "+faststart",
        str(output_path),
    ]
    return cmd, concat_list_path


def render_motion(
    frame_paths: list[Path],
    output_path: Path,
    *,
    size: tuple[int, int],
    effect: str = "fade",
    fps: int = 30,
    duration_per_frame: float = 1.0,
    bgm_path: Optional[Path] = None,
) -> Path:
    """PNG 프레임 시퀀스를 ffmpeg로 MP4 동영상으로 렌더링합니다.

    Args:
        frame_paths: PNG 프레임 파일 경로 목록 (순서대로)
        output_path: 출력 MP4 파일 경로
        size: (width, height) 출력 해상도
        effect: 모션 효과 ('fade', 'slide', 'zoom', 'dissolve', 'sequence')
        fps: 초당 프레임 수 (기본값: 30)
        duration_per_frame: 프레임당 표시 시간(초) (기본값: 1.0)
        bgm_path: BGM 파일 경로 (없으면 None)

    Returns:
        생성된 MP4 파일의 Path 객체

    Raises:
        ValueError: 알 수 없는 효과 이름인 경우
        RuntimeError: ffmpeg 실행 실패 시 (stderr 포함)
        FileNotFoundError: ffmpeg 바이너리를 찾을 수 없는 경우
    """
    # L1 standalone import fallback
    try:
        from .effects import get_effect_filter  # pyright: ignore[reportMissingImports]
        _ = get_effect_filter
    except ImportError:
        try:
            import importlib.util as _ilu
            _spec = _ilu.spec_from_file_location(
                "_mc_effects_standalone",
                Path(__file__).parent / "effects.py",
            )
            assert _spec is not None and _spec.loader is not None, "effects.py 모듈을 로드할 수 없습니다"
            _effects_mod = _ilu.module_from_spec(_spec)
            _spec.loader.exec_module(_effects_mod)  # type: ignore[union-attr]
        except Exception:
            pass

    output_path = Path(output_path)
    output_path.parent.mkdir(parents=True, exist_ok=True)

    # 효과 유효성 검사
    valid_effects = {"fade", "slide", "zoom", "dissolve", "sequence"}
    if effect not in valid_effects:
        raise ValueError(f"알 수 없는 효과: '{effect}'")

    ffmpeg_bin = _get_ffmpeg_bin()
    concat_list_path: Optional[str] = None

    try:
        if effect == "fade":
            cmd: list[str] = _build_fade_cmd(frame_paths, output_path, size, fps, duration_per_frame, bgm_path, ffmpeg_bin)
        elif effect == "slide":
            cmd = _build_slide_cmd(frame_paths, output_path, size, fps, duration_per_frame, bgm_path, ffmpeg_bin)
        elif effect == "zoom":
            cmd = _build_zoom_cmd(frame_paths, output_path, size, fps, duration_per_frame, bgm_path, ffmpeg_bin)
        elif effect == "dissolve":
            cmd, concat_list_path = _build_dissolve_cmd(frame_paths, output_path, size, fps, duration_per_frame, bgm_path, ffmpeg_bin)
        else:  # sequence
            cmd, concat_list_path = _build_sequence_cmd(frame_paths, output_path, size, fps, duration_per_frame, bgm_path, ffmpeg_bin)

        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=300,
        )

        if result.returncode != 0:
            raise RuntimeError(
                f"ffmpeg 렌더링 실패 (returncode={result.returncode}):\n"
                f"STDERR:\n{result.stderr}\n"
                f"CMD: {' '.join(cmd)}"
            )

    finally:
        if concat_list_path and os.path.exists(concat_list_path):
            os.unlink(concat_list_path)

    return output_path
