"""Scene + VideoSpec dataclasses + YAML spec parser.

Used by:
  - adapters.py — content adapters return list[Scene] for engine to render
  - engine.py   — render loop iterates scenes
  - Phase 2+    — YAML specs in demos/specs/ replace demos/*.mjs
"""

from __future__ import annotations

from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Literal

# ── Scene action types ───────────────────────────────────────────
# Every scene is a sequence of actions. Most user-facing videos
# only need {navigate, scroll, caption, wait_for_audio}. Demo
# specs may also use {nav_click, click, type, wait}.

ActionType = Literal[
    "navigate",         # open(url): URL relative to BASE_URL or absolute
    "scroll",           # scroll(selector_or_y, dwell_seconds)
    "caption",          # caption(text, dwell_seconds): inject DOM caption
    "wait_for_audio",   # block until current scene's audio finishes
    "nav_click",        # click sidebar nav item by aria-label/data-nav/text
    "click",            # generic click (demo specs only)
    "type",             # type into selector (demo specs only)
    "wait",             # wait(milliseconds) — sleep for fixed duration
]


@dataclass
class Action:
    """Single primitive operation inside a Scene."""

    type: ActionType
    target: str | int = ""        # url / selector / text / y-offset / ms
    dwell_seconds: float = 0.0    # how long to hold this action
    text: str = ""                # for caption/type

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> Action:
        return cls(
            type=data["type"],
            target=data.get("target", "") if "target" in data else data.get("url", "") or data.get("selector", "") or data.get("to", "") or 0,
            dwell_seconds=float(data.get("dwell_seconds", 0.0)),
            text=data.get("text", ""),
        )


@dataclass
class Scene:
    """One scene = one narration + a sequence of visual actions.

    The engine iterates scenes in order. Each scene typically:
      1. Performs visual actions (navigate, scroll, caption)
      2. Waits for the scene's audio clip to play in full (`wait_for_audio`)
      3. Moves on to the next scene
    """

    id: str
    narration: str
    actions: list[Action] = field(default_factory=list)
    focus: str = ""               # short hint for caption/UI ("intro", "key takeaways")

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> Scene:
        return cls(
            id=data["id"],
            narration=data.get("narration", ""),
            actions=[Action.from_dict(a) for a in data.get("actions", [])],
            focus=data.get("focus", ""),
        )


@dataclass
class VideoSpec:
    """Top-level video definition. Shape mirrors a YAML spec file.

    Either provided directly by content adapters OR loaded from a YAML
    file (Phase 2+ demo migration).
    """

    name: str
    target: str                   # original target string ("wiki:slug" / etc.)
    style: str = "walkthrough"
    voice: str = "liam"
    duration_seconds: int = 60
    viewport: tuple[int, int] = (1440, 900)
    scenes: list[Scene] = field(default_factory=list)
    output_dir: Path | None = None
    output_name: str = ""         # filename stem (without .mp4); auto-generated if empty


def load_spec_yaml(path: Path) -> VideoSpec:
    """Load a YAML spec from disk (Phase 2+ for demo migration)."""

    raise NotImplementedError("Phase 2+ — YAML spec loading not yet implemented")


def validate_spec(spec: VideoSpec, max_scenes: int = 12, max_seconds: int = 180) -> list[str]:
    """Pure validation — return list of warnings/errors. Empty = OK."""

    issues: list[str] = []
    if not spec.scenes:
        issues.append("spec has zero scenes")
    if len(spec.scenes) > max_scenes:
        issues.append(f"spec has {len(spec.scenes)} scenes; max is {max_scenes}")
    if spec.duration_seconds > max_seconds:
        issues.append(f"duration {spec.duration_seconds}s exceeds cap {max_seconds}s")
    seen_ids: set[str] = set()
    for sc in spec.scenes:
        if sc.id in seen_ids:
            issues.append(f"duplicate scene id: {sc.id}")
        seen_ids.add(sc.id)
        if not sc.narration.strip():
            issues.append(f"scene {sc.id!r} has empty narration")
    return issues
