import time
import uuid
from logging import Logger
from xml.etree import cElementTree as eTree
from threading import (
    Lock,
    Thread,
)
from typing import (
    List,
    Optional,
)

import cfgtools
from xml.etree.ElementTree import Element

from messagehandler import MessageHandler
from messagebus import (
    AckMessage,
    DefaultToken,
    Event,
    Message,
    MessageBus,
    MessageBusException,
    NakMessage,
    TokenCreator,
    TokenPriority,
)
from msgbus import (
    FM_PARAM,
    MBEasyContext
)
from model import (
    Kds,
    KdsModel,
    KdsView,
    PickupConfig,
)

from services import DeliveryService

MSG_GRP_KDS = '0B'
TK_KDS_GETMODEL = TokenCreator.create_token(TokenPriority.high, MSG_GRP_KDS, "1")
TK_KDS_UPDATE_VIEW = TokenCreator.create_token(TokenPriority.high, MSG_GRP_KDS, "2")
TK_KDS_REFRESH = TokenCreator.create_token(TokenPriority.high, MSG_GRP_KDS, "3")
TK_KDS_REFRESH_END = TokenCreator.create_token(TokenPriority.high, MSG_GRP_KDS, "4")
TK_KDS_SET_PROD_STATE = TokenCreator.create_token(TokenPriority.high, MSG_GRP_KDS, "5")
TK_KDS_UNDO_SERVE = TokenCreator.create_token(TokenPriority.high, MSG_GRP_KDS, "6")
TK_KDS_TOGGLE_TAG_LINE = TokenCreator.create_token(TokenPriority.high, MSG_GRP_KDS, "8")
TK_KDS_CHANGE_VIEW = TokenCreator.create_token(TokenPriority.high, MSG_GRP_KDS, "A")
TK_KDS_BUMP_BAR_COMMAND = TokenCreator.create_token(TokenPriority.high, MSG_GRP_KDS, "B")
TK_KDS_BUMP_ZOOM_NEXT = TokenCreator.create_token(TokenPriority.high, MSG_GRP_KDS, "C")
TK_KDS_RELOAD = TokenCreator.create_token(TokenPriority.high, MSG_GRP_KDS, "14")
TK_KDS_SELECT_ORDER = TokenCreator.create_token(TokenPriority.high, MSG_GRP_KDS, "20")
TK_KDS_UPDATE_STATISTICS = TokenCreator.create_token(TokenPriority.high, MSG_GRP_KDS, "21")

MSG_GRP_PRODUCTION = "0C"
TK_PROD_REFRESH_VIEW = TokenCreator.create_token(TokenPriority.low, MSG_GRP_PRODUCTION, "1")
TK_PROD_SET_ORDER_STATE = TokenCreator.create_token(TokenPriority.low, MSG_GRP_PRODUCTION, "2")
TK_PROD_UNDO = TokenCreator.create_token(TokenPriority.low, MSG_GRP_PRODUCTION, "7")
TK_PROD_TOGGLE_TAG_LINE = TokenCreator.create_token(TokenPriority.low, MSG_GRP_PRODUCTION, "8")
TK_PROD_GET_STATISTICS = TokenCreator.create_token(TokenPriority.low, MSG_GRP_PRODUCTION, "102")

NEW_PRODUCTION_COMP = "ProductionSystem"
NO_KDS_FOUND_MESSAGE = "No KDS found"


kds_list_lock = Lock()


