import asyncio
import logging
from typing import Callable, Coroutine

from aiospb import Clock, UtcClock
from aiospb import messages as m
from aiospb.data import DataType, Metric, MetricChange, MetricChangeRequest, ValueType
from aiospb.groups import MqttServer
from aiospb.mqtt import MqttClient, Will

logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)


class _Observer:
    def __init__(self, callback: Callable, filter: str = ""):
        self.callback = callback
        self._filter = filter

    async def notify(self, message: m.SpbMessage):
        if not self._filter or self._filter in message.topic.component_name:
            await self.callback(message)


class _MessageSorter:
    def __init__(self, node_name: str, clock: Clock, reorder_time: float = 2.0):
        self._name = node_name
        self._next_seq = 0
        self._clock = clock
        self._bd_seq = 0
        self._reorder_time = int(reorder_time * 1000)  # ms
        self._messages = {}
        self._timeout = 0

    def register_message(self, message: m.SpbMessage):
        content = message.payload.to_dict()
        if "seq" in content:  # death certificare
            seq = content["seq"]
            if message.is_a("NBIRTH"):
                self._messages.clear()
                self._next_seq = seq
                self._bd_seq = self._get_bd_seq(content["metrics"])
            self._messages[seq] = message
        else:  # death
            self._messages[None] = message

    def _get_bd_seq(self, metrics):
        for metric in metrics:
            if metric.get("name") == "bdSeq":
                return metric["value"]

    def nexts(self) -> list[m.SpbMessage]:
        output = []
        while self._messages:
            if self._next_seq in self._messages:
                output.append(self._messages.pop(self._next_seq))
                self._next_seq = 0 if self._next_seq == 255 else self._next_seq + 1
                self._timeout = self._clock.timestamp() + self._reorder_time
            elif None in self._messages:
                message = self._messages.pop(None)
                if message.payload.to_dict()["metrics"][0]["value"] != self._bd_seq:
                    continue

                output.append(message)
                self._messages.clear()
                self._next_seq = 0
            else:
                if self._clock.timestamp() > self._timeout:
                    raise TimeoutError(
                        f"There are {len(self._messages)} messagesdisordered too much time"
                    )
                break
        return output


class HostBridge:
    """Easy interface to host applications"""

    def __init__(
        self,
        hostname: str,
        mqtt_client: MqttClient,
        reorder_timeout: float = 2,
        max_delay: int = 10,
        clock: Clock | None = None,
    ):
        self._mqtt_client = mqtt_client
        self._hostname = hostname
        self._observers = []
        self._clock = clock if clock else UtcClock()
        self._state = "offline"
        self._listen_nodes = None
        self._max_delay = max_delay
        self._node_sequences = {}
        self._reorder_timeout = reorder_timeout
        self._sorters = {}

    @property
    def hostname(self) -> str:
        """Name of the host application"""
        return self._hostname

    @property
    def state(self):
        return self._state

    async def establish_session(self, group_names: list[str] | None = None):
        """Init session to listen edge nodes"""

        await m.HconnectContent(group_names).send(
            self._mqtt_client, self._clock.timestamp(), self._hostname
        )
        await m.StateContent(True).send(
            self._mqtt_client, self._clock.timestamp(), self._hostname
        )
        logger.debug(f'Host application "{self._hostname}" has established session')
        self._listening_nodes = asyncio.create_task(self._recieve_node_messages())

    def done(self):
        return self._listen_nodes is None or self._listen_nodes.done()

    def observe_nodes(
        self,
        callback: Callable[[m.SpbMessage], Coroutine[None, None, None]],
        node_filter: str = "",
    ) -> None:
        """Add one callable observer when it rece"""

        self._observers.append(_Observer(callback, node_filter))

    async def _recieve_node_messages(self):
        while True:
            try:
                message: m.SpbMessage = await self._mqtt_client.deliver_message()
                if not message.is_from_node():
                    continue

                node_name = message.topic.component_name
                if node_name not in self._sorters:
                    self._sorters[node_name] = _MessageSorter(
                        node_name, self._clock, self._reorder_timeout
                    )
                sorter = self._sorters[node_name]
                sorter.register_message(message)

                try:
                    for message in sorter.nexts():
                        for observer in self._observers:
                            await observer.notify(message)
                except TimeoutError:
                    ts = self._clock.timestamp()
                    await m.NcmdContent(
                        [
                            m.MetricDataTransfer(
                                ts, True, DataType.Boolean, name="Node Control/Rebirth"
                            )
                        ]
                    ).send(self._mqtt_client, ts, node_name)

            except Exception as e:
                logger.error("Host app has stopped receiving by mqtt error")
                logger.exception(e)
                raise e

            # delay = (self._clock.now() - message.timestamp) / 1000
            # try:
            #     await self._notify(message)
            #     logger.debug(
            #         f"Host app '{self._hostname}' has recieved {message.content_type} with delay of {delay} s"
            #     )

            #     if delay > self._max_delay:
            #         await self._notify(
            #             Message.create(
            #                 message.component_name,
            #                 WarningContent(
            #                     "ExcessOfDelay",
            #                     f"Delay (seconds) is over max {self._max_delay} s",
            #                     [delay],
            #                 ),
            #                 self._clock,
            #             )
            #         )
            #         logger.debug(
            #             f'Host app "{self._hostname}" has notified warning by delay'
            #         )
            # except Exception as e:
            #     logger.error("Host application has stopped by notification exception")
            #     logger.exception(e)
            #     raise e

    # async def _notify(self, message):
    #     awaitables = [callback(message) for callback in self._callbacks[None]]
    #     node_name = message.component_name
    #     if node_name in self._callbacks:
    #         awaitables.extend(
    #             [callback(message) for callback in self._callbacks[node_name]]
    #         )
    #     await asyncio.gather(*awaitables)

    async def request_changes_to_metrics(
        self, node_name: str, *change_requests: MetricChangeRequest
    ):
        """Request changes to metrics"""
        try:
            datatype = self._data_types[node_name]
            # m.NcmdContent([m.MetricDataTransfer(self._clock.timestamp(), change.
            #                                     for change in change_requests])
        except Exception as e:
            logger.error("Request of changes has been stopped by mqtt exception")
            logger.exception(e)
            raise e

    async def terminate_session(self):
        """Close cleanly a session"""
        if self._listening_nodes:
            self._listening_nodes.cancel()

        await m.StateContent(False).send(
            self._mqtt_client, self._clock.timestamp(), self._hostname
        )
        await m.DisconnectContent().send(
            self._mqtt_client, self._clock.timestamp(), self._hostname
        )
        logger.debug(
            f'Host application "{self._hostname}" has cleanly terminated session'
        )
