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 product_api import ProductService
from typing import (
    Dict,
    Optional,
    Union,
    List,
)
from .dto import (
    DeliveryInfoDto,
    Order,
    parse_order,
    CustomProperty,
    SaleLineIdentifier,
    OrderState,
)
from .dto.exception import (
    CannotAddProductWithMandatoryCustomizationOnTotal,
    OrderException,
    ProductBarcodeNotFound,
    ProductInRupture,
    UnavailableProductStock,
)

from .error import OrderError
from .token import OrderApiTokens


class OrderService(object):

    def __init__(
        self,                   # type: OrderService
        message_bus,            # type: MessageBus
        pos_id,                 # type: int
        pos_service=None,       # type: Optional[PosService]
        product_service=None,   # type: Optional[ProductService]
    ):
        # 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,
            )

        if product_service is not None:
            self.product_service = product_service
        else:
            self.product_service = ProductService(message_bus=message_bus)

    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: 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=OrderApiTokens.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=None,                   # type: Optional[str]
        barcode=None,                   # type: Optional[str]
        qty=1,                          # type: float
        apply_default_options=True,     # type: bool
        custom_properties=None,         # type: Optional[Dict[str, str]]
        blk_notify=False,               # type: bool
        sell_ignoring_rupture=False,    # type: bool
    ):
        # type: (...) -> eTree.Element

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

        params = {
            "blkNotify": blk_notify,
            "posId": int(self.pos_id),
            "itemId": item_id,
            "barcode": barcode,
            "qty": qty,
            "applyDefaultOptions": apply_default_options,
            "customProperties": custom_properties,
            "sellIgnoringRupture": sell_ignoring_rupture,
        }
        return self._get_order_picture_from_success_message_response(
            token=OrderApiTokens.TK_SLCTRL_OMGR_DOSALE,
            data=json.dumps(params),
        )

    def do_option(
        self,                           # type: OrderService
        item_id,                        # type: str
        qty,                            # type: Union[str, int]
        line_number="",                 # type: Union[str, int]
        apply_default_options=False,    # type: bool
        blk_notify=False,               # type: bool
    ):
        # type: (...) -> 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)

        return self._get_order_picture_from_success_message_response(
            token=OrderApiTokens.TK_SLCTRL_OMGR_DOOPTION,
            data=buffer,
        )

    def do_tender(
        self,                   # type: OrderService
        tender_id,              # type: Union[int, str]
        amount,                 # type: Union[float, str]
        do_not_close=False,     # type: bool
        order_tender_id=None,   # type: Optional[Union[int, str]]
        tip_amount=None,        # type: Optional[float]
        tender_detail=None,     # type: Optional[Union[Dict[str, Union[bool, int, str]], str]]
        blk_notify=False,       # type: bool
    ):
        # type: (...) -> eTree.Element

        request = {
            "posId": int(self.pos_id),
            "blkNotify": blk_notify,
            "tenderId": int(tender_id),
            "amount": float(amount),
            "doNotClose": do_not_close,
            "orderTenderId": int(order_tender_id) if order_tender_id not in [None, ""] else None,
            "tipAmount": float(tip_amount) if tip_amount not in [None, ""] else None,
            "tenderDetail": json.dumps(tender_detail) if isinstance(tender_detail, dict) else tender_detail,
        }

        return self._get_order_picture_from_success_message_response(
            token=OrderApiTokens.TK_SLCTRL_OMGR_DOTENDER,
            data=json.dumps(request),
        )

    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: bool
    ):
        # type: (...) -> 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)

        return self._get_order_picture_from_success_message_response(
            token=OrderApiTokens.TK_SLCTRL_OMGR_CHANGEMODIFIER,
            data=buffer,
        )

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

        params = ("1" if blk_notify else "0", self.pos_id, line_number, item_id, quantity)

        return self._get_order_picture_from_success_message_response(
            token=OrderApiTokens.TK_SLCTRL_OMGR_CLEAROPTION,
            data="\x00".join((str(p) for p in params)),
        )

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

        no_cache = "true" if no_cache else "false"
        data = "{}\0{}\0{}".format(str(self.pos_id), str(order_id or ""), no_cache).encode("utf-8")
        msg = self.message_bus.send_message(
            component=self.component_name,
            message=Message(
                token=OrderApiTokens.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 get_order(
        self,           # type: OrderService
        order_id,       # type: int
    ):
        # type: (...) -> Order

        order_xml = self.get_order_picture(order_id=order_id)
        return parse_order(order_xml=order_xml)

    def has_current_order(
        self,               # type: OrderService
        order_picture=None,  # type: Optional[eTree.Element]
    ):
        # type: (...) -> bool

        if order_picture is None:
            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: bool
        custom_property=None,   # type: Optional[List[Dict[str, str]]]
    ):
        # type: (...) -> eTree.Element

        request = {
            "posId": int(self.pos_id),
            "lineNumbers": ([int(line) for line in line_numbers]) or None,
            "customProperties": custom_property or [],
            "blkNotify": blk_notify,
        }

        return self._get_order_picture_from_success_message_response(
            token=OrderApiTokens.TK_SLCTRL_OMGR_VOIDLINE,
            data=json.dumps(request),
        )

    def void_order(
        self,                   # type: OrderService
        order_id="",            # type: Union[str, int]
        abandon=False,          # type: bool
        order_status="",        # type: str
        void_reason="",         # type: str
        blk_notify=False,       # type: bool
    ):
        # type: (...) -> eTree.Element

        request = {
            "posId": int(self.pos_id),
            "blkNotify": blk_notify,
            "orderId": int(order_id) if order_id else None,
            "abandon": abandon,
            "orderStatus": int(order_status) if order_status not in ("", None) else None,
            "voidReason": void_reason if void_reason else None,
        }

        return self._get_order_picture_from_success_message_response(
            token=OrderApiTokens.TK_SLCTRL_OMGR_VOIDORDER,
            data=json.dumps(request),
        )

    def store_order(
        self,               # type: OrderService
        blk_notify=False,   # type: str
    ):
        # type: (...) -> eTree.Element

        request = {
            "posId": int(self.pos_id),
            "blkNotify": blk_notify,
        }

        return self._get_order_picture_from_success_message_response(
            token=OrderApiTokens.TK_SLCTRL_OMGR_STOREORDER,
            data=json.dumps(request),
        )

    def recall_order(
        self,                       # type: OrderService
        order_id,                   # type: Union[int, str]
        session_id="",              # type: str
        blk_notify=False,           # type: str
        notify_ndiscount=True,      # type: bool
    ):
        # type: (...) -> eTree.Element

        request = {
            "posId": int(self.pos_id),
            "orderId": int(order_id),
            "sessionId": session_id,
            "blkNotify": blk_notify,
            "notifyNdiscount": notify_ndiscount,
        }

        return self._get_order_picture_from_success_message_response(
            token=OrderApiTokens.TK_SLCTRL_OMGR_RECALLORDER,
            data=json.dumps(request),
        )

    def pull_orders(
        self,           # type: OrderService
        orders_ids,     # type: List[int]
    ):
        # type: (...) -> eTree.Element

        request = {
            "posId": int(self.pos_id),
            "ordersIds": orders_ids,
        }

        return self._get_order_picture_from_success_message_response(
            token=OrderApiTokens.TK_SLCTRL_OMGR_PULLORDERS,
            data=json.dumps(request),
        )

    def reopen_order(
        self,               # type: OrderService
        blk_notify=False,   # type: Optional[bool]
    ):
        # type: (...) -> eTree.Element

        request = {
            "posId": int(self.pos_id),
            "blkNotify": blk_notify,
        }

        return self._get_order_picture_from_success_message_response(
            token=OrderApiTokens.TK_SLCTRL_OMGR_ORDERREOPEN,
            data=json.dumps(request),
        )

    def total_order(
        self,                   # type: OrderService
        blk_notify=False,       # type: bool
        notify_ndiscount=True,  # type: bool
    ):
        # type: (...) -> eTree.Element

        request = {
            "posId": int(self.pos_id),
            "blkNotify": blk_notify,
            "notifyNdiscount": notify_ndiscount,
        }

        return self._get_order_picture_from_success_message_response(
            token=OrderApiTokens.TK_SLCTRL_OMGR_ORDERTOTAL,
            data=json.dumps(request),
        )

    def change_line_quantity(
        self,               # type: OrderService
        line_number,        # type: int
        item_id,            # type: str
        new_quantity,       # type: int
        blk_notify=False,   # type: bool
    ):
        # type: (...) -> eTree.Element

        request = {
            "posId": int(self.pos_id),
            "blkNotify": blk_notify,
            "lineNumber": line_number,
            "itemId": item_id,
            "newQuantity": new_quantity,
        }

        return self._get_order_picture_from_success_message_response(
            token=OrderApiTokens.TK_SLCTRL_OMGR_CHANGEQTY,
            data=json.dumps(request),
        )

    def set_order_custom_property(
        self,               # type: OrderService
        custom_property,    # type: CustomProperty
        order_id=None,      # type: Optional[int]
        blk_notify=False,   # type: bool
    ):
        # type: (...) -> Optional[eTree.Element]

        return self._set_custom_properties(
            custom_properties=[custom_property],
            order_id=order_id,
            blk_notify=blk_notify,
        )

    def set_order_custom_properties(
        self,               # type: OrderService
        custom_properties,  # type: List[CustomProperty]
        order_id=None,      # type: Optional[int]
        blk_notify=False,   # type: bool
    ):
        # type: (...) -> eTree.Element

        return self._set_custom_properties(
            custom_properties=custom_properties,
            order_id=order_id,
            blk_notify=blk_notify,
        )

    def remove_order_custom_property(
        self,               # type: OrderService
        key,                # type: str
        order_id=None,      # type: Optional[int]
        blk_notify=False,   # type: bool
    ):
        # type: (...) -> eTree.Element

        return self._set_custom_properties(
            custom_properties=[CustomProperty(
                key=key,
                value="clear",
            )],
            order_id=order_id,
            blk_notify=blk_notify,
        )

    def remove_order_custom_properties(
        self,               # type: OrderService
        keys,               # type: List[str]
        order_id=None,      # type: Optional[int]
        blk_notify=False,   # type: bool
    ):
        # type: (...) -> eTree.Element

        remove_properties = [
            CustomProperty(
                key=key,
                value="clear",
            ) for key in keys
        ]
        return self._set_custom_properties(
            custom_properties=remove_properties,
            order_id=order_id,
            blk_notify=blk_notify,
        )

    def set_sale_line_custom_property(
        self,                   # type: OrderService
        custom_property,        # type: CustomProperty
        sale_line_identifier,   # type: SaleLineIdentifier
        order_id=None,          # type: Optional[int]
        blk_notify=False,       # type: bool
    ):
        # type: (...) -> eTree.Element

        return self._set_custom_properties(
            custom_properties=[custom_property],
            sale_line_identifier=sale_line_identifier,
            order_id=order_id,
            blk_notify=blk_notify,
        )

    def set_sale_line_custom_properties(
        self,                   # type: OrderService
        custom_properties,      # type: List[CustomProperty]
        sale_line_identifier,   # type: SaleLineIdentifier
        order_id=None,          # type: Optional[int]
        blk_notify=False,       # type: bool
    ):
        # type: (...) -> eTree.Element

        return self._set_custom_properties(
            custom_properties=custom_properties,
            sale_line_identifier=sale_line_identifier,
            order_id=order_id,
            blk_notify=blk_notify,
        )

    def remove_sale_line_custom_property(
        self,                   # type: OrderService
        key,                    # type: str
        sale_line_identifier,   # type: SaleLineIdentifier
        order_id=None,          # type: Optional[int]
        blk_notify=False,       # type: bool
    ):
        # type: (...) -> eTree.Element

        return self._set_custom_properties(
            custom_properties=[CustomProperty(
                key=key,
                value="clear",
            )],
            sale_line_identifier=sale_line_identifier,
            order_id=order_id,
            blk_notify=blk_notify,
        )

    def remove_sale_line_custom_properties(
        self,                   # type: OrderService
        keys,                   # type: List[str]
        sale_line_identifier,   # type: SaleLineIdentifier
        order_id=None,          # type: Optional[int]
        blk_notify=False,       # type: bool
    ):
        # type: (...) -> eTree.Element

        remove_properties = [
            CustomProperty(
                key=key,
                value="clear",
            ) for key in keys
        ]
        return self._set_custom_properties(
            custom_properties=remove_properties,
            sale_line_identifier=sale_line_identifier,
            order_id=order_id,
            blk_notify=blk_notify,
        )

    def _set_custom_properties(
        self,                       # type: OrderService
        custom_properties,          # type: List[CustomProperty]
        sale_line_identifier=None,  # type: SaleLineIdentifier
        order_id=None,              # type: Optional[int]
        blk_notify=False,           # type: bool
    ):
        # type: (...) -> Optional[eTree.Element]

        properties = {}
        for prop in custom_properties:
            if not isinstance(prop.value, type(u"")):
                if isinstance(prop.value, bytes):
                    prop.value = prop.value.decode("utf-8")
                else:
                    prop.value = str(prop.value)

            properties[prop.key] = prop.value

        request = {
            "customProperties": properties,
            "posId": int(self.pos_id),
            "orderId": order_id,
            "lineNumber": sale_line_identifier.line_number if sale_line_identifier else None,
            "itemId": sale_line_identifier.item_id if sale_line_identifier else None,
            "level": sale_line_identifier.level if sale_line_identifier else None,
            "partCode": sale_line_identifier.part_code if sale_line_identifier else None,
            "blkNotify": blk_notify,
        }

        return self._get_order_picture_from_success_message_response(
            token=OrderApiTokens.TK_SLCTRL_OMGR_SETCUSTOMPROPERTIES,
            data=json.dumps(request),
        )

    def list_orders(
        self,                    # type: OrderService
        states=None,             # type: Optional[List[OrderState]]
        pos_id=None,             # type: Optional[str]
        pod_type=None,           # type: Optional[str]
        since=None,              # type: Optional[str]
        until=None,              # type: Optional[str]
        session_id=None,         # type: Optional[str]
        period=None,             # type: Optional[str]
        limit=None,              # type: Optional[str]
        include_property=None,   # type: Optional[str]
        exclude_property=None,   # type: Optional[str]
        property_val=None,       # type: Optional[str]
        descending=False,        # type: Optional[bool]
    ):
        # type: (...) -> List[Dict[str, Union[str, dict, list]]]

        data = {
            "states": [state.value for state in states] if states is not None else [],
            "posId": pos_id,
            "podType": pod_type,
            "since": since,
            "until": until,
            "sessionId": session_id,
            "period": period,
            "limit": limit,
            "includeProperty": include_property,
            "excludeProperty": exclude_property,
            "propertyValue": property_val,
            "descending": descending,
        }

        msg = self.message_bus.send_message(
            component=self.component_name,
            message=Message(
                token=OrderApiTokens.TK_SLCTRL_OMGR_LISTORDERS,
                data_type=DataType.json,
                data=json.dumps(data).encode("utf-8"),
            )
        )

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

        return self._parse_listed_orders(orders_data=msg.data.decode("utf-8"))

    def get_order_config(
        self,    # type: OrderService
    ):
        # type: (...) -> Dict[str, Union[str, int]]

        msg = self.message_bus.send_message(
            component=self.component_name,
            message=Message(
                token=OrderApiTokens.TK_SLCTRL_OMGR_GET_CONFIG,
            )
        )
        if msg.token != DefaultToken.TK_SYS_ACK:
            raise self._build_order_exception(msg=msg)

        return json.loads(msg.data.decode("utf-8"))

    @staticmethod
    def _parse_listed_orders(
        orders_data,    # type: str
    ):
        # type: (...) -> List[Dict[str, Union[str, dict, list]]]

        response_params = orders_data.split("\0")
        xml = eTree.XML(response_params[2].encode("utf-8"))
        result = []
        for order in xml.findall("Order"):
            order_dict = dict(order.items())
            order_dict["custom_properties"] = {}
            for order_property in order.findall("CustomOrderProperties/OrderProperty"):
                order_dict["custom_properties"][order_property.get("key")] = order_property.get("value")

            order_dict["tender_history"] = []
            for tender in order.findall("TenderHistory/Tender"):
                order_dict["tender_history"].append(dict(tender.items()))

            result.append(order_dict)

        return result

    @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_order_picture_from_success_message_response(
        self,   # type: OrderService
        token,  # type: int
        data,   # type: str
    ):
        # type: (...) -> eTree.Element

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

        return eTree.XML(reply.data)

    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=int(application_error.error_message),
                    part_name=self._get_part_name(application_error.error_message),
                )

            if application_error.error_code == OrderError.UNAVAILABLE_PRODUCT_STOCK.code:
                raise UnavailableProductStock(
                    part_code=int(application_error.error_message),
                    part_name=self._get_part_name(application_error.error_message),
                )
            if application_error.error_code == OrderError.ADD_PRODUCT_WITH_MANDATORY_CUSTOMIZATION_ON_TOTAL.code:
                raise CannotAddProductWithMandatoryCustomizationOnTotal(
                    part_code=int(application_error.error_message),
                    part_name=self._get_part_name(application_error.error_message),
                )
            if application_error.error_code == OrderError.PRODUCT_BARCODE_NOT_FOUND.code:
                raise ProductBarcodeNotFound(
                    part_code=application_error.error_message,
                )

            if application_error.error_code == OrderError.INVALID_CONTEXT.code:
                raise OrderException(
                    code=OrderError.INVALID_CONTEXT.code,
                    description=application_error.error_message
                )

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

        return reply

    def _get_part_name(
        self,       # type: OrderService
        part_code,  # type: str
    ):
        # type: (...) -> str

        products_name = self.product_service.get_products_name()

        return products_name.get(part_code, part_code)