class ProxyMessageHandler(MessageHandler):
    def __init__(
        self,
        comp_name,    # type: str
        mb_context,   # type: MBEasyContext
        message_bus,  # type: MessageBus
        config,       # type: cfgtools.Group
        logger        # type: Logger
    ):
        # type: (...) -> None

        self.comp_name = comp_name
        self.kds_list = None
        self.brands_loaded = False
        self.mb_context = mb_context
        self.message_bus = message_bus
        self.logger = logger
        self.commands_uuid = []
        self.config = config

        self.statistics_cache = ''
        init_thread = Thread(target=self.get_current_statistics)
        init_thread.daemon = True
        init_thread.start()

    def handle_event(self, message_bus, event):
        return

    def handle_message(self, message_bus, message):
        data = message.data.split('\0')
        resp = None

        if message.token == TK_KDS_SELECT_ORDER:
            kds_id = int(data[0])
            self.logger.info("Receiving TK_KDS_SELECT_ORDER for KDS - {:02d}".format(kds_id))

            # TODO: Change Selected Order and Store it
            message.token = DefaultToken.TK_SYS_ACK

        elif message.token == TK_KDS_GETMODEL:
            resp = self.handle_kds_get_model(data)
        elif message.token == TK_KDS_CHANGE_VIEW:
            resp = self.handle_kds_change_view(data)
        elif message.token == TK_KDS_UPDATE_VIEW:
            resp = self.handle_kds_update_view(data, message_bus)
        elif message.token == TK_KDS_REFRESH:
            resp = self.handle_kds_refresh(data, message_bus)
        elif message.token == TK_KDS_REFRESH_END:
            resp = self.handle_kds_refresh_end(data, message_bus)
        elif message.token == TK_KDS_SET_PROD_STATE:
            resp = self.handle_kds_set_prod_state(data, message_bus)
        elif message.token == TK_KDS_UNDO_SERVE:
            resp = self.handle_kds_undo_serve(data, message_bus)
        elif message.token == TK_KDS_UPDATE_STATISTICS:
            resp = self.handle_kds_update_statistics(data, message_bus)
        elif message.token == TK_KDS_BUMP_BAR_COMMAND:
            resp = self.handle_kds_bump_bar_command(data, message_bus)
        elif message.token == TK_KDS_BUMP_ZOOM_NEXT:
            resp = self.handle_kds_zoom_next(data)
        elif message.token == TK_KDS_TOGGLE_TAG_LINE:
            resp = self.handle_kds_toggle_tag_line(data, message_bus)
        elif message.token == TK_KDS_RELOAD:
            resp = self.handle_kds_reload(data, message_bus)
        else:
            resp = self.forward_message(message, message_bus)

        message_bus.reply_message(message, resp)
        self.logger.info("Message replied")

    def handle_kds_get_model(self, data):
        self.logger.info("TK_KDS_GETMODEL - Received")

        kds_id = data[0]
        kds = self.get_kds(kds_id)

        if not kds:
            self.logger.info("TK_KDS_GETMODEL - KdsId: {} not found".format(kds_id))
            return NakMessage(NO_KDS_FOUND_MESSAGE, 0)

        self.logger.info("TK_KDS_GETMODEL - KdsId: {}".format(kds_id))

        kds_model = KdsModel(kds, kds_id)
        kds_model_xml = kds_model.get_model_xml()
        kds_model_xml.append(eTree.Element("Statistics", text=self.statistics_cache))
        resp = AckMessage(eTree.tostring(kds_model_xml, encoding='utf8', method='xml'), 0)

        self.logger.info("TK_KDS_GETMODEL - Finished")
        return resp

    def handle_kds_change_view(self, data):
        self.logger.info("TK_KDS_CHANGE_VIEW - Received")

        kds_id = data[0]
        view_name = data[1]
        cmd_uuid = data[2] if len(data) > 2 else '0'
        kds = self.get_kds(kds_id)

        if not kds:
            self.logger.info("TK_KDS_CHANGE_VIEW - KdsId: {} not found".format(kds_id))
            return NakMessage(NO_KDS_FOUND_MESSAGE, 0)

        if self._is_uuid_unique(cmd_uuid):
            self.set_selected_view(kds, view_name)
            self.logger.info("TK_KDS_CHANGE_VIEW - KdsId: {}".format(kds_id))

        kds_model = KdsModel(kds, kds_id)
        kds_model_xml = kds_model.get_model_xml()
        resp = AckMessage(eTree.tostring(kds_model_xml, encoding='utf8', method='xml'), 0)

        self.logger.info("TK_KDS_CHANGE_VIEW - Finished")
        return resp

    def get_kds(self, kds_id):
        return next((x for x in self._get_kds_list(need_brands=True) if x.name == kds_id), None)

    @staticmethod
    def get_bump_bar_command(bump_bar_commands, command_code):
        return next((x for x in bump_bar_commands if command_code in x.keys), None)

    @staticmethod
    def set_selected_view(kds, view_name):
        for view in kds.views:
            view.selected = view.name == view_name

        has_selected_view = next((x for x in kds.views if x.selected), None)
        if not has_selected_view:
            kds.views[0].selected = True

    @staticmethod
    def set_view_zoom(kds, production_view):
        view = next((x for x in kds.views if x.production_view == production_view), None)
        if not view:
            return

        view.selected_zoom += 1
        if view.selected_zoom == len(view.zoom_levels):
            view.selected_zoom = 0

    def handle_kds_update_view(self, data, message_bus):
        self.logger.info("TK_KDS_UPDATE_VIEW - Received")

        production_view = data[0]
        xml = eTree.XML(data[1])
        order_id = xml.get("order_id")

        self.logger.info("TK_KDS_UPDATE_VIEW - ProductionView: {} / OrderId: {}".format(production_view, order_id))

        view_update_xml = self.create_view_update_xml(production_view, xml)
        for kds in self.get_kds_list_with_production_view(production_view):
            self.logger.info("TK_KDS_UPDATE_VIEW - Sending event for KDS: {} / OrderId: {}".format(kds, order_id))

            event_subject = "KDS" + kds
            event_type = "PROD_ORDER_" + order_id + "_" + production_view
            event = self.send_event(event_subject, event_type, view_update_xml)
            message_bus.publish_event(event)

        self.logger.info("TK_KDS_UPDATE_VIEW - Finished")

        return AckMessage('', 0)

    def handle_kds_refresh(self, data, message_bus):
        self.logger.info("TK_KDS_REFRESH - Received")
        response = AckMessage('', 0)

        kds_id = data[0]
        for view in self.get_production_views_with_kds_id(kds_id):
            self.logger.info("TK_KDS_REFRESH - Sending event for View: {}".format(view))

            message = Message(TK_PROD_REFRESH_VIEW, FM_PARAM, str(view), 3000000)
            reply = message_bus.send_message(NEW_PRODUCTION_COMP, message)
            if reply.token != DefaultToken.TK_SYS_ACK:
                response = NakMessage()

        self.logger.info("TK_KDS_REFRESH - Finished")
        return response

    def handle_kds_set_prod_state(self, data, message_bus):
        self.logger.info("TK_KDS_SET_PROD_STATE - Received")

        kds_id = data[0]
        order_id = data[1]
        prod_state = data[2]
        view = data[3]
        pickup_area_info = data[4] if len(data) > 4 else ''
        cmd_uuid = data[5] if len(data) > 5 else '0'

        if self._is_uuid_unique(cmd_uuid):
            log_message = "TK_KDS_SET_PROD_STATE - Sending event to KDS: {} / OrderId: {} / ProdState {}"
            self.logger.info(log_message.format(kds_id, order_id, prod_state))

            message_data = order_id + "\0" + view + "\0" + prod_state + "\0" + pickup_area_info
            message = Message(TK_PROD_SET_ORDER_STATE, FM_PARAM, message_data, 3000000)
            message_bus.send_message(NEW_PRODUCTION_COMP, message)

        self.logger.info("TK_KDS_SET_PROD_STATE - Finished")
        return AckMessage('', 0)

    def handle_kds_zoom_next(self, data):
        self.logger.info("TK_KDS_BUMP_ZOOM_NEXT - Received")

        kds_id = data[0]
        production_view = data[1]
        cmd_uuid = data[2] if len(data) > 2 else '0'
        kds = self.get_kds(kds_id)

        if not kds:
            self.logger.info("TK_KDS_BUMP_ZOOM_NEXT - KdsId: {} not found".format(kds_id))
            return NakMessage(NO_KDS_FOUND_MESSAGE, 0)

        if self._is_uuid_unique(cmd_uuid):
            self.set_view_zoom(kds, production_view)
            self.logger.info("TK_KDS_BUMP_ZOOM_NEXT - KdsId: {}".format(kds_id))

        kds_model = KdsModel(kds, kds_id)
        kds_model_xml = kds_model.get_model_xml()
        resp = AckMessage(eTree.tostring(kds_model_xml, encoding='utf8', method='xml'), 0)

        self.logger.info("TK_KDS_BUMP_ZOOM_NEXT - Finished")
        return resp

    def handle_kds_refresh_end(self, data, message_bus):
        self.logger.info("TK_KDS_REFRESH_END - Received")

        production_view = data[0]
        for kds in self.get_kds_list_with_production_view(production_view):
            self.logger.info("TK_KDS_REFRESH_END - Sending event to KDS: {} - VIEW: {}".format(kds, production_view))

            event_subject = "KDS" + kds
            event_type = "REFRESH_END_" + production_view
            refresh_end_xml = eTree.XML("<RefreshEnd name=\"{}\" updated=\"true\"/>".format(production_view))
            event = self.send_event(event_subject, event_type, refresh_end_xml)
            message_bus.publish_event(event)

        self.logger.info("TK_KDS_REFRESH_END - Finished")

        return AckMessage('', 0)

    def handle_kds_reload(self, data, message_bus):
        self.logger.info("TK_KDS_RELOAD - Received")

        production_view = data[0]
        for kds in self.get_kds_list_with_production_view(production_view):
            self.logger.info("TK_KDS_RELOAD - Sending event to KDS: {}".format(kds))

            event_subject = "KDS" + kds
            event_type = "RELOAD_" + production_view
            refresh_end_xml = eTree.XML("<Reload name=\"{}\"/>".format(production_view))
            event = self.send_event(event_subject, event_type, refresh_end_xml)
            message_bus.publish_event(event)

        self.logger.info("TK_KDS_RELOAD - Finished")

        return AckMessage('', 0)

    def handle_kds_toggle_tag_line(self, data, message_bus):
        self.logger.info("TK_KDS_TOGGLE_TAG_LINE - Received")

        kds_id = data[0]
        order_id = data[1]
        line_number = data[2]
        tag_name = data[3]
        view = data[4]
        cmd_uuid = data[5] if len(data) > 5 else '0'

        if self._is_uuid_unique(cmd_uuid):
            log_message = "TK_KDS_TOGGLE_TAG_LINE - Sending event to KDS: {} / OrderId: {} / LineNumber {} / TagName {}"
            self.logger.info(log_message.format(kds_id, order_id, line_number, tag_name))

            message_data = order_id + "\0" + view + "\0" + line_number + "\0" + tag_name
            message = Message(TK_PROD_TOGGLE_TAG_LINE, FM_PARAM, message_data, 3000000)
            message_bus.send_message(NEW_PRODUCTION_COMP, message)

        self.logger.info("TK_KDS_TOGGLE_TAG_LINE - Finished")
        return AckMessage('', 0)

    def handle_kds_update_statistics(self, data, message_bus):
        self.logger.info("TK_KDS_UPDATE_STATISTICS - Received")

        try:
            statistics = data[0]
            self.statistics_cache = statistics
        except ValueError:
            pass

        for kds_id in self.get_all_kds_ids():
            event_data = eTree.tostring(self.create_statistics_event_from_json(self.statistics_cache))
            event = Event(subject='KDS{}'.format(kds_id), event_type='Statistics', data=event_data)
            message_bus.publish_event(event)

        self.logger.info("TK_KDS_UPDATE_STATISTICS - Finished")

        return AckMessage('', 0)

    def handle_kds_bump_bar_command(self, data, message_bus):
        self.logger.info("TK_KDS_BUMP_BAR_COMMAND - Received")

        kds_id = data[0]
        bump_bar_name = data[1]
        command_code = data[2]

        log_message = "TK_KDS_BUMP_BAR_COMMAND - KdsId: {} / BumpBar: {} / Command: {}"
        self.logger.info(log_message.format(kds_id, bump_bar_name, command_code))

        kds = self.get_kds(kds_id)
        if not kds:
            self.logger.info("TK_KDS_BUMP_BAR_COMMAND - KdsId: {} not found".format(kds_id))
            return NakMessage(NO_KDS_FOUND_MESSAGE, 0)

        bump_bar = kds.bump_bar
        command = self.get_bump_bar_command(bump_bar.commands, command_code)
        if not command:
            self.logger.info("TK_KDS_BUMP_BAR_COMMAND - Key not mapped")
            return AckMessage('', 0)

        root_element = eTree.Element("Event")
        command_uuid = str(uuid.uuid4())
        eTree.SubElement(root_element, "Command", name=command.name, value=command_uuid)
        event = Event(subject='KDS{}'.format(kds_id), event_type='KdsModel', data=eTree.tostring(root_element))
        message_bus.publish_event(event)

        self.logger.info("TK_KDS_BUMP_BAR_COMMAND - Received - UUID: {}".format(command_uuid))
        return AckMessage('', 0)

    def handle_kds_undo_serve(self, data, message_bus):
        self.logger.info("TK_KDS_UNDO_SERVE - Received")

        kds_id = data[0]
        view = data[1]
        cmd_uuid = data[2] if len(data) > 2 else '0'

        if self._is_uuid_unique(cmd_uuid):
            log_message = "TK_KDS_UNDO_SERVE - Sending event to KDS: {}"
            self.logger.info(log_message.format(kds_id))

            message = Message(TK_PROD_UNDO, FM_PARAM, str(view), 3000000)
            message_bus.send_message(NEW_PRODUCTION_COMP, message)

        self.logger.info("TK_KDS_UNDO_SERVE - Finished")
        return AckMessage('', 0)

    @staticmethod
    def create_view_update_xml(production_view, xml):
        view_update_xml = eTree.Element("ViewUpdate", name=production_view)
        view_update_xml.append(xml)

        return view_update_xml

    @staticmethod
    def send_event(event_subject, event_type, xml):
        event_xml = eTree.Element("Event", subject=event_subject, type=event_type)
        event_xml.append(xml)
        event = Event(event_subject, event_type, eTree.tostring(event_xml))

        return event

    def forward_message(self, message, _):
        resp = AckMessage('', 0)
        try:
            self.logger.info("Receiving message for KDS - data = {}".format(message))
            self.logger.info("Message Ignored")
        except MessageBusException:
            self.logger.info("Error forwarding message - Component not found")

        return resp

    def get_kds_list_with_production_view(self, kds_view):
        kds_list = []
        for kds in self._get_kds_list(need_brands=False):
            if len([v for v in kds.views if v.production_view == kds_view]) > 0:
                kds_list.append(kds.name)

        return list(set(kds_list))

    def get_production_views_with_kds_id(self, kds_id):
        view_list = []

        kds = next((x for x in self._get_kds_list(need_brands=False) if x.name == kds_id), None)
        for view in kds.views:
            view_list.append(view.production_view)

        return list(set(view_list))

    @staticmethod
    def create_statistics_event_from_json(json_data):
        # type: (str) -> Element
        root_element = eTree.Element("Event")
        eTree.SubElement(root_element, "Statistics", text=json_data)
        return root_element

    def get_all_kds_ids(self):
        kds_list = []
        for kds in self._get_kds_list(need_brands=False):
            kds_list.append(kds.name)

        return list(set(kds_list))

    def get_current_statistics(self):
        statistics_ok = False
        while statistics_ok is False:
            try:
                resp = self.message_bus.send_message(NEW_PRODUCTION_COMP, Message(TK_PROD_GET_STATISTICS))
                if resp.token == DefaultToken.TK_SYS_ACK:
                    self.statistics_cache = resp.data
                    statistics_ok = True
            except (BaseException,):
                time.sleep(0.500)

    def _is_uuid_unique(self, cmd_uuid):
        # type: (str) -> bool

        self.logger.info('UUID: {}'.format(cmd_uuid))
        if cmd_uuid != '0' and cmd_uuid in self.commands_uuid:
            self.logger.info('Command Ignored')
            return False

        self.commands_uuid.append(cmd_uuid)
        self.commands_uuid = self.commands_uuid[-5:]
        return True

    def _get_kds_list(
            self,           # type: ProxyMessageHandler
            need_brands     # type: bool
    ):
        # type: (...) -> List[Kds]

        with kds_list_lock:
            return self._inner_get_kds_list(need_brands=need_brands)

    def _inner_get_kds_list(
            self,           # type: ProxyMessageHandler
            need_brands,    # type: bool
    ):
        # type: (...) -> Optional[List[Kds]]

        if self.kds_list and self.brands_loaded:
            return self.kds_list

        if self.kds_list is None:
            kds_views = self._get_kds_views()
            kds_list = []
            kds_config = self.config.find_group("KDS")
            if not kds_config:
                return []

            for kds_config_group in kds_config.groups:
                kds_list.append(
                    Kds(
                        kds_config=kds_config_group,
                        kds_views=kds_views,
                        brands=None,
                        pickup_config=self._get_pickup_config(),
                    )
                )

            self.kds_list = kds_list

        if need_brands:
            delivery_service = DeliveryService(self.mb_context, self.logger)
            delivery_brands = delivery_service.get_delivery_brands()
            if delivery_brands is None:
                return self.kds_list

            for kds in self.kds_list:
                kds.set_brands(delivery_brands)

            self.brands_loaded = True

        return self.kds_list

    def _get_kds_views(self):
        # type: (...) -> List[KdsView]

        views = []
        view_config = self.config.find_group("Views")
        if not view_config:
            return []

        for view_config_group in view_config.groups:
            views.append(
                KdsView(view_config_group)
            )

        return views

    def _get_pickup_config(
        self,   # type: ProxyMessageHandler
    ):
        # type: (...) -> PickupConfig

        pickup_config = self.config.find_group("PickupConfig")
        if not pickup_config:
            return []

        return PickupConfig(pickup_config.groups[0])
