import json
from json import dumps, loads
from xml.etree import cElementTree as eTree

from application.model.benefit import OrderBenefitInfo, Benefit, Conditions
from posot import OrderTakerException, OrderTaker
from sysactions import get_model
from typing import Dict, Optional, List

from ._SaleLine import SaleLine
from application.util import get_range_discount_ids


class Order(object):
    def __init__(self, order_picture, sale_lines, model):
        pos_id = int(order_picture.get("posId"))

        self.pos_id = pos_id
        self.pod_type = order_picture.get("podType")
        self.id = int(order_picture.get("orderId")) if order_picture.get("orderId") else None
        self.order_state = order_picture.get("state")
        self.sale_lines = [SaleLine(o) for o in sale_lines]
        self.total_amount = float(order_picture.get("totalAmount", 0)) + float(order_picture.get("taxTotal", 0))
        self.total_gross = float(order_picture.get("totalGross", self.total_amount))
        self.discount_amount = float(order_picture.get("discountAmount", 0))
        self.due_amount = float(order_picture.attrib["dueAmount"])
        self.sale_type = order_picture.get("saleTypeDescr")
        self.order_picture = order_picture
        self.custom_properties = self._get_custom_order_properties(order_picture)
        self.sector = self.custom_properties.get("SECTOR")
        self.table_id = self.custom_properties.get("TABLE_ID") if self.custom_properties.get("TABLE_ID") else None
        self.brand = self.custom_properties.get("BRAND") if self.custom_properties.get("BRAND") else None

        self.seats = 0
        self.tip = 0
        self.special_catalog = None
        self.model = model
        self.get_table_info()

        self.applied_benefits = self._get_applied_benefits(order_picture)

    def get_table_info(self):
        model = self.model
        if model is None:
            model = get_model(self.pos_id)
        table_xml = self._get_table_xml(model)

        if table_xml:
            self.sector = table_xml.get("sector", None)
            self.seats = int(table_xml.get("serviceSeats", 0))
            self.tip = float(table_xml.get("tip", 0))
            self._get_table_custom_property_info(table_xml)

    def _get_table_custom_property_info(self, table_xml):
        for custom_property in table_xml.findall(".//Properties"):
            service_period = custom_property.get('SERVICE_PERIOD_TABLE', None)
            if service_period:
                self.special_catalog = loads(service_period)
                break

    def _get_table_xml(self, model):
        # type: (eTree.XML) -> Optional[eTree.XML]

        from tablemgrapi import get_pos_ts

        table_id = self.table_id
        if not table_id:
            return

        pos_ts = get_pos_ts(model)
        return eTree.XML(pos_ts.getTableInfo(table_id))

    @staticmethod
    def _get_custom_order_properties(order_picture):
        # type: (eTree.XML) -> Dict[str, str]

        custom_properties = {}
        for custom_property in order_picture.find(".//CustomOrderProperties"):
            custom_properties[custom_property.get("key")] = custom_property.get("value")

        return custom_properties

    @staticmethod
    def get_burnt_items(order):
        # type: (Order) -> dict

        burnt_items = dict()
        for benefit in order.applied_benefits:
            for entry in benefit.burnt_items.items():
                if burnt_items.get(entry[0]) is None:
                    burnt_items[entry[0]] = entry[1]
                else:
                    burnt_items[entry[0]] = (burnt_items.get(entry[0]) + entry[1])

        return burnt_items

    @staticmethod
    def get_used_discount_ids(pos_ot):
        # type: (OrderTaker) -> List[int]
        try:
            order_discount_list = eTree.XML(pos_ot.getOrderDiscounts(applied=True)).findall(".//OrderDiscount")
            return [int(el.get("discountId")) for el in order_discount_list if el.tag == "OrderDiscount"]
        except OrderTakerException:
            return []

    @staticmethod
    def get_available_discount_ids(pos_ot):
        # type: (OrderTaker) -> set

        available_discount_ids = get_range_discount_ids()
        discount_ids = set(Order.get_used_discount_ids(pos_ot))
        return available_discount_ids.difference(discount_ids)

    @staticmethod
    def get_required_items(order, benefit):
        # type: (Order, Benefit) -> List

        conditions = benefit.conditions
        required_items_on_sale = {}

        for item in conditions.required_items_on_sale:
            key = "{};{}".format(item.context if item.context else 'any', str(item.item_id))
            required_items_on_sale[key] = item.quantity

        sale_lines = order.sale_lines
        required_items = []
        for sale_line in sale_lines:
            context = sale_line.context
            for item_index in required_items_on_sale.keys():
                required_item_context, required_item_id = item_index.split(';')

                if int(sale_line.item_id) == int(required_item_id) \
                        and (required_item_context == 'any' or required_item_context in context):
                    required_items.append(sale_line.key)

        return required_items

    @staticmethod
    def _check_and_burn_by_required_items(items_to_burn, required_items, sale_line, burnt_items, conditions):
        # type: (Dict, Dict, SaleLine, Dict, Conditions) -> None

        context = sale_line.context
        for item_index in required_items.keys():
            required_item_context, required_item_id = item_index.split(';')

            if int(sale_line.item_id) != int(required_item_id) or required_items[item_index] <= 0 \
                    or burnt_items.get(sale_line.key, -1) >= sale_line.qty:
                continue

            if required_item_context == 'any' or required_item_context in context:
                burnt_qty = min(required_items[item_index], sale_line.qty)
                required_items[item_index] -= burnt_qty
                qty = 0 if conditions.ignore_burned_items else int(burnt_items.get(sale_line.key, 0)) + burnt_qty
                items_to_burn[sale_line.key] = qty

    @staticmethod
    def _check_and_burn_by_required_groups(items_to_burn, required_groups, product_groups, sale_line, burnt_items,
                                           conditions):
        # type: (Dict, Dict, Dict, SaleLine, Dict, Conditions) -> None

        for group_index in required_groups.keys():
            item_id = int(sale_line.item_id)
            if item_id not in product_groups[group_index] or required_groups[group_index] <= 0 \
                    or burnt_items.get(sale_line.key, -1) >= sale_line.qty \
                    or items_to_burn.get(sale_line.key, -1) >= sale_line.qty:
                continue

            burnt_qty = min(required_groups[group_index], sale_line.qty)
            required_groups[group_index] -= burnt_qty
            qty = 0
            if not conditions.ignore_burned_items:
                qty = int(items_to_burn.get(sale_line.key, 0)) + int(burnt_items.get(sale_line.key, 0)) + burnt_qty
            items_to_burn[sale_line.key] = qty

    def get_items_to_burn(self, order, benefit, burnt_items, product_groups):
        # type: (Order, Benefit, Dict, Dict) -> Dict

        if not benefit.conditions.burn_items:
            return {}

        sale_lines = order.sale_lines
        conditions = benefit.conditions
        items_to_burn = {}

        required_items_on_sale = {}
        required_groups_on_sale = {}

        for item in conditions.required_items_on_sale:
            key = "{};{}".format(item.context if item.context else 'any', str(item.item_id))
            required_items_on_sale[key] = item.quantity

        for group in conditions.required_groups_on_sale:
            required_groups_on_sale[group.group] = group.quantity

        for sale_line in sale_lines:
            self._check_and_burn_by_required_items(
                items_to_burn, required_items_on_sale, sale_line, burnt_items, conditions
            )
            self._check_and_burn_by_required_groups(
                items_to_burn, required_groups_on_sale, product_groups, sale_line, burnt_items, conditions
            )

        for key in items_to_burn.keys():
            if items_to_burn.get(key, 0) == 0:
                items_to_burn.pop(key)

        return items_to_burn

    def get_applied_benefits_from_order_picture(self, order_picture):
        # type: (Optional[eTree]) -> List

        benefit_list = self.get_benefit_list_from_order_picture(order_picture)
        if not benefit_list:
            return []

        return json.loads(benefit_list.get("benefitList", "[]"))

    @staticmethod
    def get_benefit_list_from_order_picture(order_picture):
        # type: (Optional[eTree]) -> Dict

        try:
            value = order_picture.find(".//OrderProperty[@key=\"BENEFIT_LIST\"]").get("value")
            return json.loads(value)
        except OrderTakerException:
            pass
        except AttributeError:
            pass
        return {}

    def _get_applied_benefits(self, order_picture):
        order_picture_benefits = self.get_applied_benefits_from_order_picture(order_picture)
        applied_benefits = [
            OrderBenefitInfo(
                    benefit.get("benefit"),
                    benefit.get("used_discount_ids"),
                    benefit.get("added_sale_lines"),
                    benefit.get("burnt_items"),
                    benefit.get("promotion_tender"),
                    benefit.get("discount_item_values"),
                    benefit.get("payment_discount_id")
            )
            for benefit in order_picture_benefits
        ]

        return applied_benefits

    def to_json(self):
        return dumps(self, default=lambda o: o.__dict__ if hasattr(o, '__dict__') else str(o), sort_keys=True)
