"""Deterministic preview renderer for AutoShortsBot render manifests.

This v1 renderer intentionally uses manifest-driven cards/placeholders instead of
AI-generated video. It renders a simple vertical MP4 suitable for timing and
approval review before investing in polished assets.
"""

from __future__ import annotations

import shutil
import subprocess
import tempfile
import textwrap
from pathlib import Path
from typing import Any

from PIL import Image, ImageDraw, ImageFont

BACKGROUND_BY_TYPE = {
    "kinetic_text": ((19, 23, 38), (79, 70, 229)),
    "screen_recording": ((15, 23, 42), (14, 165, 233)),
    "card_animation": ((17, 24, 39), (245, 158, 11)),
    "stock_broll": ((28, 25, 23), (34, 197, 94)),
    "generated_background": ((24, 24, 27), (168, 85, 247)),
}


def _font(size: int, bold: bool = False) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
    candidates = [
        "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" if bold else "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
        "/usr/share/fonts/truetype/liberation2/LiberationSans-Bold.ttf" if bold else "/usr/share/fonts/truetype/liberation2/LiberationSans-Regular.ttf",
    ]
    for path in candidates:
        if Path(path).exists():
            return ImageFont.truetype(path, size=size)
    return ImageFont.load_default()


def _wrap_text(text: str, width_chars: int) -> str:
    return "\n".join(textwrap.wrap(" ".join(text.split()), width=width_chars))


def _text_block_size(draw: ImageDraw.ImageDraw, text: str, font: Any, spacing: int) -> tuple[int, int]:
    lines = text.splitlines() or [""]
    boxes = [draw.textbbox((0, 0), line, font=font) for line in lines]
    width = max((box[2] - box[0] for box in boxes), default=0)
    height = sum(box[3] - box[1] for box in boxes) + spacing * max(0, len(lines) - 1)
    return int(width), int(height)


def _fit_text_block(
    draw: ImageDraw.ImageDraw,
    text: str,
    max_width: int,
    max_height: int,
    max_lines: int,
    preferred_size: int,
    min_size: int,
    bold: bool,
) -> dict[str, Any]:
    """Choose a readable font size and wrapping that fits a text box."""
    cleaned = " ".join(text.split())
    for size in range(preferred_size, min_size - 1, -1):
        wrap_chars = max(10, round(max_width / (size * 0.54)))
        wrapped = _wrap_text(cleaned, wrap_chars)
        lines = wrapped.splitlines()
        font = _font(size, bold=bold)
        spacing = max(4, round(size * 0.25))
        block_width, block_height = _text_block_size(draw, wrapped, font, spacing)
        if len(lines) <= max_lines and block_width <= max_width and block_height <= max_height:
            return {
                "text": wrapped,
                "font": font,
                "font_size": size,
                "line_count": len(lines),
                "spacing": spacing,
            }

    fallback_font = _font(min_size, bold=bold)
    fallback_chars = max(10, round(max_width / (min_size * 0.54)))
    fallback = _wrap_text(cleaned, fallback_chars)
    return {
        "text": fallback,
        "font": fallback_font,
        "font_size": min_size,
        "line_count": len(fallback.splitlines()),
        "spacing": max(4, round(min_size * 0.25)),
    }


def _gradient(width: int, height: int, start: tuple[int, int, int], end: tuple[int, int, int]) -> Image.Image:
    image = Image.new("RGB", (width, height), start)
    draw = ImageDraw.Draw(image)
    for y in range(height):
        ratio = y / max(1, height - 1)
        color = tuple(round(start[i] * (1 - ratio) + end[i] * ratio) for i in range(3))
        draw.line([(0, y), (width, y)], fill=color)
    return image


