from __future__ import annotations

import asyncio
import dataclasses
import logging
import typing

from typing_extensions import Self
from zigpy.serial import SerialProtocol
import zigpy.types as t

from .common import BufferTooShort, Version, asyncio_timeout

_LOGGER = logging.getLogger(__name__)

COMMAND_TIMEOUT = 2


class MessageType(t.enum8):
    REQUEST = 0x00
    RESPONSE = 0x01


class FunctionID(t.enum8):
    SERIAL_API_GET_CAPABILITIES = 0x07
    SERIAL_API_ENTER_BOOTLOADER = 0x27


SOF = 0x01
ACK = 0x06
NAK = 0x15
CAN = 0x18


@dataclasses.dataclass(frozen=True)
class ZWaveFrame:
    type: MessageType
    function_id: FunctionID
    data: bytes

    @classmethod
    def deserialize(cls, data: bytes | bytearray) -> tuple[Self, bytes | bytearray]:
        if len(data) < 2:
            raise BufferTooShort()

        if data[0] != SOF:
            raise ValueError(f"Expected SOF, got 0x{data[0]:02x}")

        length = data[1]

        if len(data) < 2 + length:
            raise BufferTooShort()

        expected_checksum = data[1 + length]
        computed_checksum = 0xFF

        for b in data[1 : 1 + length]:
            computed_checksum ^= b

        if computed_checksum != expected_checksum:
            raise ValueError(
                f"Checksum mismatch: expected 0x{computed_checksum:02x},"
                f" got 0x{expected_checksum:02x}"
            )

        msg_type = MessageType(data[2])  # type: ignore[no-untyped-call]
        function_id = FunctionID(data[3])  # type: ignore[no-untyped-call]
        payload = bytes(data[4 : 1 + length])

        return (
            cls(type=msg_type, function_id=function_id, data=payload),
            data[2 + length :],
        )

    def serialize(self) -> bytes:
        length = 1 + 1 + len(self.data) + 1
        data = (
            bytes([length])
            + self.type.serialize()
            + self.function_id.serialize()
            + self.data
        )

        checksum = 0xFF
        for b in data:
            checksum ^= b

        return bytes([SOF]) + data + bytes([checksum])


class ZWaveProtocol(SerialProtocol):
    _buffer: bytearray

    def __init__(self) -> None:
        super().__init__()
        self._pending_frames: dict[FunctionID, asyncio.Future[ZWaveFrame]] = {}

    def send_data(self, data: bytes) -> None:
        assert self._transport is not None
        _LOGGER.debug("Sending data %s", data)
        self._transport.write(data)

    def data_received(self, data: bytes) -> None:
        super().data_received(data)

        while self._buffer:
            byte = self._buffer[0]

            if byte == ACK:
                _LOGGER.debug("Received ACK")
                self._buffer = self._buffer[1:]
                continue

            if byte in (NAK, CAN):
                _LOGGER.debug("Received 0x%02x", byte)
                self._buffer = self._buffer[1:]
                continue

            if byte != SOF:
                _LOGGER.debug("Discarding unexpected byte 0x%02x", byte)
                self._buffer = self._buffer[1:]
                continue

            try:
                frame, new_buffer = ZWaveFrame.deserialize(self._buffer)
            except BufferTooShort:
                break
            except ValueError as e:
                _LOGGER.debug("Failed to parse buffer %r: %r", self._buffer, e)
                self._buffer = self._buffer[1:]
            else:
                self._buffer = typing.cast(bytearray, new_buffer)
                self.send_data(bytes([ACK]))
                self.frame_received(frame)

    def frame_received(self, frame: ZWaveFrame) -> None:
        _LOGGER.debug("Received frame %r", frame)

        if frame.type != MessageType.RESPONSE:
            return

        if frame.function_id not in self._pending_frames:
            _LOGGER.debug("Received unsolicited response for %r", frame.function_id)
            return

        future = self._pending_frames[frame.function_id]

        if future.done():
            _LOGGER.debug("Ignoring duplicate response for %r", frame.function_id)
            return

        future.set_result(frame)

    async def send_command(
        self,
        function_id: FunctionID,
        data: bytes = b"",
        *,
        retries: int = 2,
        timeout: float = COMMAND_TIMEOUT,
        retry_delay: float = 0.1,
    ) -> ZWaveFrame:
        assert function_id not in self._pending_frames

        future: asyncio.Future[ZWaveFrame] = asyncio.get_running_loop().create_future()
        self._pending_frames[function_id] = future

        try:
            for attempt in range(retries + 1):
                frame = ZWaveFrame(
                    type=MessageType.REQUEST, function_id=function_id, data=data
                )

                _LOGGER.debug("Sending frame %r", frame)
                self.send_data(frame.serialize())

                try:
                    async with asyncio_timeout(timeout):
                        return await future
                except asyncio.TimeoutError:
                    _LOGGER.debug(
                        "Failed to send %r, trying again in %0.2fs (attempt %s of %s)",
                        frame,
                        retry_delay,
                        attempt + 1,
                        retries + 1,
                    )

                    if attempt >= retries:
                        raise

                    await asyncio.sleep(retry_delay)
        finally:
            self._pending_frames.pop(function_id, None)

        # Unreachable
        assert False

    async def probe(self) -> Version:
        rsp = await self.send_command(FunctionID.SERIAL_API_GET_CAPABILITIES)

        major = rsp.data[0]
        minor = rsp.data[1]

        return Version(f"{major}.{minor}")

    async def enter_bootloader(self) -> None:
        """Reboot the device into the bootloader. No response is expected."""
        frame = ZWaveFrame(
            type=MessageType.REQUEST,
            function_id=FunctionID.SERIAL_API_ENTER_BOOTLOADER,
            data=b"",
        )
        self.send_data(frame.serialize())
