"""Production stack — the EXPLICIT, operator-chosen lane selection for a production.

The director plans shot CONTENT (subject, narration, mood, keyframe prompts); it must
NOT silently pick the video/image models. WHICH video lane, WHICH keyframe lane, the
continuity mode, and whether narration/music are present is an operator decision —
defaulted-but-visible, overridable (CLI flags / OWUI valves), validated against what
the production executor can actually render, and recorded in the manifest.

Two registries declare what's selectable AND what the executor renders TODAY:
  VIDEO_LANES    — the one pinned video model for every shot.
  KEYFRAME_LANES — the image model for continuity keyframes (storyboard / hero).

`wired` = lanes.py can render it through the production agent right now. The interactive
OWUI Studio lanes can drive more models than the production agent has wired (e.g. the
LTX-family video lanes are generated by a checkpoint-swap graph in build_studio_pipe.py
that the production executor hasn't ported yet) — so we keep the production-renderable
set honest: an operator is never offered a choice that errors at render time. Picking an
unwired lane raises a clear StackError that points at the interactive lane instead.

stdlib-only (dataclasses) — imported by schema.py / planner.py with no cycle.
"""
from __future__ import annotations

from dataclasses import dataclass


class StackError(ValueError):
    """A stack selection that can't be honoured (unknown / unwired lane, bad combo)."""


# -- video lanes — one pinned lane per production -----------------------------
#   wired = the production executor (lanes.py) renders it today.
#   i2v   = the lane has an image->video workflow (every continuity mode needs i2v).
VIDEO_LANES: dict[str, dict] = {
    "wan": {
        "label": "Wan2.2 (uncensored · 832×480 · no native audio)",
        "wired": True, "i2v": True,
    },
    "ltx": {
        "label": "LTX-2.3 (768×512 · the distilled base)",
        "wired": True, "i2v": True,
    },
    "sulphur": {
        "label": "Sulphur (uncensored LTX-2.3 dev fine-tune · 1280×720)",
        "wired": True, "i2v": True,
    },
    "10eros": {
        "label": "10Eros (uncensored LTX-2.3 dev fine-tune · 1280×720)",
        "wired": True, "i2v": True,
    },
}

# -- keyframe (image) lanes — the continuity-quality lever --------------------
#   Keyframe prompts are PROSE (style bible + shot intent). chroma/zimage/krea/hidream
#   take natural-language prompts; ideogram is trained on STRUCTURED JSON captions and
#   emits a "safety filter" placeholder for prose — so it's a title-card/design lane,
#   NOT a continuity keyframe lane (wired=False; matches its label).
#
#   `tier` frames the choice (all 4 live-validated 2026-06-28): the everyday default is
#   a fast lane (chroma is the conservative default; zimage is the fastest); HiDream is
#   the quality/hero-frame option but is NOT the default — Wan downscales 832×480 and
#   reinterprets, so its native 2560×1440 detail is largely compressed away and ~1 min/kf
#   is too costly for everyday storyboard iteration. krea is the aesthetic/stylized option.
KEYFRAME_LANES: dict[str, dict] = {
    "chroma": {"label": "Chroma (uncensored · strong general keyframes)",
               "wired": True, "tier": "default"},
    "zimage": {"label": "Z-Image-Turbo (uncensored · fast 8-step turbo)",
               "wired": True, "tier": "fast"},
    "krea": {"label": "Krea 2 (aesthetic / stylized)", "wired": True, "tier": "aesthetic"},
    "hidream": {"label": "HiDream-O1 (top quality / hero frames · slow ~1 min/kf, native 2560×1440)",
                "wired": True, "tier": "quality"},
    "ideogram": {"label": "Ideogram-4 (design / text / title-card — needs JSON, not "
                          "prose → not a continuity keyframe lane)", "wired": False, "tier": None},
}

DEFAULT_VIDEO = "wan"
DEFAULT_KEYFRAME = "chroma"
DEFAULT_CONTINUITY = "storyboard"
CONTINUITY_MODES = ("none", "chain", "hero", "storyboard")
# continuity modes that need an image->video (i2v) workflow on the pinned video lane
_I2V_CONTINUITY = ("chain", "hero", "storyboard")


def wired_video_lanes() -> list[str]:
    return [k for k, v in VIDEO_LANES.items() if v["wired"]]


def wired_keyframe_lanes() -> list[str]:
    return [k for k, v in KEYFRAME_LANES.items() if v["wired"]]


@dataclass
class ProductionStack:
    """The resolved, validated stack a production runs on (recorded in the manifest)."""
    video_lane: str = DEFAULT_VIDEO
    keyframe_lane: str = DEFAULT_KEYFRAME
    continuity: str = DEFAULT_CONTINUITY
    narration: bool = True
    music: bool = True
    voice: str = ""        # Kokoro voice id ("" => service default af_heart)
    auto: bool = False     # True when the operator pinned no lane (planner defaults applied)

    def to_dict(self) -> dict:
        return {
            "video_lane": self.video_lane, "keyframe_lane": self.keyframe_lane,
            "continuity": self.continuity, "narration": self.narration,
            "music": self.music, "voice": self.voice, "auto": self.auto,
        }