def _draw_multiline_center(
    draw: ImageDraw.ImageDraw,
    xy: tuple[int, int],
    text: str,
    font: Any,
    fill: tuple[int, int, int],
    spacing: int = 8,
) -> None:
    lines = text.splitlines()
    line_boxes = [draw.textbbox((0, 0), line, font=font) for line in lines]
    line_heights = [box[3] - box[1] for box in line_boxes]
    total_height = sum(line_heights) + spacing * max(0, len(lines) - 1)
    y = xy[1] - total_height // 2
    for line, box, line_height in zip(lines, line_boxes, line_heights):
        line_width = box[2] - box[0]
        draw.text((xy[0] - line_width // 2, y), line, font=font, fill=fill)
        y += line_height + spacing


def _render_scene_image(
    item: dict[str, Any], manifest: dict[str, Any], width: int, height: int
) -> tuple[Image.Image, dict[str, int]]:
    start, end = BACKGROUND_BY_TYPE.get(item["type"], ((24, 24, 27), (64, 64, 64)))
    image = _gradient(width, height, start, end)
    draw = ImageDraw.Draw(image)

    margin = round(width * 0.075)
    title_font = _font(max(18, round(width * 0.06)), bold=True)
    label_font = _font(max(12, round(width * 0.032)), bold=False)
    small_font = _font(max(12, round(width * 0.034)), bold=False)

    # Safe area and content card.
    card_top = round(height * 0.18)
    card_bottom = round(height * 0.78)
    draw.rounded_rectangle(
        [margin, card_top, width - margin, card_bottom],
        radius=round(width * 0.04),
        fill=(255, 255, 255),
        outline=(226, 232, 240),
        width=max(2, width // 180),
    )

    draw.text((margin, round(height * 0.055)), manifest["video"]["id"], font=label_font, fill=(226, 232, 240))
    draw.text((margin, round(height * 0.09)), item["type"].replace("_", " ").upper(), font=title_font, fill=(255, 255, 255))

    scene_label = item["id"].replace("_", " ").upper()
    draw.text((margin * 1.4, card_top + margin), scene_label, font=small_font, fill=(71, 85, 105))

    main_fit = _fit_text_block(
        draw,
        item["text"],
        max_width=width - margin * 4,
        max_height=round(height * 0.24),
        max_lines=4,
        preferred_size=max(22, round(width * 0.078)),
        min_size=max(18, round(width * 0.063)),
        bold=True,
    )
    _draw_multiline_center(
        draw,
        (width // 2, round(height * 0.45)),
        main_fit["text"],
        main_fit["font"],
        (15, 23, 42),
        spacing=main_fit["spacing"],
    )

    retention_fit = _fit_text_block(
        draw,
        item.get("retention_goal", ""),
        max_width=width - margin * 4,
        max_height=round(height * 0.14),
        max_lines=3,
        preferred_size=max(14, round(width * 0.048)),
        min_size=max(12, round(width * 0.041)),
        bold=False,
    )
    _draw_multiline_center(
        draw,
        (width // 2, round(height * 0.69)),
        retention_fit["text"],
        retention_fit["font"],
        (71, 85, 105),
        spacing=retention_fit["spacing"],
    )

    timing = f"{item['start_ms'] / 1000:.1f}s – {(item['start_ms'] + item['duration_ms']) / 1000:.1f}s"
    draw.text((margin, height - round(height * 0.075)), timing, font=label_font, fill=(226, 232, 240))
    draw.text((width - margin - draw.textlength("PREVIEW", font=label_font), height - round(height * 0.075)), "PREVIEW", font=label_font, fill=(226, 232, 240))
    return image, {
        "main_font_size": main_fit["font_size"],
        "main_line_count": main_fit["line_count"],
        "retention_font_size": retention_fit["font_size"],
        "retention_line_count": retention_fit["line_count"],
    }


def build_preview_frames(manifest: dict[str, Any], width: int = 540, height: int = 960) -> list[dict[str, Any]]:
    """Build one representative image per visual timeline item."""
    visual_items = [item for item in manifest["timeline"] if item["track"] == "visual"]
    frames: list[dict[str, Any]] = []
    for item in visual_items:
        image, typography = _render_scene_image(item, manifest, width, height)
        frames.append(
            {
                "scene_id": item["id"],
                "start_ms": item["start_ms"],
                "duration_ms": item["duration_ms"],
                "text": item["text"],
                "typography": typography,
                "image": image,
            }
        )
    return frames


def write_frame_images(frames: list[dict[str, Any]], output_dir: Path) -> list[Path]:
    """Write preview frames as numbered PNG files."""
    output_dir.mkdir(parents=True, exist_ok=True)
    paths: list[Path] = []
    for index, frame in enumerate(frames, start=1):
        path = output_dir / f"frame_{index:03d}_{frame['scene_id']}.png"
        frame["image"].save(path)
        paths.append(path)
    return paths


def _write_concat_file(frames: list[dict[str, Any]], image_paths: list[Path], concat_path: Path) -> None:
    lines: list[str] = []
    for frame, image_path in zip(frames, image_paths):
        seconds = max(0.5, frame["duration_ms"] / 1000)
        lines.append(f"file '{image_path.as_posix()}'")
        lines.append(f"duration {seconds:.3f}")
    if image_paths:
        lines.append(f"file '{image_paths[-1].as_posix()}'")
    concat_path.write_text("\n".join(lines) + "\n", encoding="utf-8")


def render_preview_video(
    manifest: dict[str, Any],
    output_path: Path,
    width: int = 540,
    height: int = 960,
    fps: int = 24,
) -> dict[str, Any]:
    """Render a manifest-driven preview MP4 using Pillow frames and ffmpeg."""
    ffmpeg = shutil.which("ffmpeg")
    if not ffmpeg:
        raise RuntimeError("ffmpeg is required to render preview MP4 files")

    output_path.parent.mkdir(parents=True, exist_ok=True)
    frames = build_preview_frames(manifest, width=width, height=height)
    with tempfile.TemporaryDirectory(prefix="autoshorts_preview_") as tmp:
        tmp_dir = Path(tmp)
        image_paths = write_frame_images(frames, tmp_dir / "frames")
        concat_path = tmp_dir / "concat.txt"
        _write_concat_file(frames, image_paths, concat_path)
        command = [
            ffmpeg,
            "-y",
            "-f",
            "concat",
            "-safe",
            "0",
            "-i",
            str(concat_path),
            "-vf",
            f"fps={fps},format=yuv420p",
            "-movflags",
            "+faststart",
            str(output_path),
        ]
        completed = subprocess.run(command, check=True, capture_output=True, text=True)
    return {
        "output_path": output_path,
        "frame_count": len(frames),
        "fps": fps,
        "stderr": completed.stderr,
    }
