from __future__ import annotations

from dataclasses import dataclass, field
from typing import Iterable

from modbus_tk.defines import READ_COILS, WRITE_MULTIPLE_COILS, WRITE_SINGLE_COIL

from matterlab_relays.r421b16_relay import R421B16Relay
from matterlab_serial_device import ScriptedSerialTransport
from matterlab_serial_device.transport import SerialTransport, SerialTransportConfig


@dataclass
class RelayBankSimState:
    size: int
    channels: list[bool] = field(default_factory=list)

    def __post_init__(self) -> None:
        if not self.channels:
            self.channels = [False] * self.size
        elif len(self.channels) < self.size:
            self.channels.extend([False] * (self.size - len(self.channels)))
        else:
            self.channels = list(self.channels[: self.size])

    def set_channel(self, channel: int, value: bool, *, one_based: bool = True) -> None:
        index = channel - 1 if one_based else channel
        self.channels[index] = bool(value)

    def get_channel(self, channel: int, *, one_based: bool = True) -> bool:
        index = channel - 1 if one_based else channel
        return bool(self.channels[index])


class StatefulRelayTransport(SerialTransport):
    def __init__(self, config: SerialTransportConfig, protocol) -> None:
        self.config = config
        self._protocol = protocol
        self._is_open = True
        self._pending_response = 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.write_history.append(data)
        self._pending_response = self._protocol(data)

    def read_until(self, expected: bytes = b"\n", size: int | None = None) -> bytes:
        del expected
        response = self._pending_response
        self._pending_response = b""
        if size is not None:
            response = response[:size]
        return response


class CE221ASimProtocol:
    def __init__(self, state: RelayBankSimState) -> None:
        self.state = state

    def __call__(self, write_data: bytes) -> bytes:
        if len(write_data) != 8 or write_data[:2] != bytes.fromhex("5556"):
            return b""
        channel = write_data[5]
        command = write_data[6]
        if command == 0x01:
            self.state.set_channel(channel, True)
        elif command == 0x02:
            self.state.set_channel(channel, False)
        return b""


class R421B16SimProtocol:
    def __init__(self, state: RelayBankSimState, address: int) -> None:
        self.state = state
        self.address = address

    def __call__(self, write_data: bytes) -> bytes:
        if len(write_data) < 6 or write_data[0] != self.address:
            return b""

        function_code = write_data[1]
        channel = int.from_bytes(write_data[2:4], byteorder="big")
        if function_code == 0x03:
            status = 1 if self.state.get_channel(channel) else 0
            response = bytes([self.address, 0x03, 0x02, 0x00, status])
            return R421B16Relay._cal_modbus_CRC16(response)
        if function_code == 0x06:
            value = write_data[4:6]
            if value == bytes.fromhex("0100"):
                self.state.set_channel(channel, True)
            elif value == bytes.fromhex("0200"):
                self.state.set_channel(channel, False)
            return write_data
        return b""


class SimulatedJYdaqRtuMaster:
    def __init__(self, device, state: RelayBankSimState) -> None:
        self.device = device
        self.state = state
        self.timeout: float | None = None
        self.verbose = False
        self.calls: list[tuple[int, int, int, object]] = []

    def set_timeout(self, timeout: float) -> None:
        self.timeout = timeout

    def set_verbose(self, verbose: bool) -> None:
        self.verbose = verbose

    def execute(
        self,
        address: int,
        *,
        function_code: int,
        starting_address: int,
        output_value=None,
        quantity_of_x: int | None = None,
    ):
        self.calls.append((address, function_code, starting_address, output_value if output_value is not None else quantity_of_x))
        if function_code == WRITE_SINGLE_COIL:
            self.state.set_channel(starting_address, bool(output_value), one_based=False)
            return (address, function_code, starting_address, int(bool(output_value)))
        if function_code == WRITE_MULTIPLE_COILS:
            values = [bool(x) for x in output_value]
            for offset, value in enumerate(values):
                self.state.set_channel(starting_address + offset, value, one_based=False)
            return tuple(int(value) for value in values)
        if function_code == READ_COILS:
            quantity = quantity_of_x if quantity_of_x is not None else self.state.size
            return tuple(int(self.state.get_channel(starting_address + offset, one_based=False)) for offset in range(quantity))
        raise ValueError(f"Unsupported Modbus function code: {function_code}")


def _normalize_initial_states(size: int, initial_states: Iterable[bool] | None) -> RelayBankSimState:
    channels = list(initial_states) if initial_states is not None else []
    return RelayBankSimState(size=size, channels=channels)


def create_ce221a_sim_transport_factory(*, initial_states: Iterable[bool] | None = None):
    state = _normalize_initial_states(16, initial_states)
    protocol = CE221ASimProtocol(state)

    def _factory(config: SerialTransportConfig):
        return StatefulRelayTransport(config, protocol)

    _factory.state = state
    return _factory


def create_r421b16_sim_transport_factory(*, address: int, initial_states: Iterable[bool] | None = None):
    state = _normalize_initial_states(16, initial_states)
    protocol = R421B16SimProtocol(state, address=address)

    def _factory(config: SerialTransportConfig):
        return StatefulRelayTransport(config, protocol)

    _factory.state = state
    return _factory


def create_jydaq_sim_transport_factory():
    def _factory(config: SerialTransportConfig):
        return ScriptedSerialTransport(config)

    return _factory


def create_jydaq_sim_rtu_master_factory(*, initial_states: Iterable[bool] | None = None):
    state = _normalize_initial_states(8, initial_states)

    def _factory(device):
        return SimulatedJYdaqRtuMaster(device, state)

    _factory.state = state
    return _factory
