import functools
import json
import logging
import sys
from threading import (
    Condition,
    Thread,
)
from typing import (
    Callable,
    List,
    Optional,
)

from messagebus import (
    Event,
    Message,
    TokenCreator,
)
from messagehandler import MessageHandlerBuilder
from msgbus import (
    SE_SUCCESS,
    TK_CMP_RECREATE,
    TK_CMP_RELOAD,
    TK_CMP_START,
    TK_CMP_TERM_NOW,
    TK_EVT_EVENT,
    TK_HV_SERVICEOFFLINE,
    TK_HV_SERVICEONLINE,
    TK_SYS_ACK,
    TK_SYS_NAK,
)
from ._MbContextMessageBus import MbContextMessageBus

logger = logging.getLogger(__name__)


class MbContextMessageHandler(object):
    def __init__(
        self,
        message_bus: MbContextMessageBus,
        service_name: str,
        service_type: str,
        required_services: List[str],
        message_handler_builder: MessageHandlerBuilder,
        recreate_listener: Callable[[Optional[str]], None] = None,
    ) -> None:

        self.message_bus = message_bus
        self.service_name = service_name
        self.service_type = service_type
        self.required_services = required_services or []
        self.message_handler_builder = message_handler_builder
        self.recreate_listener = recreate_listener

        self.reentrant_subjects = {}
        self.non_reentrant_subjects = {}
        self.thread_handling_subject = {}

        self.queue_subjects = {}
        self.event_queue: List[Event] = []
        self.event_queue_thread: Optional[Thread] = None
        self.event_queue_condition = Condition()

        self.queue_tokens = {}
        self.message_queue: List[Message] = []
        self.message_queue_thread: Optional[Thread] = None
        self.message_queue_condition = Condition()

        self.group_to_filter: Optional[str] = None
        self.finished = False

        if self.service_name and self.service_type != "":
            status = self.message_bus.mb_context.MB_EasyWaitStart(
                "{}:{}".format(self.service_name, self.service_type),
                "|".join(required_services)
            )
            if status != SE_SUCCESS:
                logger.error("Failed to perform MB_EasyWaitStart. Terminating")
                sys.exit(status)

    def subscribe_reentrant_events(self, subjects: List[str]) -> None:

        for subject in subjects:
            self.reentrant_subjects[subject] = subject
            self.message_bus.subscribe(subject)

    def subscribe_non_reentrant_events(self, subjects: List[str]) -> None:

        for subject in subjects:
            self.non_reentrant_subjects[subject] = subject
            self.message_bus.subscribe(subject)

    def subscribe_queue_events(self, subjects: List[str]) -> None:

        subject_event_queue_thread = Thread(
            target=self._event_queue_method,
            name="Event Queue Thread",
            daemon=True
        )
        subject_event_queue_thread.start()
        self.queue_subjects = subjects

        for subject in subjects:
            self.message_bus.subscribe(subject)


    def subscribe_queue_messages(self, tokens: List[int]) -> None:

        token_message_queue_thread = Thread(
            target=self._message_queue_method,
            name="Message Queue Thread",
            daemon=True,
        )
        token_message_queue_thread.start()
        self.queue_tokens = tokens

    def handle_messages(self):
        self.message_handler_builder.build_singletons()

        while not self.finished:
            msg = self.message_bus.mb_context.MB_GetMessage()

            if not msg or msg.token == TK_CMP_TERM_NOW:
                self.message_handler_builder.destroy_singletons()
                self.message_bus.mb_context.MB_EasyReplyMessage(msg)
                self.finished = True
                break

            if msg.token in (TK_CMP_RELOAD, TK_CMP_RECREATE):
                self.message_handler_builder.destroy_singletons()
                if msg.token == TK_CMP_RECREATE and self.recreate_listener is not None:
                    new_config = None
                    if msg.data is not None and len(msg.data) > 0:
                        new_config = msg.data.decode("utf-8")
                    self.recreate_listener(new_config)

                self.message_handler_builder.build_singletons()
                self.message_bus.mb_context.MB_EasyReplyMessage(msg)
                continue

            if msg.token in (TK_CMP_START, TK_HV_SERVICEONLINE, TK_HV_SERVICEOFFLINE):
                msg.token = TK_SYS_ACK
                self.message_bus.mb_context.MB_EasyReplyMessage(msg)

            elif msg.token == TK_EVT_EVENT:
                params = msg.data.split(b"\0")
                data = params[0] if len(params) > 0 else None
                subject = params[1] if len(params) > 1 else None
                subject = subject.decode("utf-8")
                evt_type = params[2] if len(params) > 2 else None
                if evt_type == b'':
                    evt_type = None
                if evt_type is not None:
                    evt_type = evt_type.decode("utf-8")
                sync = params[3] == b'true' if len(params) > 3 else False
                source_id = int(params[4]) if len(params) > 4 else None

                event = Event(
                    subject=subject,
                    event_type=evt_type,
                    data=data,
                    source=source_id,
                    synchronous=sync,
                    headers=self.parse_headers(msg.headers),
                    ttl=msg.ttl,
                )
                setattr(event, "_imp_message", msg)

                if subject in self.non_reentrant_subjects:
                    handling_thread = False
                    if subject in self.thread_handling_subject:
                        handling_thread = self.thread_handling_subject[subject].is_alive()

                    if handling_thread:
                        self.message_bus.mb_context.MB_EasyReplyMessage(msg)
                        continue

                if subject in self.queue_subjects:
                    with self.event_queue_condition:
                        self.event_queue.append(event)
                        self.event_queue_condition.notify()

                    self.message_bus.mb_context.MB_EasyReplyMessage(msg)
                    continue

                handler_thread = Thread(
                    target=self._handle_message_or_event,
                    args=(None, event),
                    daemon=True,
                )

                if subject in self.non_reentrant_subjects:
                    self.thread_handling_subject[subject] = handler_thread

                handler_thread.start()

                if not sync:
                    self.message_bus.mb_context.MB_EasyReplyMessage(msg)
            else:
                token = msg.token
                if self.group_to_filter is not None and TokenCreator.is_in_group(token, self.group_to_filter):
                    msg.token = TK_SYS_NAK
                    self.message_bus.mb_context.MB_EasyReplyMessage(msg)
                    continue

                message = Message(
                    token=token,
                    data_type=msg.format,
                    data=msg.data,
                    headers=self.parse_headers(msg.headers),
                    ttl=msg.ttl,
                )
                setattr(message, "_imp_message", msg)

                if token in self.queue_tokens:
                    with self.message_queue_condition:
                        self.message_queue.append(message)
                        self.message_queue_condition.notify()

                    continue

                Thread(
                    target=self._handle_message_or_event,
                    args=(message,None),
                    daemon=True,
                ).start()

    def _message_queue_method(self):
        self._handle_each_queued_object(
            my_condition=self.message_queue_condition,
            my_queue=self.message_queue,
            handler_fn=self._handle_message_or_event,
        )

    def _event_queue_method(self):
        self._handle_each_queued_object(
            my_condition=self.event_queue_condition,
            my_queue=self.event_queue,
            handler_fn=functools.partial(self._handle_message_or_event, None)
        )

    def _handle_each_queued_object(self, my_condition, my_queue, handler_fn):
        while not self.finished:
            while len(my_queue) > 0:
                with my_condition:
                    obj = my_queue.pop(0)

                try:
                    handler_fn(obj)
                except (Exception,):
                    logger.exception("Exception while handling queue message")

            with my_condition:
                my_condition.wait()

    def _handle_message_or_event(
        self,
        message: Optional[Message] = None,
        event: Optional[Event] = None,
    ) -> None:

        message_handler = None
        try:
            message_handler = self.message_handler_builder.build_message_handler(
                message=message,
                event=event,
            )
            if message is not None:
                message_handler.handle_message(
                    message_bus=self.message_bus,
                    message=message,
                )
            else:
                message_handler.handle_event(
                    message_bus=self.message_bus,
                    event=event,
                )

            if event is not None and event.synchronous and not hasattr(event, '_replied'):
                logger.error("Sync event without reply: {}".format(event.subject))
        except (Exception,):
            logger.exception("Exception while handling event")
        finally:
            if message_handler is not None:
                self.message_handler_builder.destroy_message_handler(message_handler)

    @staticmethod
    def parse_headers(
        headers,         # type: bytes
    ):
        if headers is None or headers == b"":
            return {}

        try:
            parts = headers.split(b"\0")

            header_qty = int(parts[0])
            if len(parts[1:]) == header_qty * 2:
                headers = {}
                for i in range(header_qty):
                    headers[parts[1 + 2 * i].decode("utf-8")] = parts[2 + 2 * i].decode("utf-8")

                return headers
        except (Exception,):
            pass

        try:
            header_str = headers.decode("utf-8")
            return json.loads(header_str)
        except ValueError:
            return {}
