# encoding: utf-8
import logging
import sys
from threading import Thread, Condition

from ._MbContextMessageBus import MbContextMessageBus
from messagebus import Event, Message, DataType, TokenCreator
from messagehandler import MessageHandlerBuilder
from msgbus import (
    SE_SUCCESS,
    TK_CMP_TERM_NOW,
    TK_CMP_START,
    TK_EVT_EVENT,
    TK_SYS_ACK,
    TK_SYS_NAK,
    TK_HV_SERVICEONLINE,
    TK_HV_SERVICEOFFLINE,
)
from systools import sys_log_error
from typing import Dict, List, Optional

logger = logging.getLogger(__name__)


class MbContextMessageHandler(object):
    def __init__(self, message_bus, service_name, service_type, required_services, event_handler_builder):
        # type: (MbContextMessageBus, str, str, str, MessageHandlerBuilder) -> None
        self.message_bus = message_bus
        self.service_name = service_name
        self.service_type = service_type
        self.required_services = required_services
        self.event_handler_builder = event_handler_builder
        self.reentrant_subjects = {}
        self.non_reentrant_subjects = {}
        self.thread_handling_subject = {}

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

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

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

        if self.service_name and self.service_type != "":
            status = self.message_bus.mbcontext.MB_EasyWaitStart(
                "{}:{}".format(self.service_name, self.service_type),
                self.required_services
            )
            if status != SE_SUCCESS:
                sys_log_error("Terminating component after 'MB_EasyWaitStart'")
                sys.exit(status)

    def subscribe_reentrant_events(self, subjects):
        # type: (List[unicode]) -> MbContextMessageHandler
        for subject in subjects:
            self.reentrant_subjects[subject] = subject
            self.message_bus.subscribe(subject)

        return self

    def subscribe_non_reentrant_events(self, subjects):
        # type: (List[unicode]) -> MbContextMessageHandler
        for subject in subjects:
            self.non_reentrant_subjects[subject] = subject
            self.message_bus.subscribe(subject)

        return self

    def subscribe_queue_events(self, subjects):
        # type: (List[unicode]) -> MbContextMessageHandler

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

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

        return self

    def subscribe_queue_messages(self, tokens):
        # type: (List[int]) -> MbContextMessageHandler

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

        return self

    def handle_events(self):
        self.event_handler_builder.build_singletons()

        while not self.finished:
            msg = self.message_bus.mbcontext.MB_EasyGetMessage()

            # Check for component terminate request
            if (not msg) or (msg.token == TK_CMP_TERM_NOW):
                # Component terminated, warn the handler that we are terminated
                self.event_handler_builder.destroy_singletons()
                # reply the message
                self.message_bus.mbcontext.MB_EasyReplyMessage(msg)
                # Finish the loop and terminate the component
                self.finished = True
                break

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

            elif msg.token == TK_EVT_EVENT:
                params = msg.data.split(b'\x00')
                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,
                )
                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()

                    # Se o evento é não reentrante e já temos uma thread tratando outra instância do mesmo evento,
                    # respondemos e não criamos outra thread
                    if handling_thread:
                        self.message_bus.mbcontext.MB_EasyReplyMessage(msg)
                        continue

                if subject in self.queue_subjects:
                    # Se o evento é do tipo fila, enfileiramos a tupla e deixamos a thread responsável tratá-lo
                    with self.event_queue_condition:
                        self.event_queue.append(event)
                        self.event_queue_condition.notify()

                    self.message_bus.mbcontext.MB_EasyReplyMessage(msg)
                    continue

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

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

                handler_thread.start()

                # Se o evento for syncrono, deixamos a thread responsável por dar o reply na mensagem
                if not sync:
                    self.message_bus.mbcontext.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.mbcontext.MB_EasyReplyMessage(msg)
                    continue

                data = msg.data
                data_format = msg.format
                if msg.data is None or msg.data == "":
                    data = None
                    data_format = DataType.empty

                message = Message(
                    token=token,
                    data_type=data_format,
                    data=data,
                    headers=self.parse_headers(msg.headers),
                    ttl=msg.ttl,
                )
                message.imp_message = msg

                if token in self.queue_tokens:
                    # Se a mensagem é do tipo fila, enfileiramos a tupla e deixamos a thread responsável tratá-lo
                    with self.message_queue_condition:
                        self.message_queue.append(message)
                        self.message_queue_condition.notify()

                    continue

                handler_thread = Thread(target=self._handle_message, args=(message,))
                handler_thread.daemon = True
                handler_thread.start()

    def _message_queue_method(self):
        my_condition = self.message_queue_condition
        my_queue = self.message_queue

        self._handle_each_queued_object(my_condition, my_queue, self._handle_message)

    def _event_queue_method(self):
        my_condition = self.event_queue_condition
        my_queue = self.event_queue

        self._handle_each_queued_object(my_condition, my_queue, self._handle_event)

    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: # noqa
                    pass

            with my_condition:
                my_condition.wait()

    def _handle_event(self, event):
        # type: (Event) -> None
        event_handler = None
        try:
            event_handler = self.event_handler_builder.build_message_handler(None, event)
            event_handler.handle_event(self.message_bus, event)
            if event.synchronous and not hasattr(event, '_replied'):
                logger.error("Sync event without reply: {}".format(event.subject))
        finally:
            if event_handler is not None:
                self.event_handler_builder.destroy_message_handler(event_handler)

    def _handle_message(self, message):
        # type: (Message) -> None
        message_handler = None
        try:
            message_handler = self.event_handler_builder.build_message_handler(message, None)
            message_handler.handle_message(self.message_bus, message)
        finally:
            if message_handler is not None:
                self.event_handler_builder.destroy_message_handler(message_handler)

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

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