"""
VidPuls Artist Pipeline – wie 7ART / AI-Artist-Generatoren:

1. Feste Charaktere (Sarah, Laura) mit Referenzbildern
2. Pro Dialogzeile: KI-Video-Clip (PixVerse Bild→Video) aus Keyframe
3. ElevenLabs-Sprache + leise Musik darüber
4. Fallback: 3D-Zwei-Personen-Szene ohne PixVerse
"""

from __future__ import annotations

import json
import subprocess
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
CONFIG = ROOT / "data" / "vidpuls-config.json"


def _load_config() -> dict:
    if not CONFIG.is_file():
        return {}
    try:
        return json.loads(CONFIG.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError):
        return {}


def dialogue_engine() -> str:
    import os

    env = os.environ.get("VIDPULS_DIALOGUE_ENGINE", "").strip().lower()
    if env in ("cinema3d", "artist", "scene", "static"):
        return env
    val = str(_load_config().get("dialogue_engine") or "cinema3d").lower()
    if val in ("cinema3d", "artist", "scene", "static"):
        return val
    return "cinema3d"


def _cinema3d_ready() -> bool:
    from jam.cinema3d import available

    return available()


def _find_ffmpeg() -> str | None:
    from jam.tiktok_remix import _find_ffmpeg

    return _find_ffmpeg()


def _probe(path: Path) -> float:
    from jam.ia1minute import _probe_media_duration

    return _probe_media_duration(path)


def _pixverse_ready() -> bool:
    from jam.pixverse_api import api_key

    return bool(api_key())


def _shot_prompt(
    speaker: str,
    *,
    line: str = "",
    idle: bool = False,
) -> str:
    from jam.artist_characters import get_character, load_cast

    cast = load_cast()
    sp = get_character(speaker)
    other_id = "laura" if sp.id == "sarah" else "sarah"
    other = cast.get(other_id) or get_character(other_id)
    if idle:
        return (
            f"3D CGI animated movie, Pixar trickfilm style. "
            f"{sp.name} and {other.name} stand facing each other in a cozy apartment. "
            "Subtle idle animation, breathing, blinking, soft camera drift, cinematic lighting, "
            "vertical 9:16, no text"
        )
    snippet = (line or "")[:120].strip()
    return (
        f"3D CGI animated movie scene, Pixar trickfilm. "
        f"{sp.prompt_anchor()} speaks expressively to {other.name} ({other.prompt_anchor()}). "
        f"They face each other across a coffee table. Natural talking motion, lip sync, hand gestures, "
        f"emotion: {snippet or 'friendly conversation'}. "
        "Smooth cinematic camera, warm window light, vertical 9:16, no text, no watermark"
    )


def _pixverse_duration(target_sec: float) -> int:
    if target_sec <= 5.5:
        return 5
    if target_sec <= 8.5:
        return 8
    return 10


def _generate_pixverse_clip(
    keyframe: Path,
    out: Path,
    *,
    prompt: str,
    target_sec: float,
    aspect_ratio: str = "9:16",
    quality: str = "1080p",
) -> bool:
    from jam import pixverse_api

    if not keyframe.is_file():
        return False
    out.parent.mkdir(parents=True, exist_ok=True)
    try:
        img_id = pixverse_api.upload_image(keyframe)
        dur = _pixverse_duration(target_sec)
        vid = pixverse_api.image_to_video(
            img_id,
            prompt,
            aspect_ratio=aspect_ratio,
            duration=dur,
            model=str(_load_config().get("pixverse_model") or "v5.6"),
            quality=quality,
            motion_mode="normal",
            negative_prompt="blurry, low quality, text, watermark, distorted face, 2D flat",
        )
        result = pixverse_api.wait_for_video(vid, timeout_sec=900)
        url = str(result.get("url") or "")
        if not url:
            return False
        pixverse_api.download_video(url, out)
        return out.is_file() and out.stat().st_size > 5000
    except (RuntimeError, OSError, TimeoutError):
        return False


