"""Local rover command state and watchdog safety logic for Lab038.

The educational control payload is carried inside the unchanged Lab033 common
packet.  It is intentionally small and is not a production motor-controller
protocol.
"""

from __future__ import annotations

from dataclasses import dataclass
import struct

from protocol.link_packet import LinkPacket, TrafficClass


CONTROL_MAGIC = b"CTL1"
CONTROL_PAYLOAD_FORMAT = "!4sffBB"
CONTROL_PAYLOAD_SIZE = struct.calcsize(CONTROL_PAYLOAD_FORMAT)


class ControlPayloadError(ValueError):
    """The educational Lab038 control payload is malformed."""


@dataclass(frozen=True)
class ControlState:
    desired_speed_mps: float
    desired_turn: float
    braking: bool
    movement_allowed: bool


SAFE_STATE = ControlState(0.0, 0.0, True, False)


def encode_control_state(state: ControlState) -> bytes:
    """Serialize the Lab038 teaching payload without changing LinkPacket."""

    if not isinstance(state, ControlState):
        raise TypeError("state must be ControlState")
    return struct.pack(
        CONTROL_PAYLOAD_FORMAT,
        CONTROL_MAGIC,
        float(state.desired_speed_mps),
        float(state.desired_turn),
        int(bool(state.braking)),
        int(bool(state.movement_allowed)),
    )


def decode_control_state(payload: bytes) -> ControlState:
    """Decode and validate the Lab038 teaching payload."""

    if not isinstance(payload, (bytes, bytearray)):
        raise TypeError("payload must be bytes or bytearray")
    if len(payload) != CONTROL_PAYLOAD_SIZE:
        raise ControlPayloadError("invalid control payload length")
    magic, speed, turn, braking, allowed = struct.unpack(CONTROL_PAYLOAD_FORMAT, bytes(payload))
    if magic != CONTROL_MAGIC:
        raise ControlPayloadError("invalid control payload magic")
    if braking not in (0, 1) or allowed not in (0, 1):
        raise ControlPayloadError("control booleans must be zero or one")
    return ControlState(speed, turn, bool(braking), bool(allowed))


class RoverControlFailsafe:
    """Strictly-monotonic command receiver with temporary and latched stops."""

    def __init__(self, watchdog_timeout_us: int, boot_time_us: int = 0) -> None:
        if watchdog_timeout_us <= 0:
            raise ValueError("watchdog_timeout_us must be positive")
        self.watchdog_timeout_us = watchdog_timeout_us
        self.last_sequence = -1
        self.last_new_command_time_us = boot_time_us
        self.requested_state = SAFE_STATE
        self.temporary_safe_stop = False
        self.emergency_stop_latched = False
        self.stale_commands = 0
        self.new_commands = 0
        self.watchdog_triggers = 0
        self.emergency_actions = 0
        self.emergency_duplicates = 0
        self._emergency_keys: set[tuple[int, int]] = set()

    @property
    def effective_state(self) -> ControlState:
        if self.emergency_stop_latched or self.temporary_safe_stop:
            return SAFE_STATE
        return self.requested_state

    def receive_state(self, packet: LinkPacket, receive_time_us: int) -> bool:
        """Apply a strictly newer normal command and refresh the watchdog."""

        if packet.traffic_class is not TrafficClass.CONTROL:
            raise ValueError("normal state must use CONTROL traffic class")
        state = decode_control_state(packet.payload)
        if packet.sequence_number <= self.last_sequence:
            self.stale_commands += 1
            return False
        self.last_sequence = packet.sequence_number
        self.last_new_command_time_us = receive_time_us
        self.requested_state = state
        self.temporary_safe_stop = False
        self.new_commands += 1
        return True

    def check_watchdog(self, now_us: int) -> bool:
        """Enter a temporary safe stop when fresh control has timed out."""

        if now_us < self.last_new_command_time_us:
            raise ValueError("watchdog time moved backwards")
        expired = now_us - self.last_new_command_time_us >= self.watchdog_timeout_us
        if expired and not self.temporary_safe_stop:
            self.temporary_safe_stop = True
            self.watchdog_triggers += 1
            return True
        return False

    def receive_emergency(self, packet: LinkPacket) -> bool:
        """Latch emergency stop once; return false for subsequent copies."""

        if packet.traffic_class is not TrafficClass.EMERGENCY:
            raise ValueError("emergency command must use EMERGENCY traffic class")
        key = packet.stream_id, packet.sequence_number
        if key in self._emergency_keys:
            self.emergency_duplicates += 1
            return False
        self._emergency_keys.add(key)
        self.emergency_stop_latched = True
        self.temporary_safe_stop = True
        self.emergency_actions += 1
        return True
