"""Side-effect-free local preview planning from render manifests."""

from __future__ import annotations

from dataclasses import dataclass

from autoshorts.ideas.candidate import ValidationResult
from autoshorts.rendering.manifest import RenderManifest, validate_render_manifest


@dataclass(frozen=True)
class LocalPreviewFrame:
    frame_id: str
    scene_purpose: str
    start_ms: int
    duration_ms: int
    caption_text: str
    label_text: str
    asset_mode: str
    motion_instruction: str
    placeholder_image_path: str


@dataclass(frozen=True)
class LocalPreviewPlan:
    script_id: str
    title: str
    width: int
    height: int
    fps: int
    output_dir: str
    frames: tuple[LocalPreviewFrame, ...]
    side_effects: tuple[str, ...] = ()
    external_calls: tuple[str, ...] = ()


def _is_blank(value: str) -> bool:
    return not (value or "").strip()


def _frame_id(index: int, purpose: str) -> str:
    return f"frame-{index:02d}-{purpose}"


def build_local_preview_plan(
    manifest: RenderManifest,
    *,
    width: int = 540,
    height: int = 960,
    fps: int = 24,
) -> LocalPreviewPlan:
    """Build a deterministic frame plan without writing images or calling FFmpeg."""

    manifest_validation = validate_render_manifest(manifest)
    if not manifest_validation.is_valid:
        raise ValueError("; ".join(manifest_validation.errors))

    output_dir = f"data/previews/{manifest.script_id}"
    frames = tuple(
        LocalPreviewFrame(
            frame_id=_frame_id(index, item.scene_purpose),
            scene_purpose=item.scene_purpose,
            start_ms=item.start_seconds * 1000,
            duration_ms=(item.end_seconds - item.start_seconds) * 1000,
            caption_text=item.caption_layer.text,
            label_text=item.label_layer.text,
            asset_mode=item.asset_mode,
            motion_instruction=item.motion_instruction,
            placeholder_image_path=f"{output_dir}/{_frame_id(index, item.scene_purpose)}.png",
        )
        for index, item in enumerate(manifest.timeline, start=1)
    )
    plan = LocalPreviewPlan(
        script_id=manifest.script_id,
        title=manifest.title,
        width=width,
        height=height,
        fps=fps,
        output_dir=output_dir,
        frames=frames,
        side_effects=(),
        external_calls=(),
    )
    validation = validate_local_preview_plan(plan)
    if not validation.is_valid:
        raise ValueError("; ".join(validation.errors))
    return plan


def validate_local_preview_plan(plan: LocalPreviewPlan) -> ValidationResult:
    errors: list[str] = []
    if _is_blank(plan.script_id):
        errors.append("script_id is required")
    if _is_blank(plan.title):
        errors.append("title is required")
    if plan.width <= 0 or plan.height <= 0 or plan.height <= plan.width:
        errors.append("preview size must be vertical")
    if plan.fps <= 0:
        errors.append("fps must be positive")
    if _is_blank(plan.output_dir):
        errors.append("output_dir is required")
    if plan.side_effects:
        errors.append("preview plan must not have side effects")
    if plan.external_calls:
        errors.append("preview plan must not call external tools")
    if not plan.frames:
        errors.append("at least one preview frame is required")

    previous_end: int | None = None
    for index, frame in enumerate(plan.frames, start=1):
        for field_name, value in {
            "frame_id": frame.frame_id,
            "scene_purpose": frame.scene_purpose,
            "caption_text": frame.caption_text,
            "label_text": frame.label_text,
            "asset_mode": frame.asset_mode,
            "motion_instruction": frame.motion_instruction,
            "placeholder_image_path": frame.placeholder_image_path,
        }.items():
            if _is_blank(value):
                errors.append(f"frame {index} {field_name} is required")
        if frame.duration_ms <= 0:
            errors.append(f"frame {index} duration_ms must be positive")
        if index == 1 and frame.start_ms != 0:
            errors.append("first frame must start at 0ms")
        if previous_end is not None and frame.start_ms < previous_end:
            errors.append(f"frame {index} start_ms overlaps previous frame")
        previous_end = frame.start_ms + frame.duration_ms

    return ValidationResult(is_valid=not errors, errors=tuple(errors))
