import json

from xml.etree.ElementTree import Element
from xml.etree import cElementTree as eTree

from messagebus import (
    ApplicationErrorMessage,
    MessageBus,
    Message,
    DataType,
    DefaultToken,
)
from pos_api import PosService
from typing import (
    Dict,
    Optional,
    Union,
    List,
)
from .dto import DeliveryInfoDto
from .dto.exception import (
    OrderException,
    ProductInRupture,
)

from .error import OrderError
from .token import (
    TK_SLCTRL_OMGR_CHANGEMODIFIER,
    TK_SLCTRL_OMGR_CREATEORDER,
    TK_SLCTRL_OMGR_DOOPTION,
    TK_SLCTRL_OMGR_DOSALE,
    TK_SLCTRL_OMGR_ORDERPICT,
    TK_SLCTRL_OMGR_VOIDLINE,
)


class OrderService(object):

    def __init__(
        self,                   # type: OrderService
        message_bus,            # type: MessageBus
        pos_id,                 # type: int
        pos_service=None,       # type: Optional[PosService]
    ):
        # type: (...) -> None

        self.message_bus = message_bus
        self.pos_id = pos_id

        self.component_name = "ORDERMGR{}".format(self.pos_id)
        if pos_service is not None:
            self.pos_service = pos_service
        else:
            self.pos_service = PosService(
                message_bus=message_bus,
                pos_id=pos_id,
            )

    def create_order(
        self,                       # type: OrderService
        sale_type,                  # type: str
        session_id=None,            # type: Optional[str]
        pod_type=None,              # type: Optional[str]
        order_type="SALE",          # type: Optional[str]
        order_sub_type=None,        # type: Optional[str]
        service_period_id=None,     # type: Optional[int]
        delivery_info=None,         # type: Optional[DeliveryInfoDto]
        additional_info=None,       # type: Optional[str]
        custom_properties=None,     # type: Optional[Dict[str, str]]
        blk_notify=False,           # type: bool
    ):
        # type: (...) -> Element

        if session_id is None:
            session_id = self.pos_service.get_operator_session()

        if pod_type is None:
            pod_type = self.pos_service.get_pod_type()

        request = {
            "blkNotify": blk_notify,
            "posId": self.pos_id,
            "sessionId": session_id,
            "saleType": sale_type,
            "podType": pod_type,
            "orderType": order_type,
        }
        if order_sub_type is not None:
            request["orderSubType"] = order_sub_type
        if additional_info is not None:
            request["additionalInfo"] = additional_info
        if service_period_id is not None:
            request["servicePeriodId"] = service_period_id
        if custom_properties is not None:
            request["customProperties"] = custom_properties
        if delivery_info is not None:
            request["deliveryInfo"] = delivery_info.to_dict()

        msg = self.message_bus.send_message(
            component=self.component_name,
            message=Message(
                token=TK_SLCTRL_OMGR_CREATEORDER,
                data_type=DataType.param,
                data=json.dumps(request).encode("utf-8"),
                timeout=600000000,
            )
        )

        if msg.token != DefaultToken.TK_SYS_ACK:
            raise self._build_order_exception(msg=msg)

        return eTree.XML(msg.data)

    def do_sale(
        self,                               # type: OrderService
        item_id,                            # type: str
        qty=1,                              # type: float
        apply_default_options=True,         # type: bool
        custom_properties=None,             # type: Dict[str, str]
        blk_notify=False,                   # type: bool
    ):
        # type: (...) -> Optional[eTree.Element]

        if custom_properties is not None:
            custom_properties = json.dumps(custom_properties)

        params = (
            "1" if blk_notify else "0",
            self.pos_id,
            item_id,
            qty,
            "1" if apply_default_options else "0",
            custom_properties or "",
        )
        data = "\x00".join((str(p) for p in params))
        reply = self._get_success_message_response(
            token=TK_SLCTRL_OMGR_DOSALE,
            data=data,
        )

        params_response = reply.data.split(b"\0")
        if len(params_response) <= 2 or not params_response[2]:
            return None

        return eTree.XML(params_response[2])

    def do_option(
        self,                           # type: OrderService
        item_id,                        # type: str
        qty,                            # type: Union[str, int]
        line_number="",                 # type: Optional[Union[str, int]]
        apply_default_options=False,    # type: Optional[bool]
        blk_notify=False,               # type: Optional[bool]
    ):
        # type: (...) -> Optional[eTree.Element]

        blk_notify = "1" if blk_notify else "0"
        apply_default_options = "1" if apply_default_options else "0"
        params = (blk_notify, self.pos_id, item_id, qty, line_number, "", apply_default_options)
        buffer = "\0".join(str(p) for p in params)

        reply = self._get_success_message_response(
            token=TK_SLCTRL_OMGR_DOOPTION,
            data=buffer,
        )

        params_response = reply.data.split(b"\0")
        if len(params_response) <= 2 or not params_response[2]:
            return None

        return eTree.XML(params_response[2])

    def change_modifier(
        self,               # type: OrderService
        line_number,        # type: int
        item_id,            # type: str
        level,              # type: int
        part_code,          # type: str
        new_qty,            # type: Union[str, int]
        blk_notify=False,   # type: Optional[bool]
    ):
        # type: (...) -> Optional[eTree.Element]

        blk_notify = "1" if blk_notify else "0"
        params = (blk_notify, self.pos_id, line_number, item_id, level, part_code, new_qty)
        buffer = "\0".join(str(p) for p in params)

        reply = self._get_success_message_response(
            token=TK_SLCTRL_OMGR_CHANGEMODIFIER,
            data=buffer,
        )

        params_response = reply.data.split(b"\0")
        if len(params_response) <= 2 or not params_response[2]:
            return None

        return eTree.XML(params_response[2])

    def get_order_picture(
        self,               # type: OrderService
        order_id=None,      # type: int
    ):
        # type: (...) -> Optional[Element]

        data = "{}\0{}".format(str(self.pos_id), str(order_id or "")).encode("utf-8")
        msg = self.message_bus.send_message(
            component=self.component_name,
            message=Message(
                token=TK_SLCTRL_OMGR_ORDERPICT,
                data_type=DataType.param,
                data=data,
                timeout=600000000,
            )
        )

        if msg.token != DefaultToken.TK_SYS_ACK:
            raise self._build_order_exception(msg=msg)

        order_str = msg.data.split(b"\0")[2]
        if not order_str:
            return None

        order_picture = eTree.XML(order_str)
        if order_id and order_picture.tag != "Order":
            order_picture = order_picture.findall(".//Order")[0]
        return order_picture

    def has_current_order(
        self,       # type: OrderService
    ):
        # type: (...) -> bool

        order_picture = self.get_order_picture()
        if order_picture is None:
            return False

        state = order_picture.get("state")
        return state in ["IN_PROGRESS", "TOTALED"]

    def void_lines(
        self,                   # type: OrderService
        line_numbers,           # type: List[int]
        blk_notify=False,       # type: Optional[bool]
        custom_property=None,   # type: Optional[List[Dict[str, str]]]
    ):
        # type: (...) -> None

        if custom_property:
            custom_properties = json.dumps(custom_property)
        else:
            custom_properties = "[]"

        line_numbers_str = "|".join(map(str, line_numbers))
        params = ("1" if blk_notify else "0", self.pos_id, line_numbers_str, custom_properties)

        data = "\x00".join((str(p) for p in params))
        msg = self.message_bus.send_message(
            component=self.component_name,
            message=Message(
                token=TK_SLCTRL_OMGR_VOIDLINE,
                data_type=DataType.param,
                data=data.encode("utf-8"),
            ),
        )
        if msg.token != DefaultToken.TK_SYS_ACK:
            raise self._build_order_exception(msg=msg)

    @staticmethod
    def _build_order_exception(
        msg,  # type: Message
    ):
        # type: (...) -> OrderException

        try:
            data_text = msg.data.decode("utf-8")
        except UnicodeDecodeError:
            data_text = msg.data.hex()

        parts = data_text.split("\0")
        if len(parts) >= 2:
            error_code = parts[0]
            error_message = parts[1]
            return OrderException(
                code=error_code,
                description=error_message,
            )

        return OrderException(
            code=-1,
            description="Unknown error: {}".format(data_text),
        )

    def _get_success_message_response(
        self,   # type: OrderService
        token,  # type: int
        data,   # type: str
    ):
        # type: (...) -> Message

        reply = self.message_bus.send_message(
            component=self.component_name,
            message=Message(
                token=token,
                data_type=DataType.param,
                data=data.encode("utf-8"),
            ),
        )

        if reply.token == DefaultToken.TK_SYS_APPLICATION_ERROR:
            application_error = ApplicationErrorMessage.build_from_message(message=reply)
            if application_error.error_code == OrderError.PRODUCT_IN_RUPTURE.code:
                raise ProductInRupture(part_code=application_error.error_message)

        if reply.token != DefaultToken.TK_SYS_ACK:
            raise self._build_order_exception(msg=reply)

        return reply
