"""Implementation of a Threaded Modbus Server."""
from __future__ import annotations

from collections.abc import Callable

from ..datastore import ModbusServerContext
from ..framer import FramerType
from ..pdu import ModbusPDU
from ..pdu.device import ModbusDeviceIdentification
from ..simulator import SimDevice
from ..transport import CommParams, CommType
from .base import ModbusBaseServer


class ModbusTcpServer(ModbusBaseServer):
    """A modbus threaded tcp socket server.

    .. tip::
        Remember to call serve_forever to start server.
    """

    def __init__(  # pylint: disable=too-many-arguments
        self,
        context: ModbusServerContext | SimDevice | list[SimDevice],
        *,
        framer=FramerType.SOCKET,
        identity: ModbusDeviceIdentification | None = None,
        address: tuple[str, int] = ("", 502),
        ignore_missing_devices: bool = False,
        broadcast_enable: bool = False,
        trace_packet: Callable[[bool, bytes], bytes] | None = None,
        trace_pdu: Callable[[bool, ModbusPDU], ModbusPDU] | None = None,
        trace_connect: Callable[[bool], None] | None = None,
        custom_pdu: list[type[ModbusPDU]] | None = None,
    ):
        """Initialize the socket server.

        If the identify structure is not passed in, the ModbusControlBlock
        uses its own empty structure.

        :param context: The ModbusServerContext datastore
        :param framer: The framer strategy to use
        :param identity: An optional identify structure
        :param address: An optional (interface, port) to bind to.
        :param ignore_missing_devices: True to not send errors on a missing device
        :param broadcast_enable: True to treat dev_id 0 as broadcast address,
                        False to treat 0 as any other dev_id
        :param trace_packet: Called with bytestream received/to be sent
        :param trace_pdu: Called with PDU received/to be sent
        :param trace_connect: Called when connected/disconnected
        :param custom_pdu: list of ModbusPDU custom classes
        """
        params = getattr(
            self,
            "tls_setup",
            CommParams(
                comm_type=CommType.TCP,
                comm_name="server_listener",
                reconnect_delay=0.0,
                reconnect_delay_max=0.0,
                timeout_connect=0.0,
            ),
        )
        params.source_address = address
        super().__init__(
            params,
            context,
            ignore_missing_devices,
            broadcast_enable,
            identity,
            framer,
            trace_packet,
            trace_pdu,
            trace_connect,
            custom_pdu,
        )


class ModbusTlsServer(ModbusTcpServer):
    """A modbus threaded tls socket server.

    .. tip::
        Remember to call serve_forever to start server.
    """

    def __init__(  # pylint: disable=too-many-arguments
        self,
        context: ModbusServerContext | SimDevice | list[SimDevice],
        *,
        framer=FramerType.TLS,
        identity: ModbusDeviceIdentification | None = None,
        address: tuple[str, int] = ("", 502),
        sslctx=None,
        certfile=None,
        keyfile=None,
        password=None,
        ignore_missing_devices=False,
        broadcast_enable=False,
        trace_packet: Callable[[bool, bytes], bytes] | None = None,
        trace_pdu: Callable[[bool, ModbusPDU], ModbusPDU] | None = None,
        trace_connect: Callable[[bool], None] | None = None,
        custom_pdu: list[type[ModbusPDU]] | None = None,
    ):
        """Overloaded initializer for the socket server.

        If the identify structure is not passed in, the ModbusControlBlock
        uses its own empty structure.

        :param context: The ModbusServerContext datastore
        :param framer: The framer strategy to use
        :param identity: An optional identify structure
        :param address: An optional (interface, port) to bind to.
        :param sslctx: The SSLContext to use for TLS (default None and auto
                       create)
        :param certfile: The cert file path for TLS (used if sslctx is None)
        :param keyfile: The key file path for TLS (used if sslctx is None)
        :param password: The password for for decrypting the private key file
        :param ignore_missing_devices: True to not send errors on a  missing device
        :param broadcast_enable: True to treat dev_id 0 as broadcast address,
                        False to treat 0 as any other dev_id
        :param trace_packet: Called with bytestream received/to be sent
        :param trace_pdu: Called with PDU received/to be sent
        :param trace_connect: Called when connected/disconnected
        :param custom_pdu: list of ModbusPDU custom classes
        """
        self.tls_setup = CommParams(
            comm_type=CommType.TLS,
            comm_name="server_listener",
            reconnect_delay=0.0,
            reconnect_delay_max=0.0,
            timeout_connect=0.0,
            sslctx=CommParams.generate_ssl(
                True, certfile, keyfile, password, sslctx=sslctx
            ),
        )
        super().__init__(
            context,
            framer=framer,
            identity=identity,
            address=address,
            ignore_missing_devices=ignore_missing_devices,
            broadcast_enable=broadcast_enable,
            trace_packet=trace_packet,
            trace_pdu=trace_pdu,
            trace_connect=trace_connect,
            custom_pdu=custom_pdu,
        )


