from __future__ import annotations

import ipaddress
import platform
import queue
import random
import re
import socket
import subprocess
import threading
import time
import uuid
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, Optional, Tuple

from ..core.bus import CANBusEmulation, CANBusParticipant
from ..core.can import CANFrame
from ..core.isotp import (
    FD_EXTENDED_FRAME_LENGTH,
    FD_STANDARD_FRAME_LENGTH,
    ISOTPAction,
    ISOTPFDTransceiver,
    ISOTPTransceiver,
)
from .protocol import (
    Arbitration,
    ChannelProtocol,
    PDU,
    PDUType,
    _uint16_from_bytes,
    _uint32_from_bytes,
)

try:
    import lz4.block as lz4_block
except Exception:  # pragma: no cover - optional dependency
    lz4_block = None


_BUFFER_SIZE = 8192
_SERIAL_FALLBACK = "000000"
_FIRMWARE_VERSION_PATTERN = re.compile(r"\d+\.\d+\.\d+(?:\.\d+)?")
_RAW_PROTOCOLS = {int(ChannelProtocol.raw), int(ChannelProtocol.raw_fd)}
_ISOTP_PROTOCOLS = {int(ChannelProtocol.isotp), int(ChannelProtocol.isotp_fd)}
_FD_PROTOCOLS = {int(ChannelProtocol.raw_fd), int(ChannelProtocol.isotp_fd)}


def _is_raw_protocol(protocol: int) -> bool:
    return int(protocol) in _RAW_PROTOCOLS


def _is_isotp_protocol(protocol: int) -> bool:
    return int(protocol) in _ISOTP_PROTOCOLS


def _is_fd_protocol(protocol: int) -> bool:
    return int(protocol) in _FD_PROTOCOLS


def _format_serial_from_mac(mac: str) -> str:
    hex_chars = re.sub(r"[^0-9a-fA-F]", "", mac)
    if len(hex_chars) < 6:
        return _SERIAL_FALLBACK
    return hex_chars[-6:].upper()


def _resolve_bind_ip(host: str) -> Optional[str]:
    if not host or host in ("0.0.0.0", "::"):
        return _resolve_default_route_ip()
    try:
        infos = socket.getaddrinfo(host, None)
    except socket.gaierror:
        return None
    for family, _socktype, _proto, _canonname, sockaddr in infos:
        if family == socket.AF_INET:
            return sockaddr[0]
        if family == socket.AF_INET6:
            return sockaddr[0]
    return None


def _resolve_default_route_ip() -> Optional[str]:
    for family, target in (
        (socket.AF_INET, ("8.8.8.8", 80)),
        (socket.AF_INET6, ("2001:4860:4860::8888", 80, 0, 0)),
    ):
        try:
            sock = socket.socket(family, socket.SOCK_DGRAM)
            try:
                sock.connect(target)
                return sock.getsockname()[0]
            finally:
                sock.close()
        except OSError:
            continue
    return None


def _mac_from_psutil(ip: str) -> Optional[str]:
    try:
        import psutil  # type: ignore
    except Exception:
        return None
    af_link = getattr(psutil, "AF_LINK", None)
    for _name, addrs in psutil.net_if_addrs().items():
        has_ip = any(
            addr.address == ip and addr.family in (socket.AF_INET, socket.AF_INET6)
            for addr in addrs
        )
        if not has_ip:
            continue
        for addr in addrs:
            if af_link is not None and addr.family == af_link:
                return addr.address
            if hasattr(socket, "AF_LINK") and addr.family == socket.AF_LINK:
                return addr.address
    return None


def _mac_from_ip_command(ip: str) -> Optional[str]:
    try:
        addr_proc = subprocess.run(
            ["ip", "-o", "addr", "show"],
            check=False,
            capture_output=True,
            text=True,
        )
    except FileNotFoundError:
        return None
    if addr_proc.returncode != 0:
        return None
    interface = None
    for line in addr_proc.stdout.splitlines():
        parts = line.split()
        if len(parts) < 4:
            continue
        ifname = parts[1]
        family = parts[2]
        if family not in ("inet", "inet6"):
            continue
        addr = parts[3].split("/", 1)[0]
        if addr == ip:
            interface = ifname
            break
    if not interface:
        return None
    link_proc = subprocess.run(
        ["ip", "-o", "link", "show", interface],
        check=False,
        capture_output=True,
        text=True,
    )
    if link_proc.returncode != 0:
        return None
    match = re.search(r"link/(?:ether|loopback)\s+([0-9a-fA-F:]{17})", link_proc.stdout)
    if not match:
        return None
    return match.group(1)