def _fit_clip(
    ff: str,
    src: Path,
    dest: Path,
    *,
    duration: float,
    width: int,
    height: int,
) -> bool:
    from jam.gpu_render import ffmpeg_encode_args

    dest.parent.mkdir(parents=True, exist_ok=True)
    src_dur = _probe(src)
    dur = max(0.35, duration)
    vf = f"scale={width}:{height}:flags=lanczos,setsar=1,format=yuv420p"
    enc = ffmpeg_encode_args(crf_fallback="18") + ["-pix_fmt", "yuv420p"]
    if src_dur >= dur - 0.05:
        cmd = [ff, "-y", "-i", str(src), "-t", f"{dur:.3f}", "-vf", vf, "-an", *enc, str(dest)]
    else:
        cmd = [
            ff, "-y", "-stream_loop", "-1", "-i", str(src), "-t", f"{dur:.3f}",
            "-vf", vf, "-an", *enc, str(dest),
        ]
    try:
        subprocess.run(cmd, capture_output=True, timeout=180, check=True)
        return dest.is_file() and dest.stat().st_size > 500
    except (subprocess.CalledProcessError, subprocess.TimeoutExpired, OSError):
        return False


def _static_segment(
    ff: str,
    frame: Path,
    dest: Path,
    *,
    duration: float,
    width: int,
    height: int,
    visual_style: str,
) -> bool:
    from jam.dialogue_scene import _segment_video_filter

    compose_w = min(width, 1080)
    compose_h = int(height * (compose_w / width))
    if compose_h % 2:
        compose_h += 1
    fps = 24
    dur = max(0.35, duration)
    active = "sarah" if "sarah" in frame.name else ("laura" if "laura" in frame.name else None)
    vf = _segment_video_filter(
        compose_w=compose_w,
        compose_h=compose_h,
        dur=dur,
        fps=fps,
        active=active,
        speaker=active or "neutral",
        visual_style=visual_style,
    )
    from jam.gpu_render import ffmpeg_encode_args

    cmd = [
        ff, "-y", "-loop", "1", "-i", str(frame), "-t", f"{dur:.3f}",
        "-vf", vf, "-r", str(fps), "-an",
        *ffmpeg_encode_args(crf_fallback="20"),
        "-pix_fmt", "yuv420p",
        str(dest),
    ]
    try:
        subprocess.run(cmd, capture_output=True, timeout=120, check=True)
        if compose_w != width or compose_h != height:
            tmp = dest.with_suffix(".scaled.mp4")
            subprocess.run(
                [
                    ff, "-y", "-i", str(dest),
                    "-vf", f"scale={width}:{height}:flags=lanczos,format=yuv420p",
                    *ffmpeg_encode_args(crf_fallback="18"),
                    str(tmp),
                ],
                capture_output=True, timeout=120, check=True,
            )
            if tmp.is_file():
                tmp.replace(dest)
        return dest.is_file() and dest.stat().st_size > 500
    except (subprocess.CalledProcessError, subprocess.TimeoutExpired, OSError):
        return False


