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

import asyncio
import traceback

from ..constants import ExcCodes
from ..exceptions import ModbusIOException, NoSuchIdException
from ..logging import Log
from ..pdu import ExceptionResponse
from ..transaction import TransactionManager
from ..transport import CommParams


class ServerRequestHandler(TransactionManager):
    """Handle client connection."""

    def __init__(self, owner, trace_packet, trace_pdu, trace_connect):
        """Initialize."""
        params = CommParams(
            comm_name="server",
            comm_type=owner.comm_params.comm_type,
            reconnect_delay=0.0,
            reconnect_delay_max=0.0,
            timeout_connect=0.0,
            host=owner.comm_params.source_address[0],
            port=owner.comm_params.source_address[1],
            handle_local_echo=owner.comm_params.handle_local_echo,
        )
        self.server = owner
        self.running = False
        framer = owner.framer(owner.decoder)
        if owner.allow_multiple_devices:
            framer.setMultidrop(owner.context.device_ids())
        super().__init__(
            params,
            framer,
            0,
            True,
            trace_packet,
            trace_pdu,
            trace_connect,
        )

    def callback_disconnected(self, exc: Exception | None) -> None:
        """Call when connection is lost."""
        super().callback_disconnected(exc)
        if exc is None:
            Log.debug(
                "Handler for stream [{}] has been canceled", self.comm_params.comm_name
            )
        else:
            Log.debug(
                "Client Disconnection {} due to {}",
                self.comm_params.comm_name,
                exc,
            )
        self.running = False

    def callback_data(self, data: bytes, addr: tuple | None = None) -> int:
        """Handle received data."""
        try:
            used_len = super().callback_data(data, addr)
        except ModbusIOException:
            response = ExceptionResponse(
                40,
                exception_code=ExcCodes.ILLEGAL_FUNCTION
            )
            self.server_send(response, 0)
            return(len(data))
        if self.last_pdu:
            self.loop.call_soon(self.handle_later)
        return used_len

    def handle_later(self):
        """Change sync (async not allowed in call_soon) to async."""
        asyncio.run_coroutine_threadsafe(self.handle_request(), self.loop)

    async def handle_request(self):
        """Handle request."""
        if not self.last_pdu:
            return
        try:
            if self.server.broadcast_enable and not self.last_pdu.dev_id:
                # if broadcasting then execute on all device contexts,
                # note response will be ignored
                for dev_id in self.server.context.device_ids():
                    await self.last_pdu.datastore_update(self.server.context, dev_id)
                return
            response = await self.last_pdu.datastore_update(self.server.context, self.last_pdu.dev_id)

        except NoSuchIdException:
            if self.server.ignore_missing_devices:
                Log.debug("ignoring request for unknown device id: {}", self.last_pdu.dev_id)
                return  # the client will simply timeout waiting for a response
            Log.error("requested device id does not exist: {}", self.last_pdu.dev_id)
            response = ExceptionResponse(self.last_pdu.function_code, ExcCodes.GATEWAY_NO_RESPONSE)
        except Exception as exc:  # pylint: disable=broad-except
            Log.error(
                "Datastore unable to fulfill request: {}; {}",
                exc,
                traceback.format_exc(),
            )
            response = ExceptionResponse(self.last_pdu.function_code, ExcCodes.DEVICE_FAILURE)
        response.transaction_id = self.last_pdu.transaction_id
        response.dev_id = self.last_pdu.dev_id
        self.server_send(response, self.last_addr)

    def server_send(self, pdu, addr):
        """Send message."""
        if not pdu:
            Log.debug("Skipping sending response!!")
        else:
            self.pdu_send(pdu, addr=addr)