def _mac_from_ifconfig(ip: str) -> Optional[str]:
    try:
        proc = subprocess.run(
            ["ifconfig"],
            check=False,
            capture_output=True,
            text=True,
        )
    except FileNotFoundError:
        return None
    if proc.returncode != 0:
        return None
    blocks = re.split(r"\n(?=\S)", proc.stdout)
    for block in blocks:
        header = block.splitlines()[0] if block else ""
        ifname = header.split(":", 1)[0].strip()
        if not ifname:
            continue
        ip_match = re.search(r"\binet6?\s+([0-9a-fA-F:\.]+)", block)
        if not ip_match or ip_match.group(1) != ip:
            continue
        mac_match = re.search(r"\bether\s+([0-9a-fA-F:]{17})", block)
        if mac_match:
            return mac_match.group(1)
    return None


def _derive_serial_from_host(host: str) -> str:
    ip = _resolve_bind_ip(host)
    if not ip:
        return _SERIAL_FALLBACK
    try:
        ip_obj = ipaddress.ip_address(ip)
    except ValueError:
        return _SERIAL_FALLBACK
    if ip_obj.is_loopback:
        node = uuid.getnode()
        return _format_serial_from_mac(f"{node:012x}")
    mac = _mac_from_psutil(ip)
    if mac:
        return _format_serial_from_mac(mac)
    system = platform.system().lower()
    if "linux" in system:
        mac = _mac_from_ip_command(ip)
        if mac:
            return _format_serial_from_mac(mac)
    mac = _mac_from_ifconfig(ip)
    if mac:
        return _format_serial_from_mac(mac)
    node = uuid.getnode()
    return _format_serial_from_mac(f"{node:012x}")


def _is_plausible_firmware_version(value: str) -> bool:
    if value == "0.0.0.0":
        return False
    return _FIRMWARE_VERSION_PATTERN.fullmatch(value) is not None


def _extract_firmware_version_from_image(image: bytes) -> Optional[str]:
    if not image:
        return None

    # Firmware blobs include a null-terminated metadata header near the beginning.
    for token in image[:4096].split(b"\x00"):
        if not token or len(token) > 96:
            continue
        if any(byte < 0x20 or byte > 0x7E for byte in token):
            continue
        text = token.decode("ascii", errors="ignore").strip()
        if not text:
            continue
        if _is_plausible_firmware_version(text):
            return text
        if "ECUconnectOS" in text:
            match = _FIRMWARE_VERSION_PATTERN.search(text)
            if match and _is_plausible_firmware_version(match.group(0)):
                return match.group(0)

    decoded = image[:65536].decode("latin-1", errors="ignore")
    for match in _FIRMWARE_VERSION_PATTERN.finditer(decoded):
        version = match.group(0)
        if _is_plausible_firmware_version(version):
            return version
    return None


@dataclass
class ChannelState:
    protocol: int
    bitrate: int
    data_bitrate: int = 0
    is_fd: bool = False
    bitrate_switch: bool = False
    arbitration: Optional[Arbitration] = None
    rx_separation: int = 0
    tx_separation: int = 0
    tx_isotp: ISOTPTransceiver | ISOTPFDTransceiver = field(default_factory=ISOTPTransceiver)
    rx_isotp: Dict[Tuple[int, int], ISOTPTransceiver | ISOTPFDTransceiver] = field(default_factory=dict)


class ECUconnectParticipant(CANBusParticipant):
    def __init__(self, server: "ECUconnectServer") -> None:
        super().__init__()
        self.server = server

    @property
    def arbitration_ids(self):
        return set()

    def feed(self, frame: CANFrame, bus: CANBusEmulation) -> None:
        self.server.handle_incoming_frame(frame)


