import json
from typing import (
    Dict,
    List,
    Optional,
    Union,
)
from xml.etree import cElementTree as eTree

from messagebus import (
    ApplicationErrorMessage,
    DataType,
    DefaultToken,
    Message,
    MessageBus,
)
from .dto import (
    Table,
    TableMode,
    TableServiceDto,
)
from .dto.exception import (
    CurrentTabWithPartialTenders,
    InvalidTabMask,
    MissingAnyOfParameters,
    MissingNumSeats,
    OperatorNotLoggedIn,
    TabIdAlreadyExists,
    TabNotFound,
    TabToJoinDoesNotExists,
    TabToJoinInInvalidStatus,
    TabToJoinIsAlreadyJoined,
    TabToJoinIsTheSame,
    TabToJoinOpenedInAnotherPOS,
    TabToJoinWithPartialTenders,
    TableAlreadyOpened,
    TableIsNotInTabsMode,
    TableNotFound,
    TableNotOpened,
    TableServiceException,
    UnknownError,
)
from .dto.table_xml_parser import parse_table
from .error import CodedError
from .token import TK_CMP_MESSAGE


class TableService(object):

    def __init__(
        self,           # type: TableService
        message_bus,    # type: MessageBus
        pos_id,         # type: int
    ):
        # type: (...) -> None

        self.message_bus = message_bus
        self.pos_id = pos_id
        self.table_mgr = "TABLEMGR"

    def get_table_picture(
        self,       # type: TableService
        table_id,   # type: int
    ):
        # type: (...) -> eTree.Element

        params = (
            "TABLE_PICTURE",
            self.pos_id,
            table_id,
        )
        request_data = "\x00".join((str(p) for p in params))
        msg = self.message_bus.send_message(
            component=self.table_mgr,
            message=Message(
                token=TK_CMP_MESSAGE,
                data_type=DataType.param,
                data=request_data.encode(),
                timeout=600000000,
            ),
        )
        if msg.token == DefaultToken.TK_SYS_ACK:
            return eTree.XML(msg.data)

        raise TableServiceException(description=msg.data.decode())

    def get_table(
        self,       # type: TableService
        table_id,   # type: int
    ):
        # type: (...) -> Table

        xml_table_picture = self.get_table_picture(table_id=table_id)
        table = parse_table(table_xml=xml_table_picture)
        return table

    def get_sectors(
        self,       # type: TableService
    ):
        # type: (...) -> List[str]

        reply = self.message_bus.send_message(
            component=self.table_mgr,
            message=Message(
                token=TK_CMP_MESSAGE,
                data=b"GET_SECTORS",
            )
        )

        if reply.token != DefaultToken.TK_SYS_ACK:
            raise TableServiceException(description=reply.data.decode())

        return json.loads(reply.data.decode())

    def start_service(
        self,                       # type: TableService
        table_id,                   # type: int
        mode,                       # type: TableMode
        num_seats=None,             # type: Optional[int]
        custom_properties=None,     # type: Optional[Dict[str, str]]
    ):
        # type: (...) -> TableServiceDto

        request = {
            "posId": self.pos_id,
            "tableId": table_id,
            "mode": mode.value,
        }
        if num_seats is not None:
            request["numSeats"] = num_seats

        if custom_properties is not None:
            request["customProperties"] = custom_properties

        params = (
            "START_SERVICE",
            json.dumps(request),
        )
        request_data = "\0".join((str(p) for p in params))
        reply = self.message_bus.send_message(
            component=self.table_mgr,
            message=Message(
                token=TK_CMP_MESSAGE,
                data_type=DataType.param,
                data=request_data.encode(),
                timeout=600000000,
            ),
        )

        if reply.token == DefaultToken.TK_SYS_APPLICATION_ERROR:
            app_error = ApplicationErrorMessage.build_from_message(message=reply)
            ex_dict = json.loads(app_error.error_message)
            if ex_dict["ex"] == "TableNotFound":
                raise TableNotFound(table_id=ex_dict["tableId"])
            elif ex_dict["ex"] == "OperatorNotLoggedIn":
                raise OperatorNotLoggedIn(pos_id=ex_dict["posId"])
            elif ex_dict["ex"] == "MissingNumSeats":
                raise MissingNumSeats()
            elif ex_dict["ex"] == "TableAlreadyOpened":
                raise TableAlreadyOpened(table_id=ex_dict["tableId"])

        if reply.token != DefaultToken.TK_SYS_ACK:
            raise UnknownError(
                token=reply.token,
                data=reply.data,
            )

        return self._build_table_service(reply)

    def start_tab(
        self,                        # type: TableService
        tab_id,                      # type: str
        parent_table_id=None,        # type: Optional[int]
        custom_properties=None,      # type: Optional[Dict[str, str]]
    ):
        request = {
            "posId": self.pos_id,
            "tabId": tab_id,
        }

        if parent_table_id is not None:
            request["parentTableId"] = parent_table_id

        if custom_properties is not None:
            request["customProperties"] = custom_properties

        params = (
            "START_TAB",
            json.dumps(request),
        )
        request_data = "\0".join((str(p) for p in params))
        reply = self.message_bus.send_message(
            component=self.table_mgr,
            message=Message(
                token=TK_CMP_MESSAGE,
                data_type=DataType.param,
                data=request_data.encode(),
                timeout=600000000,
            ),
        )

        if reply.token == DefaultToken.TK_SYS_APPLICATION_ERROR:
            app_error = ApplicationErrorMessage.build_from_message(message=reply)
            ex_dict = json.loads(app_error.error_message)
            if ex_dict["ex"] == "TableNotFound":
                raise TableNotFound(table_id=ex_dict["tableId"])
            elif ex_dict["ex"] == "OperatorNotLoggedIn":
                raise OperatorNotLoggedIn(pos_id=ex_dict["posId"])
            elif ex_dict["ex"] == "MissingNumSeats":
                raise MissingNumSeats()
            elif ex_dict["ex"] == "TableAlreadyOpened":
                raise TableAlreadyOpened(table_id=ex_dict["tableId"])
            elif ex_dict["ex"] == "TabIdAlreadyExists":
                raise TabIdAlreadyExists(tab_id=ex_dict["tabId"])
            elif ex_dict["ex"] == "TableNotOpened":
                raise TableNotOpened(table_id=ex_dict["tableId"])
            elif ex_dict["ex"] == "TableIsNotInTabsMode":
                raise TableIsNotInTabsMode(table_id=ex_dict["tableId"])

        if reply.token != DefaultToken.TK_SYS_ACK:
            raise UnknownError(
                token=reply.token,
                data=reply.data,
            )

        return self._build_table_service(reply)

    def get_service(
        self,           # type: TableService
        table_id,       # type: Union[int, str]
    ):
        # type: (...) -> TableServiceDto

        request = {
            "tableId": table_id,
        }

        params = (
            "TABLE_SERVICE_PICTURE",
            json.dumps(request),
        )
        request_data = "\0".join((str(p) for p in params))
        reply = self.message_bus.send_message(
            component=self.table_mgr,
            message=Message(
                token=TK_CMP_MESSAGE,
                data_type=DataType.param,
                data=request_data.encode(),
                timeout=600000000,
            ),
        )

        return self._build_table_service(reply)

    def get_service_tab(
        self,           # type: TableService
        tab_id=None,    # type: Optional[str]
        tab_name=None,  # type: Optional[str]
    ):
        # type: (...) -> TableServiceDto

        request = {}

        if tab_id:
            request["tabId"] = tab_id
        if tab_name:
            request["tabName"] = tab_name

        if not request:
            raise MissingAnyOfParameters(params=["tab_id", "tab_name"])

        params = ["TAB_SERVICE_PICTURE", json.dumps(request)]
        message = Message(
            token=TK_CMP_MESSAGE,
            data_type=DataType.param,
            data="\0".join(params).encode(),
            timeout=600000000,
        )

        reply = self.message_bus.send_message(
            component=self.table_mgr,
            message=message,
        )

        if reply.token == DefaultToken.TK_SYS_APPLICATION_ERROR:
            app_error = ApplicationErrorMessage.build_from_message(message=reply)
            ex_dict = json.loads(app_error.error_message)
            if ex_dict["ex"] == "InvalidTabMask":
                raise InvalidTabMask(tab_name=ex_dict["tabName"])
            if ex_dict["ex"] == "TabNotFound":
                raise TabNotFound(
                    tab_id=ex_dict["tabId"],
                    tab_name=ex_dict["tabName"],
                )

            raise UnknownError(
                token=reply.token,
                data=reply.data,
            )
        if reply.token == DefaultToken.TK_SYS_BAD_REQUEST:
            ret = json.loads(reply.data)
            raise CodedError(
                code=ret["errorCode"],
                message=ret["errorMessage"],

            )

        return self._build_table_service(reply=reply)

    def join_tabs(
        self,               # type: TableService
        user_id,            # type: str
        current_tab_id,     # type: str
        tab_to_join,        # type: str
    ):
        # type: (...) -> TableServiceDto

        if not user_id or not current_tab_id or not tab_to_join:
            raise MissingAnyOfParameters(params=["user_id", "current_tab_id", "tab_to_join"])

        params = ["JOIN_TABS", str(self.pos_id), user_id, current_tab_id, tab_to_join]
        message = Message(
            token=TK_CMP_MESSAGE,
            data_type=DataType.param,
            data="\0".join(params).encode(),
            timeout=600000000,
        )

        reply = self.message_bus.send_message(
            component=self.table_mgr,
            message=message,
        )

        if reply.token == DefaultToken.TK_SYS_ACK:
            return self._build_table_service(reply=reply)

        self._tabs_error(ex_message=reply.data.decode())

    def unlink_tabs(
        self,               # type: TableService
        user_id,            # type: str
        current_tab_id,     # type: str
        tabs_to_unlink,     # type: List[str]
    ):
        # type: (...) -> TableServiceDto

        if not user_id or not current_tab_id or not tabs_to_unlink:
            raise MissingAnyOfParameters(params=["user_id", "current_tab_id", "tabs_to_unlink"])

        params = ["UNLINK_TABS", str(self.pos_id), user_id, current_tab_id, ",".join(tabs_to_unlink)]
        reply = self.message_bus.send_message(
            component=self.table_mgr,
            message=Message(
                token=TK_CMP_MESSAGE,
                data_type=DataType.param,
                data="\0".join(params).encode(),
                timeout=600000000,
            ),
        )

        if reply.token == DefaultToken.TK_SYS_ACK:
            return self._build_table_service(reply=reply)

        self._tabs_error(ex_message=reply.data.decode())

    @staticmethod
    def _tabs_error(
        ex_message,     # type: str
    ):
        # type: (...) -> None

        ex_params = str(ex_message).split("|")

        if "$TAB_NOT_FOUND" == ex_params[0]:
            raise InvalidTabMask(tab_name=ex_params[1])

        if "$SELECT_A_DIFFERENT_TAB" == ex_params[0]:
            raise TabToJoinIsTheSame()

        if "$TAB_TO_JOIN_DOES_NOT_EXISTS" == ex_params[0]:
            raise TabToJoinDoesNotExists(tab_name=ex_params[1])

        if "$TAB_TO_JOIN_IS_ALREADY_JOINED" == ex_params[0]:
            raise TabToJoinIsAlreadyJoined(tab_name=ex_params[1])

        if "$TAB_TO_JOIN_IS_OPENED_IN_ANOTHER_POS" == ex_params[0]:
            raise TabToJoinOpenedInAnotherPOS(tab_name=ex_params[1])

        if "$CURRENT_TAB_WITH_PARTIAL_TENDERS" == ex_params[0]:
            raise CurrentTabWithPartialTenders(tab_id=ex_params[1])

        if "$TAB_TO_JOIN_WITH_PARTIAL_TENDERS" == ex_params[0]:
            raise TabToJoinWithPartialTenders(tab_id=ex_params[1])

        if "$TAB_TO_JOIN_IN_INVALID_STATUS" == ex_params[0]:
            ex_params = ex_params[ex_params.index("$TAB_TO_JOIN_IN_INVALID_STATUS"):]
            raise TabToJoinInInvalidStatus(
                state=ex_params[2],
                tab_name=ex_params[1],
            )

        raise UnknownError(
            token=DefaultToken.TK_SYS_NAK,
            data=ex_message.encode(),
        )

    @staticmethod
    def _build_table_service(
        reply,      # type: Message
    ):
        # type: (...) -> TableServiceDto

        reply_xml = eTree.XML(reply.data)
        return TableServiceDto.from_xml(xml=reply_xml)
