"""Bridge rotary knob — armed-by-software state machine.

Section 5 of the brief: the knob never controls anything by default.
The operator must press it, pick a function, and then turn. Idle activity
auto-disarms after a configurable timeout (default 30 s).

The state machine modelled here is the **logical** one consumed by the
Display and by the controller; the actual encoder quadrature decoding
happens in the ESP32 firmware (Sprint 7).
"""

from __future__ import annotations

from datetime import UTC, datetime
from enum import StrEnum

from pydantic import BaseModel, ConfigDict, Field, model_validator


class KnobMode(StrEnum):
    """High-level state of the bridge knob."""

    LIBRE = "libre"
    """Idle: rotation does nothing. Default."""

    ARMADO = "armado"
    """Function selected; rotation now produces a pending value."""

    CONFIRMANDO = "confirmando"
    """Pending value being shown for operator confirmation."""


class KnobFunction(StrEnum):
    """The set of values the knob may target once armed."""

    NONE = "none"
    """No function selected (LIBRE only)."""

    RUMBO = "rumbo"
    """Heading setpoint."""

    GANANCIA_P = "ganancia_p"
    """Proportional gain (Technician+ only)."""

    VELOCIDAD_GIRO = "velocidad_giro"
    """Desired turn rate."""

    BRILLO = "brillo"
    """Display brightness."""

    VOLUMEN = "volumen"
    """Alarm volume."""


DEFAULT_ARMED_TIMEOUT_S: float = 30.0
"""Default auto-disarm timeout in seconds (brief section 5)."""


class KnobState(BaseModel):
    """Immutable snapshot of the knob state at a point in time."""

    model_config = ConfigDict(frozen=True, extra="forbid")

    mode: KnobMode = KnobMode.LIBRE
    function: KnobFunction = KnobFunction.NONE

    current_value: float | None = Field(
        default=None,
        description="Last confirmed value of the selected function.",
    )
    pending_value: float | None = Field(
        default=None,
        description="Value being adjusted but not yet confirmed.",
    )

    armed_at: datetime | None = Field(
        default=None,
        description="UTC timestamp when the knob entered ARMADO mode.",
    )
    timeout_remaining_s: float = Field(
        default=0.0,
        ge=0.0,
        description="Seconds remaining before auto-disarm.",
    )

    @model_validator(mode="after")
    def _consistency(self) -> KnobState:
        if self.mode == KnobMode.LIBRE:
            if self.function != KnobFunction.NONE:
                raise ValueError("LIBRE mode requires function == NONE")
            if self.pending_value is not None:
                raise ValueError("LIBRE mode forbids pending_value")
            if self.armed_at is not None:
                raise ValueError("LIBRE mode forbids armed_at")
            if self.timeout_remaining_s != 0.0:
                raise ValueError("LIBRE mode requires timeout_remaining_s == 0")
        else:
            if self.function == KnobFunction.NONE:
                raise ValueError(f"{self.mode.value} mode requires a non-NONE function")
            if self.armed_at is None:
                raise ValueError(f"{self.mode.value} mode requires armed_at to be set")
        if self.mode == KnobMode.CONFIRMANDO and self.pending_value is None:
            raise ValueError("CONFIRMANDO mode requires pending_value to be set")
        return self

    # --- Pure transition helpers (return new immutable states) --------------
    @classmethod
    def idle(cls) -> KnobState:
        """Construct the canonical idle (LIBRE) state."""
        return cls()

    def arm(
        self,
        function: KnobFunction,
        *,
        current_value: float,
        timeout_s: float = DEFAULT_ARMED_TIMEOUT_S,
        now: datetime | None = None,
    ) -> KnobState:
        """Transition LIBRE → ARMADO for the given function."""
        if self.mode != KnobMode.LIBRE:
            raise ValueError(f"Cannot arm from mode {self.mode.value}")
        if function == KnobFunction.NONE:
            raise ValueError("Cannot arm with KnobFunction.NONE")
        return KnobState(
            mode=KnobMode.ARMADO,
            function=function,
            current_value=current_value,
            pending_value=None,
            armed_at=now or datetime.now(UTC),
            timeout_remaining_s=timeout_s,
        )

    def propose(self, value: float, *, timeout_s: float = DEFAULT_ARMED_TIMEOUT_S) -> KnobState:
        """Operator turned the knob: stage a pending value (ARMADO → CONFIRMANDO)."""
        if self.mode not in (KnobMode.ARMADO, KnobMode.CONFIRMANDO):
            raise ValueError(f"Cannot propose from mode {self.mode.value}")
        return KnobState(
            mode=KnobMode.CONFIRMANDO,
            function=self.function,
            current_value=self.current_value,
            pending_value=value,
            armed_at=self.armed_at,
            timeout_remaining_s=timeout_s,
        )

    def confirm(self, *, timeout_s: float = DEFAULT_ARMED_TIMEOUT_S) -> KnobState:
        """Operator pressed to confirm; commits ``pending_value`` and stays armed."""
        if self.mode != KnobMode.CONFIRMANDO:
            raise ValueError(f"Cannot confirm from mode {self.mode.value}")
        return KnobState(
            mode=KnobMode.ARMADO,
            function=self.function,
            current_value=self.pending_value,
            pending_value=None,
            armed_at=self.armed_at,
            timeout_remaining_s=timeout_s,
        )

    def disarm(self) -> KnobState:
        """Force the knob back to LIBRE (timeout, alarm, mode change, long-press)."""
        return KnobState.idle()
