"""Control-panel state: the data shape read from the Arduino over serial.

This is the serial-contract payload shared with sub-project 4 (firmware). It
models the full panel from the audio spec §3: a Visual toggle (on/off) + an
Audio source selector (off/soundtrack; white-noise/music deferred), the four experience knobs
(0..4), and the two intensity levels (volume, brightness). The wire framing
itself is the separate 3<->4 serial contract; this module is the *decoded* form.
"""

from __future__ import annotations

from dataclasses import dataclass, fields

from player.audio import AUDIO_SOURCES, VISUAL_POSITIONS

KNOB_FIELDS = ("left", "right", "dark", "light", "volume", "brightness")
KNOB_MIN = 0
KNOB_MAX = 4


class ControlsError(ValueError):
    """Raised when a Controls payload is structurally invalid."""


@dataclass(frozen=True)
class Controls:
    visual: str
    audio: str
    left: int
    right: int
    dark: int
    light: int
    volume: int
    brightness: int


def validate_controls(c: Controls) -> None:
    """Raise ControlsError if the payload is structurally invalid."""
    if c.visual not in VISUAL_POSITIONS:
        raise ControlsError(
            f"invalid visual position {c.visual!r}; "
            f"expected one of {sorted(VISUAL_POSITIONS)}"
        )
    if c.audio not in AUDIO_SOURCES:
        raise ControlsError(
            f"invalid audio source {c.audio!r}; "
            f"expected one of {sorted(AUDIO_SOURCES)}"
        )
    for name in KNOB_FIELDS:
        value = getattr(c, name)
        if isinstance(value, bool) or not isinstance(value, int):
            raise ControlsError(f"knob {name} must be an int, got {value!r}")
        if not (KNOB_MIN <= value <= KNOB_MAX):
            raise ControlsError(
                f"knob {name}={value} out of range {KNOB_MIN}..{KNOB_MAX}"
            )


def parse_controls(data: dict) -> Controls:
    """Build a validated Controls from a decoded mapping, rejecting unknown or
    missing keys."""
    known = {f.name for f in fields(Controls)}
    unknown = set(data) - known
    if unknown:
        raise ControlsError(f"unknown keys: {sorted(unknown)}")
    missing = known - set(data)
    if missing:
        raise ControlsError(f"missing keys: {sorted(missing)}")
    c = Controls(**data)
    validate_controls(c)
    return c
