from __future__ import annotations

from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, Optional

import serial


@dataclass(frozen=True)
class SerialTransportConfig:
    port: str
    baudrate: int = 9600
    parity: str = serial.PARITY_NONE
    bytesize: int = serial.EIGHTBITS
    timeout: Optional[float] = None
    stopbits: int = serial.STOPBITS_ONE
    extra: Dict[str, Any] = field(default_factory=dict)


class SerialTransport(ABC):
    @property
    @abstractmethod
    def is_open(self) -> bool:
        """Return whether the transport is currently open."""

    @abstractmethod
    def open(self) -> None:
        """Open the transport."""

    @abstractmethod
    def close(self) -> None:
        """Close the transport."""

    @abstractmethod
    def write(self, data: bytes) -> None:
        """Write bytes to the transport."""

    @abstractmethod
    def read_until(self, expected: bytes = b"\n", size: int | None = None) -> bytes:
        """Read bytes from the transport until the expected delimiter or size limit."""


class PySerialTransport(SerialTransport):
    def __init__(self, config: SerialTransportConfig) -> None:
        self.config = config
        self._connection = serial.Serial(
            port=config.port,
            baudrate=config.baudrate,
            parity=config.parity,
            bytesize=config.bytesize,
            timeout=config.timeout,
            stopbits=config.stopbits,
            **config.extra,
        )

    @property
    def is_open(self) -> bool:
        return self._connection.is_open

    def open(self) -> None:
        self._connection.open()

    def close(self) -> None:
        self._connection.close()

    def write(self, data: bytes) -> None:
        self._connection.write(data)

    def read_until(self, expected: bytes = b"\n", size: int | None = None) -> bytes:
        return self._connection.read_until(expected=expected, size=size)


ResponseHandler = Callable[[bytes, bytes, int | None], bytes]


class ScriptedSerialTransport(SerialTransport):
    def __init__(
        self,
        config: SerialTransportConfig,
        response_map: Optional[Dict[bytes, bytes | ResponseHandler]] = None,
        default_response: bytes = b"",
    ) -> None:
        self.config = config
        self._is_open = True
        self._response_map = response_map or {}
        self._default_response = default_response
        self._last_write = b""
        self.write_history: list[bytes] = []

    @property
    def is_open(self) -> bool:
        return self._is_open

    def open(self) -> None:
        self._is_open = True

    def close(self) -> None:
        self._is_open = False

    def write(self, data: bytes) -> None:
        self._last_write = data
        self.write_history.append(data)

    def read_until(self, expected: bytes = b"\n", size: int | None = None) -> bytes:
        response = self._response_map.get(self._last_write, self._default_response)
        if callable(response):
            response = response(self._last_write, expected, size)

        if size is not None:
            response = response[:size]

        return response