class ModbusUdpServer(ModbusBaseServer):
    """A modbus threaded udp socket server.

    .. tip::
        Remember to call serve_forever to start server.
    """

    def __init__(  # pylint: disable=too-many-arguments
        self,
        context: ModbusServerContext | SimDevice | list[SimDevice],
        *,
        framer=FramerType.SOCKET,
        identity: ModbusDeviceIdentification | None = None,
        address: tuple[str, int] = ("", 502),
        ignore_missing_devices: bool = False,
        broadcast_enable: bool = False,
        trace_packet: Callable[[bool, bytes], bytes] | None = None,
        trace_pdu: Callable[[bool, ModbusPDU], ModbusPDU] | None = None,
        trace_connect: Callable[[bool], None] | None = None,
        custom_pdu: list[type[ModbusPDU]] | None = None,
    ):
        """Overloaded initializer for the socket server.

        If the identify structure is not passed in, the ModbusControlBlock
        uses its own empty structure.

        :param context: The ModbusServerContext datastore
        :param framer: The framer strategy to use
        :param identity: An optional identify structure
        :param address: An optional (interface, port) to bind to.
        :param ignore_missing_devices: True to not send errors on a  missing device
        :param broadcast_enable: True to treat dev_id 0 as broadcast address,
                            False to treat 0 as any other dev_id
        :param trace_packet: Called with bytestream received/to be sent
        :param trace_pdu: Called with PDU received/to be sent
        :param trace_connect: Called when connected/disconnected
        :param custom_pdu: list of ModbusPDU custom classes
        """
        # ----------------
        params = CommParams(
            comm_type=CommType.UDP,
            comm_name="server_listener",
            source_address=address,
            reconnect_delay=0.0,
            reconnect_delay_max=0.0,
            timeout_connect=0.0,
        )
        super().__init__(
            params,
            context,
            ignore_missing_devices,
            broadcast_enable,
            identity,
            framer,
            trace_packet,
            trace_pdu,
            trace_connect,
            custom_pdu,
        )


class ModbusSerialServer(ModbusBaseServer):
    """A modbus threaded serial socket server.

    .. tip::
        Remember to call serve_forever to start server.
    """

    def __init__(
        self,
        context: ModbusServerContext | SimDevice | list[SimDevice],
        *,
        framer: FramerType = FramerType.RTU,
        ignore_missing_devices: bool = False,
        identity: ModbusDeviceIdentification | None = None,
        broadcast_enable: bool = False,
        trace_packet: Callable[[bool, bytes], bytes] | None = None,
        trace_pdu: Callable[[bool, ModbusPDU], ModbusPDU] | None = None,
        trace_connect: Callable[[bool], None] | None = None,
        custom_pdu: list[type[ModbusPDU]] | None = None,
        **kwargs
    ):
        """Initialize the socket server.

        If the identity structure is not passed in, the ModbusControlBlock
        uses its own empty structure.
        :param context: The ModbusServerContext datastore
        :param framer: The framer strategy to use, default FramerType.RTU
        :param identity: An optional identify structure
        :param port: The serial port to attach to
        :param stopbits: The number of stop bits to use
        :param bytesize: The bytesize of the serial messages
        :param parity: Which kind of parity to use
        :param baudrate: The baud rate to use for the serial device
        :param timeout: The timeout to use for the serial device
        :param handle_local_echo: (optional) Discard local echo from dongle.
        :param ignore_missing_devices: True to not send errors on a  missing device
        :param broadcast_enable: True to treat dev_id 0 as broadcast address,
                            False to treat 0 as any other dev_id
        :param reconnect_delay: reconnect delay in seconds
        :param trace_packet: Called with bytestream received/to be sent
        :param trace_pdu: Called with PDU received/to be sent
        :param trace_connect: Called when connected/disconnected
        :param custom_pdu: list of ModbusPDU custom classes
        :param allow_multiple_devices: True if the rs485 have multiple devices connected.
                    **Remark** only works with baudrates <= 38.400 and with an error free RS485.
        """
        baudrate = kwargs.get("baudrate", 19200)
        params = CommParams(
            comm_type=CommType.SERIAL,
            comm_name="server_listener",
            reconnect_delay=kwargs.get("reconnect_delay", 2),
            reconnect_delay_max=0.0,
            timeout_connect=kwargs.get("timeout", 3),
            source_address=(kwargs.get("port", 0), 0),
            bytesize=kwargs.get("bytesize", 8),
            parity=kwargs.get("parity", "N"),
            baudrate=baudrate,
            stopbits=kwargs.get("stopbits", 1),
            handle_local_echo=kwargs.get("handle_local_echo", False),
        )
        super().__init__(
            params,
            context,
            ignore_missing_devices,
            broadcast_enable,
            identity,
            framer,
            trace_packet,
            trace_pdu,
            trace_connect,
            custom_pdu,
        )
        self.allow_multiple_devices = kwargs.get("allow_multiple_devices", False)
        if self.allow_multiple_devices:
            if baudrate > 38400:
                raise TypeError("allow_multiple_devices only allowed with baudrate <= 38.400")
            if framer != FramerType.RTU:
                raise TypeError("allow_multiple_devices only allowed with FramerType.RTU")
