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
from .dto.exception import (
    OrderException,
    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,                            # 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: (...) -> 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))
        return self._get_order_picture_from_success_message_response(
            token=OrderApiTokens.TK_SLCTRL_OMGR_DOSALE,
            data=data,
        )

    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 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: 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)

        msg = self._get_success_message_response(
            token=OrderApiTokens.TK_SLCTRL_OMGR_VOIDLINE,
            data="\x00".join((str(p) for p in params)),
        )

        return eTree.XML(msg.data)

    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 list_orders(
        self,                  # type: OrderService
        state="",              # type: Optional[str]
        originator_id="",      # type: Optional[str]
        pod_type="",           # type: Optional[str]
        since="",              # type: Optional[str]
        until="",              # type: Optional[str]
        session_id="",         # type: Optional[str]
        period="",             # type: Optional[str]
        limit="",              # type: Optional[str]
        descending="",         # type: Optional[str]
        instance_number="",    # type: Optional[str]
        include_property="",   # type: Optional[str]
        exclude_property="",   # type: Optional[str]
        property_val="",       # type: Optional[str]
        order_by="",           # type: Optional[str]
        show_tenders=False,    # type: Optional[bool]
    ):
        # type: (...) -> List[Dict[str, Union[str, dict, list]]]

        params = (
            state,
            originator_id,
            pod_type,
            since,
            until,
            session_id,
            period,
            limit,
            include_property,
            exclude_property,
            property_val,
            order_by,
            "1" if descending and descending != "0" else "",
            instance_number,
            "1" if show_tenders is True else "0",
        )

        data = "\0".join((str(p) for p in params)).encode("utf-8")
        msg = self.message_bus.send_message(
            component=self.component_name,
            message=Message(
                token=OrderApiTokens.TK_SLCTRL_OMGR_LISTORDERS,
                data_type=DataType.param,
                data=data,
            )
        )

        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"))

    @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])
        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 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)