def assemble_artist_dialogue(
    tracks: list,
    audio: Path,
    out: Path,
    *,
    width: int,
    height: int,
    visual_style: str = "3d",
    work_dir: Path | None = None,
    turns: list[tuple[str, str]] | None = None,
    aspect_ratio: str = "9:16",
) -> tuple[bool, str]:
    ff = _find_ffmpeg()
    if not ff or not audio.is_file() or not tracks:
        return False, "Artist: ffmpeg/Audio/Tracks fehlen"

    work = work_dir or audio.parent
    from jam.artist_characters import ensure_character_refs
    from jam.dialogue_scene import (
        _frame_path,
        _speaker_frame_key,
        prepare_scene_assets,
    )

    ensure_character_refs()
    assets = prepare_scene_assets(work, size=(width, height), visual_style=visual_style)
    audio_dur = _probe(audio)

    turn_lines: list[tuple[str, str]] = list(turns or [])
    shots_dir = work / "artist_shots"
    shots_dir.mkdir(parents=True, exist_ok=True)
    fitted: list[Path] = []
    used_pixverse = 0
    quality = "1080p" if width >= 1920 else "720p"

    shot_plan: list[tuple[str, float, str, Path | None]] = []
    for i, tr in enumerate(tracks):
        line = turn_lines[i][1] if i < len(turn_lines) else ""
        shot_plan.append((str(tr.speaker).lower(), float(tr.duration), line, tr.path))
        pause = float(getattr(tr, "pause_after", 0.0) or 0.0)
        if pause > 0.04:
            shot_plan.append(("neutral", pause, "", None))

    vis_dur = sum(d for _, d, _, _ in shot_plan)
    if audio_dur > 1.0 and vis_dur > 0.5 and abs(audio_dur - vis_dur) > 0.35:
        ratio = audio_dur / vis_dur
        shot_plan = [(sp, d * ratio, ln, ap) for sp, d, ln, ap in shot_plan]

    cinema_shots = 0
    for i, (speaker, dur, line, audio_part) in enumerate(shot_plan):
        active = _speaker_frame_key(speaker)
        frame = _frame_path(assets, active)
        fitted_path = shots_dir / f"shot_{i:03d}.mp4"
        raw_path = shots_dir / f"raw_{i:03d}.mp4"
        ok = False

        if _cinema3d_ready():
            from jam.cinema3d import render_dialogue_shot

            if render_dialogue_shot(
                speaker, fitted_path,
                duration=dur, width=width, height=height,
                audio_part=audio_part if speaker != "neutral" else None,
                visual_style=visual_style,
                assets_dir=assets,
            ):
                ok = True
                cinema_shots += 1

        if not ok and _pixverse_ready() and frame.is_file():
            prompt = _shot_prompt(
                speaker if speaker != "neutral" else "sarah",
                line=line,
                idle=(speaker == "neutral"),
            )
            if not raw_path.is_file() or raw_path.stat().st_size < 5000:
                if _generate_pixverse_clip(
                    frame, raw_path, prompt=prompt, target_sec=dur,
                    aspect_ratio=aspect_ratio, quality=quality,
                ):
                    used_pixverse += 1
            if raw_path.is_file() and raw_path.stat().st_size > 5000:
                ok = _fit_clip(ff, raw_path, fitted_path, duration=dur, width=width, height=height)

        if not ok:
            ok = _static_segment(
                ff, frame, fitted_path,
                duration=dur, width=width, height=height, visual_style=visual_style,
            )
        if ok:
            fitted.append(fitted_path)

    if not fitted:
        return False, "Artist: keine Shots erzeugt"

    list_file = work / "artist_concat.txt"
    list_file.write_text(
        "\n".join(f"file '{p.resolve().as_posix()}'" for p in fitted),
        encoding="utf-8",
    )
    silent = work / "artist_silent.mp4"
    try:
        subprocess.run(
            [ff, "-y", "-f", "concat", "-safe", "0", "-i", str(list_file), "-c", "copy", str(silent)],
            capture_output=True, timeout=300, check=True,
        )
    except (subprocess.CalledProcessError, subprocess.TimeoutExpired, OSError) as e:
        return False, f"Artist concat: {e}"[:120]

    out.parent.mkdir(parents=True, exist_ok=True)
    cmd = [
        ff, "-y", "-i", str(silent), "-i", str(audio),
        "-map", "0:v", "-map", "1:a",
        "-c:v", "copy",
        "-c:a", "aac", "-b:a", "192k",
        "-t", f"{audio_dur:.3f}",
        "-movflags", "+faststart", str(out),
    ]
    try:
        subprocess.run(cmd, capture_output=True, timeout=600, check=True)
    except (subprocess.CalledProcessError, subprocess.TimeoutExpired, OSError) as e:
        return False, f"Artist mux: {e}"[:120]

    from jam.ia1minute import _finalize_mp4

    _finalize_mp4(ff, out)
    if out.is_file() and out.stat().st_size > 5000:
        mode = f"Cinema3D ({cinema_shots}/{len(shot_plan)} Shots"
        if used_pixverse:
            mode += f" + {used_pixverse} PixVerse"
        mode += f") · {audio_dur:.0f}s · {width}×{height}"
        return True, mode
    return False, "Artist Export leer"


def assemble_dialogue_video(
    tracks: list,
    audio: Path,
    out: Path,
    *,
    width: int,
    height: int,
    visual_style: str = "3d",
    work_dir: Path | None = None,
    turns: list[tuple[str, str]] | None = None,
    aspect_ratio: str = "9:16",
) -> tuple[bool, str]:
    engine = dialogue_engine()
    if engine in ("artist", "cinema3d"):
        ok, msg = assemble_artist_dialogue(
            tracks, audio, out,
            width=width, height=height, visual_style=visual_style,
            work_dir=work_dir, turns=turns, aspect_ratio=aspect_ratio,
        )
        if ok:
            return ok, msg
    from jam.dialogue_scene import assemble_two_shot_dialogue

    return assemble_two_shot_dialogue(
        tracks, audio, out,
        width=width, height=height, visual_style=visual_style, work_dir=work_dir,
    )
