import json
import logging
from typing import Union

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


logger = logging.getLogger(__name__)


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

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

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

        msg = self.mbcontext.MB_EasyEvtSendFull(
            subject=event.subject,
            type=event.event_type,
            headers=self._convert_dict_to_bytes(event.headers),
            xml=data,
            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.mbcontext.MB_EasySendFullMessage(
                dest_name=component,
                token=message.token,
                format=message.data_type,
                data=data,
                headers=self._convert_dict_to_bytes(message.headers),
                timeout=self._convert_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):
        if isinstance(subscriptions, list):
            subject_to_subscribe = " ".join(map(lambda x: x.subject, subscriptions))
        else:
            subject_to_subscribe = subscriptions
        self.mbcontext.MB_EasyEvtSubscribe(subject_to_subscribe.encode("utf-8"))

    def reply_message(self, message, reply_message):
        self._reply_mb_message(message.imp_message, reply_message)

    def reply_event(self, event, reply_message):
        if not event.synchronous:
            logger.error("You cannot reply an asynchronous event")
        self._reply_mb_message(event.imp_message, reply_message)
        event._replied = True

    def _reply_mb_message(self, msg, reply_message):
        # type: (MBMessage, Message) -> None
        msg.token = reply_message.token
        data = reply_message.data
        if reply_message.data is None:
            data = ""

        if isinstance(reply_message, SwaggerMessage):
            transformed_data = {
                "status": reply_message.status_code
            }

            if reply_message.body is not None:
                transformed_data["body"] = reply_message.body

            if reply_message.headers is not None:
                transformed_data["headers"] = reply_message.headers

            reply_message.data = transformed_data

        if isinstance(reply_message.data, dict):
            data = json.dumps(reply_message.data)

        self.mbcontext.MB_ReplyMessage(
            msg,
            format=reply_message.data_type,
            data=data
        )

    @staticmethod
    def _convert_mb_message_to_message(msg):
        # type: (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):
        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,        # type: Union[int, Timeout]
    ):
        if isinstance(timeout, int):
            return timeout

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