class ECUconnectServer:
    def __init__(
        self,
        bus: CANBusEmulation,
        host: str,
        port: int,
        log: bool = False,
        firmware_version: str = "0.9.999",
        hardware_revision: str = "PVIRT",
        update_prepare_delay_seconds: float = 3.0,
        update_chunk_delay_seconds: float = 0.05,
        update_commit_delay_seconds: float = 2.0,
        event_sink: Optional[Callable[[dict[str, Any]], None]] = None,
    ) -> None:
        self.bus = bus
        self.host = host
        self.port = port
        self.log = log
        self._event_sink = event_sink
        self._socket: Optional[socket.socket] = None
        self._stop = threading.Event()
        self._thread: Optional[threading.Thread] = None
        self._client: Optional[socket.socket] = None
        self._client_peer: Optional[str] = None
        self._client_count = 0
        self._last_rx: Optional[float] = None
        self._last_tx: Optional[float] = None
        self._rx_bytes = 0
        self._tx_bytes = 0
        self._write_lock = threading.Lock()
        self._outgoing: "queue.Queue[PDU]" = queue.Queue()
        self._participant = ECUconnectParticipant(self)

        self._channel_lock = threading.Lock()
        self._channels: Dict[int, ChannelState] = {}
        self._periodic_lock = threading.Lock()
        self._periodic: Dict[int, threading.Event] = {}
        self._next_periodic_handle = 1

        self.vendor = "Vanille-Media"
        self.model = "ECUconnect"
        self.hardware = hardware_revision or "PVIRT"
        self.serial = _derive_serial_from_host(host)
        self.firmware = firmware_version or "0.9.999"
        self._update_prepare_delay_seconds = max(0.0, float(update_prepare_delay_seconds))
        self._update_chunk_delay_seconds = max(0.0, float(update_chunk_delay_seconds))
        self._update_commit_delay_seconds = max(0.0, float(update_commit_delay_seconds))
        self._update_started = False
        self._update_committed = False
        self._update_payload = bytearray()
        self._pending_firmware_version: Optional[str] = None

    def _log(self, message: str) -> None:
        if self.log:
            print(message)

    def _emit_event(self, kind: str, **payload: Any) -> None:
        if self._event_sink is None:
            return
        event_payload: dict[str, Any] = {"kind": kind, "timestamp": time.time()}
        event_payload.update(payload)
        try:
            self._event_sink(event_payload)
        except Exception:
            pass

    def start(self) -> None:
        if self._thread and self._thread.is_alive():
            return
        self._stop.clear()
        self.bus.attach(self._participant)
        self._thread = threading.Thread(target=self._run, daemon=True)
        self._thread.start()

    def stop(self) -> None:
        self._stop.set()
        if self._client:
            try:
                self._client.shutdown(socket.SHUT_RDWR)
            except OSError:
                pass
            try:
                self._client.close()
            except OSError:
                pass
            self._client = None
            self._client_peer = None
        if self._socket:
            try:
                self._socket.shutdown(socket.SHUT_RDWR)
            except OSError:
                pass
            try:
                self._socket.close()
            except OSError:
                pass
            self._socket = None
        if self._thread is not None:
            self._thread.join(timeout=1.0)
            self._thread = None
        print("DEBUG: ECUconnect server stopped", flush=True)

    def handle_incoming_frame(self, frame: CANFrame) -> None:
        with self._channel_lock:
            channels = list(self._channels.items())
        for channel_number, channel in channels:
            if not channel.arbitration:
                continue
            if not self._matches_reply(channel.arbitration, frame.arbitration_id):
                continue
            self._handle_channel_frame(channel_number, channel, frame)

    def _handle_channel_frame(self, channel_number: int, channel: ChannelState, frame: CANFrame) -> None:
        arbitration = channel.arbitration
        if arbitration is None:
            return
        payload, extension = self._extract_payload(frame, arbitration.reply_extension)
        if payload is None:
            return
        if _is_isotp_protocol(channel.protocol):
            if self._handle_tx_flow_control(channel, payload, arbitration.request_extension):
                return
            self._handle_rx_isotp(channel_number, channel, frame, payload, extension)
            return
        if _is_raw_protocol(channel.protocol):
            data = payload[:]
            self._queue_pdu(PDU.received(channel_number, frame.arbitration_id, extension, data))

    def _handle_tx_flow_control(self, channel: ChannelState, payload: list[int], request_extension: int) -> bool:
        if channel.tx_isotp.state != "sending":
            return False
        try:
            action = channel.tx_isotp.did_read(payload)
        except Exception:
            return False
        if action.kind != "write_frames":
            return True
        self._send_isotp_frames(channel, action, request_extension, is_request=True)
        return True

    def _handle_rx_isotp(self, channel_number: int, channel: ChannelState, frame: CANFrame, payload: list[int], extension: int) -> None:
        frame_length = len(payload)
        key = (frame.arbitration_id, extension)
        transceiver = channel.rx_isotp.get(key)
        uses_extended = extension != 0
        if int(channel.protocol) == int(ChannelProtocol.isotp_fd):
            if transceiver is None or not isinstance(transceiver, ISOTPFDTransceiver):
                transceiver = ISOTPFDTransceiver()
            transceiver.configure_link(
                frame_length=frame_length,
                extended_addressing=uses_extended,
            )
            channel.rx_isotp[key] = transceiver
        else:
            if transceiver is None or not isinstance(transceiver, ISOTPTransceiver):
                transceiver = ISOTPTransceiver()
            transceiver.frame_length = frame_length
            channel.rx_isotp[key] = transceiver
        try:
            action = transceiver.did_read(payload)
        except Exception:
            return
        if action.kind == "process" and action.bytes is not None:
            self._queue_pdu(PDU.received(channel_number, frame.arbitration_id, extension, action.bytes))
            return
        if action.kind == "write_frames":
            self._send_isotp_frames(channel, action, channel.arbitration.request_extension, is_request=True)

    def _extract_payload(self, frame: CANFrame, expected_extension: int) -> Tuple[Optional[list[int]], int]:
        data = list(frame.data)
        if expected_extension != 0:
            if not data or data[0] != expected_extension:
                return None, 0
            return data[1:], expected_extension
        return data, 0

    def _matches_reply(self, arbitration: Arbitration, can_id: int) -> bool:
        if arbitration.reply_mask == 0:
            return True
        return (can_id & arbitration.reply_mask) == (arbitration.reply_pattern & arbitration.reply_mask)

    def _queue_pdu(self, pdu: PDU) -> None:
        if self._client is None:
            return
        self._outgoing.put(pdu)

    def _send_pdu(self, pdu: PDU) -> None:
        if self._client is None:
            return
        data = pdu.frame
        with self._write_lock:
            try:
                self._client.sendall(data)
                self._last_tx = time.time()
                self._tx_bytes += len(data)
                self._emit_event("transport_tx", bytes=len(data), peer=self._client_peer or "unknown")
            except OSError:
                self._client = None
                self._emit_event("connection_error", message="socket write failed", peer=self._client_peer or "unknown")

    def _send_isotp_frames(self, channel: ChannelState, action: ISOTPAction, request_extension: int, is_request: bool) -> None:
        if action.frames is None:
            return
        for frame_bytes in action.frames:
            payload = list(frame_bytes)
            if request_extension != 0:
                payload = [request_extension] + payload
            dlc = len(payload)
            can_id = channel.arbitration.request if is_request and channel.arbitration else 0
            frame = CANFrame(
                arbitration_id=can_id,
                data=payload,
                dlc=dlc,
                is_fd=channel.is_fd,
                bitrate_switch=channel.bitrate_switch,
            )
            self.bus.write(frame, originator=self._participant)
            if action.separation_time > 0:
                time.sleep(action.separation_time / 1000.0)

    def _run(self) -> None:
        self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        for attempt in range(10):
            try:
                self._socket.bind((self.host, self.port))
                self._socket.listen(1)
                self._socket.settimeout(0.5)
                break
            except OSError as exc:
                print(f"ECUconnect: bind attempt {attempt + 1} failed: {exc}")
                if attempt == 9:
                    print(f"ECUconnect: failed to listen on {self.host}:{self.port}: {exc}")
                    self._stop.set()
                    return
                time.sleep(0.1)
        self._log(f"ECUconnect: listening on {self.host}:{self.port}")
        print(f"DEBUG: ECUconnect TCP server started on {self.host}:{self.port}", flush=True)

        while not self._stop.is_set():
            try:
                client, _addr = self._socket.accept()
            except OSError:
                if self._stop.is_set():
                    break
                continue
            self._client = client
            self._client_peer = f"{_addr[0]}:{_addr[1]}"
            self._client_count = 1
            self._log(f"ECUconnect: client connected from {_addr}")
            self._emit_event("connection_opened", peer=self._client_peer)
            client.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
            client.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
            self._serve_client(client)
            self._log("ECUconnect: client disconnected")
            self._emit_event("connection_closed", peer=self._client_peer or "unknown")
            self._client_peer = None

    def _serve_client(self, client: socket.socket) -> None:
        buffer = bytearray()
        sender = threading.Thread(target=self._outgoing_sender, daemon=True)
        sender.start()
        try:
            while True:
                try:
                    chunk = client.recv(_BUFFER_SIZE)
                except OSError:
                    return
                if not chunk:
                    return
                self._last_rx = time.time()
                self._rx_bytes += len(chunk)
                self._emit_event("transport_rx", bytes=len(chunk), peer=self._client_peer or "unknown")
                buffer.extend(chunk)
                while buffer:
                    consumed = PDU.contains_pdu(buffer)
                    if consumed == 0:
                        break
                    if consumed < 0:
                        del buffer[: -consumed]
                        continue
                    frame = bytes(buffer[:consumed])
                    del buffer[:consumed]
                    try:
                        pdu = PDU.from_bytes(frame)
                    except Exception:
                        self._send_pdu(PDU.error(PDUType.error_invalid_command))
                        continue
                    self._handle_pdu(pdu)
        finally:
            self._client = None
            self._client_count = 0
            self._reset_session()

    def ui_snapshot(self) -> dict:
        return {
            "connected": self._client is not None,
            "clients": self._client_count,
            "last_rx": self._last_rx,
            "last_tx": self._last_tx,
            "rx_bytes": self._rx_bytes,
            "tx_bytes": self._tx_bytes,
        }

    def _outgoing_sender(self) -> None:
        while self._client is not None and not self._stop.is_set():
            try:
                pdu = self._outgoing.get(timeout=0.2)
            except queue.Empty:
                continue
            self._send_pdu(pdu)

    def _handle_pdu(self, pdu: PDU) -> None:
        if pdu.pdu_type == PDUType.ping:
            self._send_pdu(PDU.pong(pdu.payload))
            return
        if pdu.pdu_type == PDUType.request_info:
            self._send_pdu(PDU.info(self.vendor, self.model, self.hardware, self.serial, self.firmware))
            return
        if pdu.pdu_type == PDUType.read_voltage:
            voltage = 12000 + random.randint(-1000, 1000)
            self._send_pdu(PDU.voltage(voltage))
            return
        if pdu.pdu_type == PDUType.open_channel:
            self._handle_open_channel(pdu, is_fd_open=False)
            return
        if pdu.pdu_type == PDUType.open_fd_channel:
            self._handle_open_channel(pdu, is_fd_open=True)
            return
        if pdu.pdu_type == PDUType.close_channel:
            self._handle_close_channel(pdu)
            return
        if pdu.pdu_type == PDUType.set_arbitration:
            self._handle_set_arbitration(pdu)
            return
        if pdu.pdu_type == PDUType.send:
            self._handle_send(pdu, compressed=False)
            return
        if pdu.pdu_type == PDUType.send_compressed:
            self._handle_send(pdu, compressed=True)
            return
        if pdu.pdu_type == PDUType.start_periodic_message:
            self._handle_start_periodic(pdu)
            return
        if pdu.pdu_type == PDUType.end_periodic_message:
            self._handle_end_periodic(pdu)
            return
        if pdu.pdu_type == PDUType.prepare_for_update:
            self._handle_prepare_for_update(pdu)
            return
        if pdu.pdu_type == PDUType.send_update_data:
            self._handle_send_update_data(pdu)
            return
        if pdu.pdu_type == PDUType.commit_update:
            self._handle_commit_update(pdu)
            return
        if pdu.pdu_type == PDUType.reset:
            self._handle_reset(pdu)
            return
        self._send_pdu(PDU.error(PDUType.error_invalid_command))

    def _handle_open_channel(self, pdu: PDU, is_fd_open: bool) -> None:
        if is_fd_open:
            if len(pdu.payload) < 10:
                self._send_pdu(PDU.error(PDUType.error_invalid_command))
                return
            protocol = pdu.payload[0]
            bitrate = _uint32_from_bytes(pdu.payload[1:5])
            data_bitrate = _uint32_from_bytes(pdu.payload[5:9])
            separation = pdu.payload[9]
            allowed = {int(ChannelProtocol.raw_fd), int(ChannelProtocol.isotp_fd)}
        else:
            if len(pdu.payload) < 6:
                self._send_pdu(PDU.error(PDUType.error_invalid_command))
                return
            protocol = pdu.payload[0]
            bitrate = _uint32_from_bytes(pdu.payload[1:5])
            data_bitrate = 0
            separation = pdu.payload[5]
            allowed = {int(ChannelProtocol.raw), int(ChannelProtocol.isotp)}

        if int(protocol) not in allowed:
            self._send_pdu(PDU.error(PDUType.error_invalid_command))
            return

        rx_separation = (separation >> 4) & 0x0F
        tx_separation = separation & 0x0F
        with self._channel_lock:
            channel = self._allocate_channel(
                protocol=protocol,
                bitrate=bitrate,
                data_bitrate=data_bitrate,
                rx_separation=rx_separation,
                tx_separation=tx_separation,
            )
        if channel == 0xFF:
            self._send_pdu(PDU.error(PDUType.error_hardware))
            return
        self._send_pdu(PDU.channel_opened(channel))

    def _allocate_channel(
        self,
        protocol: int,
        bitrate: int,
        data_bitrate: int,
        rx_separation: int,
        tx_separation: int,
    ) -> int:
        for candidate in range(0x00, 0x10):
            if candidate not in self._channels:
                is_fd = _is_fd_protocol(protocol)
                tx_isotp: ISOTPTransceiver | ISOTPFDTransceiver
                if int(protocol) == int(ChannelProtocol.isotp_fd):
                    tx_isotp = ISOTPFDTransceiver()
                else:
                    tx_isotp = ISOTPTransceiver()
                self._channels[candidate] = ChannelState(
                    protocol=protocol,
                    bitrate=bitrate,
                    data_bitrate=data_bitrate,
                    is_fd=is_fd,
                    bitrate_switch=bool(is_fd and data_bitrate > 0 and data_bitrate != bitrate),
                    rx_separation=rx_separation,
                    tx_separation=tx_separation,
                    tx_isotp=tx_isotp,
                )
                return candidate
        return 0xFF

    def _handle_close_channel(self, pdu: PDU) -> None:
        if not pdu.payload:
            self._send_pdu(PDU.error(PDUType.error_invalid_command))
            return
        channel = pdu.payload[0]
        with self._channel_lock:
            if channel not in self._channels:
                self._send_pdu(PDU.error(PDUType.error_invalid_channel))
                return
            del self._channels[channel]
        self._send_pdu(PDU.channel_closed(channel))

    def _handle_set_arbitration(self, pdu: PDU) -> None:
        if len(pdu.payload) < 1 + (4 + 1 + 4 + 4 + 1):
            self._send_pdu(PDU.error(PDUType.error_invalid_command))
            return
        channel = pdu.payload[0]
        arbitration, _ = Arbitration.from_payload(pdu.payload, 1)
        with self._channel_lock:
            channel_state = self._channels.get(channel)
            if channel_state is None:
                self._send_pdu(PDU.error(PDUType.error_invalid_channel))
                return
            channel_state.arbitration = arbitration
        self._send_pdu(PDU.ok())

    def _handle_send(self, pdu: PDU, compressed: bool) -> None:
        if not pdu.payload:
            self._send_pdu(PDU.error(PDUType.error_invalid_command))
            return
        channel = pdu.payload[0]
        with self._channel_lock:
            channel_state = self._channels.get(channel)
        if channel_state is None or channel_state.arbitration is None:
            self._send_pdu(PDU.error(PDUType.error_invalid_channel))
            return
        if not (_is_raw_protocol(channel_state.protocol) or _is_isotp_protocol(channel_state.protocol)):
            self._send_pdu(PDU.error(PDUType.error_invalid_command))
            return
        if compressed:
            if lz4_block is None:
                self._send_pdu(PDU.error(PDUType.error_invalid_command))
                return
            if len(pdu.payload) < 3:
                self._send_pdu(PDU.error(PDUType.error_invalid_command))
                return
            expected_length = _uint16_from_bytes(pdu.payload[1:3])
            compressed_payload = bytes(pdu.payload[3:])
            try:
                payload = list(lz4_block.decompress(compressed_payload, uncompressed_size=expected_length))
            except Exception:
                self._send_pdu(PDU.error(PDUType.error_invalid_command))
                return
        else:
            payload = pdu.payload[1:]
        self._send_pdu(PDU.ok())
        self._send_payload(channel_state, payload)

    def _send_payload(self, channel: ChannelState, payload: list[int]) -> None:
        arbitration = channel.arbitration
        if arbitration is None:
            return
        if _is_raw_protocol(channel.protocol):
            data = payload[:]
            if arbitration.request_extension != 0:
                data.insert(0, arbitration.request_extension)
            max_len = 64 if channel.is_fd else 8
            if len(data) > max_len:
                return
            frame = CANFrame(
                arbitration_id=arbitration.request,
                data=data,
                dlc=len(data),
                is_fd=channel.is_fd,
                bitrate_switch=channel.bitrate_switch,
            )
            self.bus.write(frame, originator=self._participant)
            return
        if _is_isotp_protocol(channel.protocol):
            extension = arbitration.request_extension
            if int(channel.protocol) == int(ChannelProtocol.isotp_fd):
                if not isinstance(channel.tx_isotp, ISOTPFDTransceiver):
                    channel.tx_isotp = ISOTPFDTransceiver()
                channel.tx_isotp.configure_link(
                    frame_length=FD_EXTENDED_FRAME_LENGTH if extension != 0 else FD_STANDARD_FRAME_LENGTH,
                    extended_addressing=extension != 0,
                )
            else:
                if not isinstance(channel.tx_isotp, ISOTPTransceiver):
                    channel.tx_isotp = ISOTPTransceiver()
                channel.tx_isotp.frame_length = 7 if extension != 0 else 8
            try:
                action = channel.tx_isotp.write(payload)
            except Exception:
                return
            if action.kind != "write_frames":
                return
            self._send_isotp_frames(channel, action, extension, is_request=True)

    def _handle_start_periodic(self, pdu: PDU) -> None:
        if len(pdu.payload) < 1 + (4 + 1 + 4 + 4 + 1):
            self._send_pdu(PDU.error(PDUType.error_invalid_command))
            return
        interval = pdu.payload[0] * 500
        arbitration, offset = Arbitration.from_payload(pdu.payload, 1)
        data = pdu.payload[offset:]
        handle = self._allocate_periodic_handle()
        stop_event = threading.Event()
        with self._periodic_lock:
            self._periodic[handle] = stop_event
        self._start_periodic_sender(arbitration, data, interval, stop_event)
        self._send_pdu(PDU(PDUType.periodic_message_started, [handle]))

    def _start_periodic_sender(self, arbitration: Arbitration, data: list[int], interval_ms: int, stop_event: threading.Event) -> None:
        def _loop() -> None:
            while self._client is not None and not stop_event.is_set():
                payload = data[:]
                if arbitration.request_extension != 0:
                    payload.insert(0, arbitration.request_extension)
                if len(payload) <= 8:
                    frame = CANFrame(arbitration_id=arbitration.request, data=payload, dlc=len(payload))
                    self.bus.write(frame, originator=self._participant)
                time.sleep(interval_ms / 1000.0)

        thread = threading.Thread(target=_loop, daemon=True)
        thread.start()

    def _handle_end_periodic(self, pdu: PDU) -> None:
        handle = pdu.payload[0] if pdu.payload else 0
        with self._periodic_lock:
            if handle == 0:
                handles = list(self._periodic.keys())
            else:
                handles = [handle] if handle in self._periodic else []
            for handle_to_stop in handles:
                self._periodic[handle_to_stop].set()
                del self._periodic[handle_to_stop]
        self._send_pdu(PDU.ok())

    def _handle_prepare_for_update(self, pdu: PDU) -> None:
        if pdu.payload:
            self._send_pdu(PDU.error(PDUType.error_invalid_command))
            return
        self._update_started = True
        self._update_committed = False
        self._update_payload = bytearray()
        self._pending_firmware_version = None
        self._delay_update_stage(self._update_prepare_delay_seconds)
        # Compatibility: older Swift-Automotive clients expect a generic OK here.
        self._send_pdu(PDU.ok())

    def _handle_send_update_data(self, pdu: PDU) -> None:
        if not self._update_started or self._update_committed:
            self._send_pdu(PDU.error(PDUType.error_invalid_command))
            return
        self._update_payload.extend(pdu.payload)
        self._delay_update_stage(self._update_chunk_delay_seconds)
        # Compatibility: older Swift-Automotive clients expect a generic OK here.
        self._send_pdu(PDU.ok())

    def _handle_commit_update(self, pdu: PDU) -> None:
        if pdu.payload:
            self._send_pdu(PDU.error(PDUType.error_invalid_command))
            return
        if not self._update_started:
            self._send_pdu(PDU.error(PDUType.error_invalid_command))
            return
        self._update_committed = True
        self._pending_firmware_version = _extract_firmware_version_from_image(bytes(self._update_payload))
        self._delay_update_stage(self._update_commit_delay_seconds)
        # Compatibility: older Swift-Automotive clients expect a generic OK here.
        self._send_pdu(PDU.ok())

    def _handle_reset(self, pdu: PDU) -> None:
        if pdu.payload:
            self._send_pdu(PDU.error(PDUType.error_invalid_command))
            return

        if self._update_committed and self._pending_firmware_version:
            next_firmware = self._pending_firmware_version
            self.firmware = next_firmware
            self._emit_event(
                "reported_identity_changed",
                firmware=self.firmware,
                hardware=self.hardware,
                source="firmware_update",
                peer=self._client_peer or "unknown",
            )

        self._send_pdu(PDU.ok())
        self._clear_update_state()
        self._reset_session()
        self._disconnect_client()

    def _allocate_periodic_handle(self) -> int:
        with self._periodic_lock:
            for _ in range(0xFE):
                handle = self._next_periodic_handle
                self._next_periodic_handle = 1 if handle >= 0xFE else handle + 1
                if handle not in self._periodic:
                    return handle
        return 0xFF

    def _clear_update_state(self) -> None:
        self._update_started = False
        self._update_committed = False
        self._update_payload = bytearray()
        self._pending_firmware_version = None

    def _delay_update_stage(self, seconds: float) -> None:
        if seconds <= 0:
            return
        deadline = time.monotonic() + seconds
        while not self._stop.is_set():
            remaining = deadline - time.monotonic()
            if remaining <= 0:
                return
            time.sleep(min(0.1, remaining))

    def _disconnect_client(self) -> None:
        if self._client is None:
            return
        try:
            self._client.shutdown(socket.SHUT_RDWR)
        except OSError:
            pass
        try:
            self._client.close()
        except OSError:
            pass
        self._client = None
        self._client_count = 0

    def _reset_session(self) -> None:
        with self._channel_lock:
            self._channels.clear()
        with self._periodic_lock:
            for stop_event in self._periodic.values():
                stop_event.set()
            self._periodic.clear()
