"""The player state machine: a stream of Controls -> playback transitions.

Pure decision logic — no mpv, no audio, no serial. Each update() resolves the
desired Playback (which neutral base clip, how it is altered, what audio plays,
at what levels) and returns the Transition from the previous Playback. The
transition KIND encodes design §4.3: the Dark/Light grade, the Left overlay AND
the Right dream (a deterministic, live luminous haze since session 0013) are all
continuous runtime ops (LIVE_UPDATE); only swapping the clip needs a CROSSFADE,
and toggling video on/off fades to/from black.

Knobs no longer *select* a clip (the base library is neutral by construction);
they *transform* it. Which neutral base to show is an injected policy
(`choose_base`), defaulting to the first clip; richer rotation is a later slice.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Callable, Optional

from hef.selection import Coordinate
from player.alteration import RenderPlan, plan_alteration
from player.audio import resolve_visual
from player.controls import Controls


class TransitionKind:
    NONE = "none"
    LIVE_UPDATE = "live_update"
    CROSSFADE = "crossfade"
    FADE_TO_BLACK = "fade_to_black"
    FADE_FROM_BLACK = "fade_from_black"


@dataclass(frozen=True)
class Playback:
    """What should currently be playing. Visual and audio are orthogonal (audio
    spec §2): `video` is whether the projector shows footage, `audio_source` is
    the independent audio dial (off / soundtrack)."""

    clip_id: Optional[str]            # None = black walls
    plan: Optional[RenderPlan]        # None when black
    video: bool                       # whether footage is shown (Visual on/off)
    audio_source: str                 # the Audio dial source (off/soundtrack)
    volume: int
    brightness: int


@dataclass(frozen=True)
class Transition:
    kind: str
    playback: Playback


def _first(library):
    if not library:
        raise ValueError("no base clips available to play video")
    return library[0]


_BLACK = Playback(
    clip_id=None,
    plan=None,
    video=False,
    audio_source="off",
    volume=0,
    brightness=0,
)


class Player:
    def __init__(
        self,
        base_library,
        *,
        choose_base: Callable = _first,
        approved_only: bool = False,
    ):
        self._library = list(base_library)
        self._choose_base = choose_base
        self._approved_only = approved_only  # reserved for catalog-backed libs
        self._current = _BLACK

    def _resolve(self, controls: Controls) -> Playback:
        video = resolve_visual(controls.visual)
        if not video:
            return Playback(
                clip_id=None,
                plan=None,
                video=False,
                audio_source=controls.audio,
                volume=controls.volume,
                brightness=controls.brightness,
            )
        clip = self._choose_base(self._library)
        coord = Coordinate(controls.left, controls.right, controls.dark, controls.light)
        return Playback(
            clip_id=clip.id,
            plan=plan_alteration(coord),
            video=True,
            audio_source=controls.audio,
            volume=controls.volume,
            brightness=controls.brightness,
        )

    def _classify(self, prev: Playback, nxt: Playback) -> str:
        prev_video = prev.clip_id is not None
        next_video = nxt.clip_id is not None
        if prev == nxt:
            return TransitionKind.NONE
        if prev_video and not next_video:
            return TransitionKind.FADE_TO_BLACK
        if next_video and not prev_video:
            return TransitionKind.FADE_FROM_BLACK
        if next_video and prev_video:
            # The Right dream is a live filter now (not a substrate swap), so it
            # no longer forces a crossfade — only a clip (scale) change does.
            if nxt.clip_id != prev.clip_id:
                return TransitionKind.CROSSFADE
            return TransitionKind.LIVE_UPDATE
        # both black: only audio/levels could have changed
        return TransitionKind.LIVE_UPDATE

    def update(self, controls: Controls) -> Transition:
        nxt = self._resolve(controls)
        kind = self._classify(self._current, nxt)
        self._current = nxt
        return Transition(kind=kind, playback=nxt)
