import json
from datetime import datetime

from domain import TableOrderRetriever, TableOrderSaleLine, TableService, TableOrder, Table
from xml.etree import cElementTree as eTree
from sysactions import get_user_information
from old_helper import round_half_away_from_zero

from typing import List, Dict, Union  # noqa


class MwTableOrderRetriever(TableOrderRetriever):
    def __init__(self, table_service):
        # type: (TableService) -> None
        self.table_service = table_service

    def get_table_order(self, pos_id, table_id):
        # type: (str, str) -> Table

        xml_table_picture = self.table_service.get_table_picture(pos_id, table_id)

        xml = eTree.XML(xml_table_picture)
        table = Table()

        table_xml = xml.find(".")
        table.table_id = table_xml.attrib["id"]
        table.total_amount = float(self._get_table_attribute_value(table_xml.attrib, "totalAmount", 0))
        table.service_seats = int(self._get_table_attribute_value(table_xml.attrib, "serviceSeats", 0))
        table.total_per_seat = float(self._get_table_attribute_value(table_xml.attrib, "totalPerSeat", 0))
        table.total_discounts = float(self._get_table_attribute_value(table_xml.attrib, "discountAmount", 0))
        tip_rate = self.table_service.get_tip_rate()
        default_tip_amount = (table.total_amount * tip_rate) / 100 if tip_rate else 0.0
        table.tip = float(table_xml.get("tip") if table_xml.get("tip") is not None else default_tip_amount)
        start_date = self._get_table_attribute_value(table_xml.attrib, "startTS", None)
        table.start_date = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S") if start_date else None
        table.user_id = table_xml.attrib["userId"] if "userId" in table_xml.attrib else ""
        user_information = eTree.XML(get_user_information(table.user_id)).find("user")
        table.user_name = user_information.get("LongName")
        self._get_table_name(table, table_xml)

        table_order_total_gross = 0.0
        total_discount_amount = 0.0
        total_tip = 0.0

        discount_by_seat = {}
        payment_by_seat = {}
        payment_by_benefit_key = {}
        tip_excluded_by_seat = {}
        used_benefit_keys = []
        discounts_tip_excluded = []
        for order_xml in xml.findall("Orders/Order"):
            total_order_amount = float(order_xml.get("totalAmount"))
            total_order_after_discount = order_xml.get("totalAfterDiscount")
            order_discount_amount = order_xml.get("discountAmount")
            order_state = order_xml.get("state")
            default_order_tip_amount = (float(order_xml.get("totalGross")) * tip_rate) / 100 if tip_rate else 0.0
            if order_state != "TOTALED":
                tip = default_order_tip_amount
            else:
                tip = float(order_xml.get("tip"))
            total_tip += tip
            table_order_total_gross += float(order_xml.get("totalGross"))
            total_discount_amount += float(order_discount_amount)
            total_order_tender = order_xml.get("totalTender")
            order_state = order_xml.get("state")
            order_id = order_xml.get("orderId")
            custom_order_properties = order_xml.findall("CustomOrderProperties/OrderProperty")
            order_tenders = order_xml.findall("TenderHistory/Tender")

            split_key = "1"
            split_key_found = False
            item_value_discounts = {}
            for custom_property in custom_order_properties:
                properties = custom_property.find(".")
                if "key" in properties.attrib:
                    if not split_key_found and properties.attrib["key"] == "SPLIT_KEY":
                        split_key = properties.attrib["value"]
                        split_key_found = True
                    if properties.attrib["key"] == "BENEFIT_LIST":
                        benefit_list = json.loads(properties.attrib["value"])
                        item_value_discounts.update(benefit_list.get("discountItemValues", {}))
                        discounts_tip_excluded.extend(benefit_list.get("discountIdTipExcluded", []))
                        self._get_promotional_tenders(benefit_list, table, payment_by_benefit_key, order_tenders)

            seats = table.service_seats
            order_sales = order_xml.findall("SaleLine")
            sale_lines = self._extract_sales_lines(order_sales, seats, item_value_discounts, discount_by_seat,
                                                   payment_by_seat, payment_by_benefit_key, used_benefit_keys,
                                                   discounts_tip_excluded, tip_excluded_by_seat)

            order = TableOrder(order_id, order_state, total_order_amount, total_order_after_discount,
                               total_order_tender, order_discount_amount, sale_lines, split_key, tip)
            table.orders.append(order)

        for benefit_key in payment_by_benefit_key.keys():
            self._add_payments_from_benefit_to_seat(
                    0,
                    benefit_key,
                    payment_by_seat,
                    payment_by_benefit_key,
                    used_benefit_keys
            )

        table.detailed_tip_excluded = tip_excluded_by_seat
        table.detailed_discounts = discount_by_seat
        table.detailed_payments = payment_by_seat
        table.sub_total_amount = table_order_total_gross + total_discount_amount
        table.tip = round_half_away_from_zero(total_tip, 2)
        total_amount = table_order_total_gross + table.tip - table.promotional_tenders
        table.total_amount = round_half_away_from_zero(total_amount, 2)

        return table

    @staticmethod
    def _get_table_name(table, table_xml):
        property_xml = table_xml.findall(".//Properties[@TAB_ID]")
        tab_name = property_xml[0].attrib["TAB_ID"] if property_xml else None
        table_name = table_xml.attrib["name"] if "name" in table_xml.attrib else table.table_id
        table.table_name = tab_name if tab_name else table_name

    @staticmethod
    def _get_table_attribute_value(table_xml_attrib, key, default_value):
        if key not in table_xml_attrib:
            return default_value

        return table_xml_attrib[key]

    def _get_discounts_from_sale_lines(self, sale_lines):
        # type: (List) -> Dict

        discount_values = {}
        for sale_line in sale_lines:
            discounts_applied = sale_line.get("discountsApplied") or None
            if discounts_applied is None:
                continue
            for discount in discounts_applied.split(','):
                discount_id, amount = discount.split(':')
                discount_id = int(discount_id)
                self._add_value_to_dict(discount_id, amount, discount_values)

        return discount_values

    def _get_discount_value(self, values_by_items, values_by_discount, tip_excluded_by_seat, discounts_tip_excluded,
                            sale_line_number, item_price, seat):
        # type: (Dict, Dict, Dict, List[int], int, float, int) -> float

        total_value = 0.0
        for discount_id in values_by_items.keys():
            for discount_line_number in values_by_items[discount_id]:
                if int(discount_line_number) == sale_line_number:
                    value = min(item_price, values_by_items[discount_id][discount_line_number])
                    values_by_items[discount_id][discount_line_number] -= value
                    values_by_discount[int(discount_id)] -= value
                    total_value += value

                    if int(discount_id) in discounts_tip_excluded:
                        self._add_value_to_dict(seat, value, tip_excluded_by_seat)

        return total_value

    @staticmethod
    def _get_service_period_abbreviation(properties):
        # type: (dict) -> Union[str, None]

        props = {}
        if 'servicePeriod' in properties:
            props = json.loads(properties['servicePeriod'])
        return props['servicePeriodAbbr'] if 'servicePeriodAbbr' in props else None

    def _get_parent_service_period(self, sales_line_list, line_number):
        # type: (List[eTree.Element], int) -> Union[str, None]

        for x in sales_line_list:
            if int(x.get("level")) != 0 or x.get("lineNumber") != line_number:
                continue
            props = x.get("customProperties")
            props = json.loads(props) if props else {}
            service_period = self._get_service_period_abbreviation(props)
            if service_period:
                return service_period
        return None

    def _get_service_period(self, sales_line_list, sale_line, properties):
        # type: (List[eTree.Element], eTree.Element, dict) -> str

        if int(sale_line.get("level")) == 0:
            return self._get_service_period_abbreviation(properties)
        else:
            item_service_period = self._get_parent_service_period(sales_line_list, sale_line.get("lineNumber"))

        return item_service_period

    def _extract_sales_lines(
            self,
            order_sales,                # type: List[eTree.Element]
            service_seats,              # type: int
            item_discount_values,       # type: Dict
            discount_by_seat,           # type: Dict
            payment_by_seat,            # type: Dict
            payment_by_benefit,         # type: Dict
            used_benefit_keys,          # type: List[str]
            discounts_tip_excluded,     # type: List[str]
            tip_excluded_by_seat        # type: Dict
    ):
        # type: (...) -> List[TableOrderSaleLine]

        deleted_line_numbers = map(lambda x: x.get("lineNumber"),
                                   filter(lambda x: x.get("level") == "0" and x.get("qty") == "0", order_sales))
        active_sale_lines = filter(lambda x: x.get("lineNumber") not in deleted_line_numbers, order_sales)

        values_by_discount = self._get_discounts_from_sale_lines(active_sale_lines)

        sales_line = []  # type: List[TableOrderSaleLine]

        for sale_line in active_sale_lines:
            products = sale_line.get("productName")
            price = float(sale_line.get("itemPrice") or 0)
            level = int(sale_line.get("level") or 0)
            qty = float(sale_line.get("qty") or 0)  if level == 0 else float(sale_line.get("multipliedQty") or 0)
            default_qty = int(sale_line.get("defaultQty")) if "defaultQty" in sale_line.attrib else 0
            item_type = sale_line.get("itemType")
            line_number = int(sale_line.get("lineNumber") or 0)
            comment = sale_line.find("Comment")
            if comment is not None:
                comment = comment.get("comment")

            seat = 0
            props = sale_line.get("customProperties")
            custom_properties = json.loads(props) if props else {}
            if 'seat' in custom_properties:
                seat = int(custom_properties['seat'])
                seat = 0 if seat > service_seats else seat

            service_period = self._get_service_period(active_sale_lines, sale_line, custom_properties)

            if 'benefitKey' in custom_properties:
                benefit_key = custom_properties['benefitKey']
                self._add_payments_from_benefit_to_seat(
                        seat,
                        benefit_key,
                        payment_by_seat,
                        payment_by_benefit,
                        used_benefit_keys
                )

            discount_value = self._get_discount_value(item_discount_values, values_by_discount, tip_excluded_by_seat,
                                                      discounts_tip_excluded, line_number, price, seat)
            self._add_value_to_dict(seat, discount_value, discount_by_seat)

            sale_line = TableOrderSaleLine(
                products, price, qty, default_qty, item_type, seat, level, comment, line_number, service_period
            )

            sales_line.append(sale_line)

        return sales_line

    def _get_promotional_tenders(self, benefit_list, table, payment_by_benefit_key, order_tenders):
        # type: (Dict, Table, Dict, List) -> None

        benefits = json.loads(benefit_list.get("benefitList"))
        benefits_with_tender = [x for x in benefits if x.get("promotion_tender")]
        for benefit in benefits_with_tender:
            benefit_key = benefit.get("benefit_key")
            for tender in benefit.get("promotion_tender"):
                amount = float(tender.get("amount"))
                self._add_value_to_dict(benefit_key, amount, payment_by_benefit_key)

        if not order_tenders or len(order_tenders) == 0:
            return

        tenders = {}
        for order_tender in order_tenders:
            tender_id = int(order_tender.get("tenderId", -1))
            amount = float(order_tender.get("tenderAmount", 0))
            self._add_value_to_dict(tender_id, amount, tenders)

        for benefit in benefits:
            for tender_id in benefit.get("order_tenders_ids"):
                amount = tenders.get(tender_id, 0)
                table.promotional_tenders += amount

    def _add_payments_from_benefit_to_seat(
            self,
            seat,                   # type: int
            benefit_key,            # type: str
            payment_by_seat,        # type: Dict
            payment_by_benefit,     # type: Dict
            used_benefit_keys       # type: List[str]
    ):
        # type: (...)-> None

        if benefit_key in used_benefit_keys or benefit_key not in payment_by_benefit:
            return

        self._add_value_to_dict(seat, payment_by_benefit[benefit_key], payment_by_seat)
        used_benefit_keys.append(benefit_key)

    @staticmethod
    def _add_value_to_dict(key, value, dict_by_key):
        # type: ([int, str], float, Dict) -> None

        if key in dict_by_key.keys():
            dict_by_key[key] += float(value)
        else:
            dict_by_key[key] = float(value)