def _norm(v) -> str | None:
    """'' / None / 'auto' -> None (use the default); else a lower-cased token."""
    if v is None:
        return None
    s = str(v).strip().lower()
    return None if s in ("", "auto") else s


def resolve_stack(*, video_lane=None, keyframe_lane=None, continuity=None,
                  narration=True, music=True, voice="") -> ProductionStack:
    """Fill 'auto'/None -> defaults, validate against what the executor renders.

    Raises StackError (with a clear message + the renderable set) on any selection
    the production executor can't honour. `auto=True` is recorded when the operator
    pinned NONE of the three lane dimensions, so a UI can show "Auto" and still
    display the resolved stack.
    """
    raw_v, raw_k, raw_c = _norm(video_lane), _norm(keyframe_lane), _norm(continuity)
    auto = raw_v is None and raw_k is None and raw_c is None
    v = raw_v or DEFAULT_VIDEO
    k = raw_k or DEFAULT_KEYFRAME
    c = raw_c or DEFAULT_CONTINUITY

    if v not in VIDEO_LANES:
        raise StackError(f"unknown video lane {v!r}; choose from {sorted(VIDEO_LANES)}")
    if not VIDEO_LANES[v]["wired"]:
        raise StackError(
            f"video lane {v!r} is a known studio lane but not yet wired into the "
            f"production executor. Renders today: {wired_video_lanes()}. "
            f"(Use the interactive '{v}' Studio lane in OWUI for one-off clips.)"
        )
    if k not in KEYFRAME_LANES:
        raise StackError(f"unknown keyframe lane {k!r}; choose from {sorted(KEYFRAME_LANES)}")
    if not KEYFRAME_LANES[k]["wired"]:
        raise StackError(
            f"keyframe lane {k!r} not wired; renders today: {wired_keyframe_lanes()}")
    if c not in CONTINUITY_MODES:
        raise StackError(f"unknown continuity {c!r}; choose from {list(CONTINUITY_MODES)}")
    if c in _I2V_CONTINUITY and not VIDEO_LANES[v]["i2v"]:
        raise StackError(
            f"continuity {c!r} needs image→video (i2v) but video lane {v!r} has no i2v "
            f"workflow. Use continuity='none' or pick a video lane with i2v."
        )
    return ProductionStack(
        video_lane=v, keyframe_lane=k, continuity=c,
        narration=bool(narration), music=bool(music), voice=voice or "", auto=auto,
    )


def stack_from_plan(plan) -> ProductionStack:
    """Best-effort reconstruction of the stack a (possibly static) plan encodes.

    Used to record a stack in the manifest when one wasn't explicitly resolved (the
    static-plan CLI path). Duck-types the plan (no schema import → no cycle).
    """
    proj = plan.project
    pol = getattr(proj, "image_policy", None) or {}
    kf = ""
    for key in ("storyboard_keyframe_lane", "hero_keyframe_lane"):
        if pol.get(key):
            kf = pol[key]
            break
    if not kf and getattr(plan, "asset_tasks", None):
        kf = plan.asset_tasks[0].lane
    narration = any((getattr(s, "narration", "") or "").strip() for s in plan.shots)
    return ProductionStack(
        video_lane=proj.video_lane, keyframe_lane=kf or DEFAULT_KEYFRAME,
        continuity=getattr(proj, "continuity", "none"),
        narration=narration, music=bool(getattr(plan, "music", None)),
        voice="", auto=False,
    )


def describe_stack(stack: ProductionStack) -> str:
    """The 'Using stack:' block — shared by CLI echo, OWUI status, the manifest note."""
    vl = VIDEO_LANES[stack.video_lane]["label"]
    has_kf = stack.continuity != "none"
    kf_label = KEYFRAME_LANES[stack.keyframe_lane]["label"] if has_kf else "— (t2v, no keyframes)"
    audio = [
        (f"Kokoro narration ({stack.voice or 'af_heart'})" if stack.narration else "no narration"),
        ("ACE-Step music" if stack.music else "no music"),
    ]
    head = "Using stack"
    if stack.auto:
        head += " (auto defaults — override with --video-lane / --keyframe-lane / --continuity)"
    return (
        f"{head}:\n"
        f"  video:      {stack.video_lane}  — {vl}\n"
        f"  keyframes:  {(stack.keyframe_lane if has_kf else '—')}  — {kf_label}\n"
        f"  continuity: {stack.continuity}\n"
        f"  audio:      {', '.join(audio)}"
    )


def lane_help() -> str:
    """Compact lane menu for the CLI --help epilog / OWUI valve docs."""
    vids = " · ".join(f"{k}{'' if v['wired'] else ' (roadmap)'}" for k, v in VIDEO_LANES.items())
    kfs = " · ".join(
        f"{k} [{v['tier']}]" if v.get("tier") else f"{k} (not for continuity)"
        for k, v in KEYFRAME_LANES.items()
    )
    return (
        f"video lanes:    {vids}\n"
        f"keyframe lanes: {kfs}\n"
        f"                (default=chroma · fast=zimage · quality=hidream · aesthetic=krea)\n"
        f"continuity:     {' · '.join(CONTINUITY_MODES)}  (storyboard = default)"
    )
