import logging
from typing import (
    Dict,
    List,
    Union,
)

from messagebus import (
    DataType,
    Event,
    Message,
    MessageBus,
    MessageBusException,
    MessageBusExceptionType,
    Timeout,
)
from msgbus import (
    FM_PARAM,
    FM_STRING,
    FM_XML,
    MBEasyContext,
    MBException,
    MBMessage,
    SE_MB_RECEIVE,
    SE_NOTFOUND,
    SE_TIMEOUT,
)

logger = logging.getLogger(__name__)


class MbContextMessageBus(MessageBus):
    def __init__(self, mb_context: MBEasyContext) -> None:
        self.mb_context = mb_context

    def publish_event(self, event: Event) -> Message:
        data = event.data
        if data is None:
            data = b""

        if event.event_type is None:
            event.event_type = ""

        msg = self.mb_context.MB_EasyEvtSendFull(
            subject=event.subject,
            type=event.event_type,
            headers=self._convert_dict_to_bytes(event.headers),
            xml=data.decode(),
            synchronous=event.synchronous,
            sourceid=event.source,
            queue=None,
            timeout=self._convert_timeout(event.timeout),
            ttl=event.ttl,
        )
        return self._convert_mb_message_to_message(msg)

    def send_message(self, component, message):
        data = message.data
        if data is None:
            data = ""

        try:
            msg = self.mb_context.MB_EasySendFullMessage(
                dest_name=component,
                token=message.token,
                format=message.data_type,
                data=data,
                headers=self._convert_dict_to_bytes(d=message.headers),
                timeout=self._convert_timeout(timeout=message.timeout),
                ttl=-1,
            )
            return self._convert_mb_message_to_message(msg)
        except MBException as ex:
            if ex.errorcode == SE_TIMEOUT:
                raise MessageBusException(MessageBusExceptionType.timeout)
            elif ex.errorcode == SE_MB_RECEIVE:
                raise MessageBusException(MessageBusExceptionType.receive_error)
            elif ex.errorcode == SE_NOTFOUND:
                raise MessageBusException(MessageBusExceptionType.not_found)
            else:
                raise MessageBusException(MessageBusExceptionType.unknown, ex=ex)

    def subscribe(
        self,
        subscriptions: Union[str, List[str]]
    ):
        if isinstance(subscriptions, list):
            subject_to_subscribe = " ".join(map(lambda x: x.subject, subscriptions))
        else:
            subject_to_subscribe = subscriptions

        self.mb_context.MB_EasyEvtSubscribe(subject_to_subscribe)

    def reply_message(
        self,
        message: Message,
        reply_message: Message,
    ):
        self._reply_mb_message(
            msg=getattr(message, "_imp_message"),
            reply_message=reply_message,
        )

    def reply_event(
        self,
        event: Event,
        reply_message: Message,
    ):
        if not event.synchronous:
            logger.error("You cannot reply an asynchronous event")
            return

        self._reply_mb_message(
            msg=getattr(event, "_imp_message"),
            reply_message=reply_message,
        )

    def _reply_mb_message(
        self,
        msg: MBMessage,
        reply_message: Message,
    ) -> None:
        if hasattr(msg, "_replied"):
            logger.error("This message was already replied")
            return

        setattr(msg, "_replied", True)

        msg.token = reply_message.token
        self.mb_context.MB_ReplyMessage(
            msg,
            format=reply_message.data_type,
            data=reply_message.data or b"",
        )

    @staticmethod
    def _convert_mb_message_to_message(msg: MBMessage) -> Message:

        data_format = msg.format
        data = msg.data
        if msg.format == FM_PARAM:
            if msg.data == "" or msg.data is None:
                data_format = DataType.empty
                data = None
            else:
                data_format = DataType.param
        elif msg.format == FM_XML:
            data_format = DataType.xml
        elif msg.format == FM_STRING:
            data_format = DataType.string

        return Message(
            token=msg.token,
            data_type=data_format,
            data=data,
        )

    @staticmethod
    def _convert_dict_to_bytes(d: Dict[str, str]) -> bytes:
        if d is None:
            return b""

        ret = str(len(d)).encode("utf-8") + b"\0"
        for key in d.keys():
            ret += key.encode("utf-8") + b"\0" + d[key].encode("utf-8") + b"\0"

        ret = ret[:-1]
        return ret

    @staticmethod
    def _convert_timeout(
        timeout: Union[int, Timeout]
    ) -> int:

        if isinstance(timeout, int):
            return timeout

        return (
            60 * 1000 * 1000 * timeout.minutes +
            1000 * 1000 * timeout.seconds +
            1000 * timeout.milliseconds +
            timeout.microseconds
        )
