import json
from datetime import datetime
from xml.etree import cElementTree as eTree
from typing import (
    Dict,
    Union,
    List,
    Tuple,
    Optional,
)

from helper import round_half_away_from_zero
from messagebus import MessageBus

from nreports.repository import (
    UserRepository,
    StoreConfigRepository,
)
from order_api import OrderService
from table_manager_api import TableService
from table_manager_api.dto import (
    Table,
    parse_service_tenders,
)

from order_api.dto import (
    SaleLine as SaleLineApi,
    CustomProperty,
    Tender,
    parse_order,
)


class PreAccountOrdersRetrieverService(object):

    def __init__(
        self,
        message_bus: MessageBus,
        user_repository: UserRepository,
        store_config_repository: StoreConfigRepository
    ) -> None:

        self._message_bus = message_bus
        self._user_repository = user_repository
        self._store_config_repository = store_config_repository

    def get_table_order(
        self,
        pos_id: int,
        table_id: int,
    ) -> Table:

        table = self.get_table(
            pos_id=pos_id,
            table_id=table_id,
        )
        tip_rate = self._store_config_repository.get_tip_rate()
        table = self._calculate_table_totals(
            table=table,
            tip_rate=tip_rate,
        )
        return table

    def get_parsed_table_order_from_quick_service(
        self,
        pos_id: int,
        order_id: int,
    ) -> Table:

        order_picture_xml = self._get_order_picture(
            pos_id=pos_id,
            order_id=order_id,
        )
        created_at = order_picture_xml.get("createdAtGMT")
        start_date = datetime.strptime(created_at, "%Y-%m-%dT%H:%M:%S.%f") if created_at else None
        service_tenders = parse_service_tenders(service_tenders=order_picture_xml.findall('TenderHistory/Tender'))
        order = parse_order(order_xml=order_picture_xml)
        table = Table(
            table_id="",
            user_id=self._get_user_id_from_session_id(session_id=order_picture_xml.get("sessionId")),
            total_amount=float(order_picture_xml.get("totalAmount")),
            total_discounts=float(order_picture_xml.get("discountAmount")),
            start_date=start_date,
            service_tenders=service_tenders,
            orders=[order],
        )
        table = self._calculate_table_totals(table=table)
        return table

    def get_table(
        self,
        pos_id: int,
        table_id: int,
    ) -> Table:

        table_service = TableService(
            message_bus=self._message_bus,
            pos_id=pos_id,
        )
        return table_service.get_table(table_id=table_id)

    def get_table_picture(
        self,
        pos_id: int,
        table_id: int,
    ) -> eTree.Element:

        table_service = TableService(
            message_bus=self._message_bus,
            pos_id=pos_id,
        )
        return table_service.get_table_picture(table_id=table_id)

    def _get_order_picture(
        self,
        pos_id: int,
        order_id: int,
    ) -> Optional[eTree.Element]:

        order_service = OrderService(
            message_bus=self._message_bus,
            pos_id=pos_id,
        )

        return order_service.get_order_picture(order_id=order_id)

    def _calculate_table_totals(
        self,
        table: Table,
        tip_rate: Optional[int] = None
    ) -> Table:

        table_order_total_gross = 0.0
        total_discount_amount = 0.0
        total_tip = 0.0

        discount_by_seat: Dict[str, float] = {}
        discount_groups: Dict[str, str] = {}
        payment_by_seat: Dict[str, float] = {}
        payment_by_benefit_key: Dict[str, float] = {}
        tip_excluded_by_seat: Dict[str, float] = {}
        used_benefit_keys: List[str] = []
        discounts_tip_excluded: List[int] = []

        for order_api in table.orders:
            if order_api.state != "TOTALED":
                table.tip = (order_api.total_gross * tip_rate) / 100 if tip_rate else 0.0

            total_tip += table.tip
            table_order_total_gross += order_api.total_gross
            total_discount_amount += order_api.discount_amount

            custom_order_properties = order_api.custom_properties
            order_tenders = order_api.tenders
            item_value_discounts, split_key = self._handle_custom_properties(
                custom_order_properties=custom_order_properties,
                discounts_tip_excluded=discounts_tip_excluded,
                order_tenders=order_tenders,
                payment_by_benefit_key=payment_by_benefit_key,
                table=table,
            )

            self._extract_sales_lines(
                order_id=order_api.order_id,
                order_sales=order_api.sale_lines,
                service_seats=table.service_seats,
                item_discount_values=item_value_discounts,
                discount_by_seat=discount_by_seat,
                payment_by_seat=payment_by_seat,
                payment_by_benefit=payment_by_benefit_key,
                used_benefit_keys=used_benefit_keys,
                discounts_tip_excluded=discounts_tip_excluded,
                tip_excluded_by_seat=tip_excluded_by_seat,
                table_mode=table.table_mode,
                tabs_names=table.tabs_names,
            )

            table.split_key = split_key
            table.discount_groups = discount_groups

        if table.user_id:
            table.user_name = self._user_repository.get_user_name(user_id=table.user_id)

        table.order_id = ",".join([str(order.order_id) for order in table.orders])
        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(
            number=total_tip,
            precision=2,
        )
        total_amount = table_order_total_gross + table.tip - table.promotional_tenders
        table.total_amount = round_half_away_from_zero(
            number=total_amount,
            precision=2,
        )

        return table

    def _extract_sales_lines(
        self,
        order_id: int,
        order_sales: List[SaleLineApi],
        service_seats: int,
        item_discount_values: Dict[str, Dict[str, float]],
        discount_by_seat: Dict[str, float],
        payment_by_seat: Dict[str, float],
        payment_by_benefit: Dict[str, float],
        used_benefit_keys: List[str],
        discounts_tip_excluded: List[int],
        tip_excluded_by_seat: Dict[str, float],
        table_mode: str,
        tabs_names: Dict[str, str],
    ) -> List[SaleLineApi]:

        active_sale_lines = self._get_active_sale_lines(order_sales=order_sales)
        values_by_discount = self._get_discounts_from_sale_lines(sale_lines=active_sale_lines)

        sale_lines: List[SaleLineApi] = []
        fractions_dict: Dict[str, int] = {}
        for sale_line in active_sale_lines:

            custom_properties = sale_line.custom_properties
            self._set_sale_line_custom_property(
                custom_properties=custom_properties,
                key="orderId",
                value=str(order_id),
            )
            seat = self._get_seat(
                custom_properties=custom_properties,
                service_seats=service_seats,
                table_mode=table_mode,
                tabs_names=tabs_names,
            )

            self._set_fraction_dict(
                custom_properties=custom_properties,
                fractions_dict=fractions_dict,
                item_id=sale_line.item_id,
                line_number=sale_line.line_number,
                part_code=sale_line.part.part_code,
            )

            current_fraction_parent = fractions_dict.get(f"{sale_line.line_number}-{sale_line.item_id}", 0)
            if current_fraction_parent:
                sale_line.fraction_parent = current_fraction_parent

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

            if "benefitKey" in custom_properties:
                benefit_key = custom_properties.get("benefitKey")
                self._add_payments_from_benefit_to_seat(
                    seat=seat,
                    benefit_key=benefit_key,
                    payment_by_seat=payment_by_seat,
                    payment_by_benefit=payment_by_benefit,
                    used_benefit_keys=used_benefit_keys,
                )

            discount_value = self._get_discount_value(
                values_by_items=item_discount_values,
                values_by_discount=values_by_discount,
                tip_excluded_by_seat=tip_excluded_by_seat,
                discounts_tip_excluded=discounts_tip_excluded,
                line_id=sale_line.get_line_id(),
                item_price=sale_line.item_price,
                seat=seat
            )
            self._add_value_to_dict(
                key=seat,
                value=discount_value,
                dict_by_key=discount_by_seat,
            )

            sale_line.seat = seat
            sale_lines.append(sale_line)

        return sale_lines

    @staticmethod
    def _set_fraction_dict(
        custom_properties: Dict[str, str],
        fractions_dict: Dict[str, int],
        item_id: str,
        line_number: int,
        part_code: int,
    ) -> None:

        fraction = None
        if "fraction" in custom_properties:
            fraction = int(custom_properties.get("fraction") or 0)
            fraction = fraction if fraction > 1 else None

        if fraction:
            key = f"{line_number}-{item_id}.{part_code}"
            fractions_dict[key] = fraction

    @staticmethod
    def _get_seat(
        custom_properties: Dict[str, str],
        service_seats: int,
        table_mode: str,
        tabs_names: Dict[str, str],
    ) -> int:

        if table_mode == "tabs" and "tab" in custom_properties:
            return tabs_names.get(custom_properties.get("tab"), 0)

        if "seat" in custom_properties:
            seat = int(custom_properties.get("seat"))
            return 0 if seat > service_seats else seat

        return 0

    @staticmethod
    def _get_active_sale_lines(
        order_sales: List[SaleLineApi],
    ) -> List[SaleLineApi]:

        deleted_line_numbers = map(
            lambda x: x.line_number,
            filter(lambda x: x.level == 0 and x.quantity == 0, order_sales)
        )

        return list(filter(lambda x: x.line_number not in deleted_line_numbers, order_sales))

    def _get_discount_value(
        self,
        values_by_items: Dict[str, Dict[str, float]],
        values_by_discount: Dict[int, float],
        tip_excluded_by_seat: Dict[str, float],
        discounts_tip_excluded: List[int],
        line_id: str,
        item_price: float,
        seat: 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 discount_line_number != line_id:
                    continue

                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(
                        key=seat,
                        value=value,
                        dict_by_key=tip_excluded_by_seat,
                    )

        return total_value

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

        if benefit_key in used_benefit_keys or benefit_key not in payment_by_benefit:
            return

        self._add_value_to_dict(
            key=seat,
            value=payment_by_benefit[benefit_key],
            dict_by_key=payment_by_seat,
        )
        used_benefit_keys.append(benefit_key)

    def _get_service_period(
        self,
        sales_line_list: List[SaleLineApi],
        sale_line: SaleLineApi,
        properties: Dict[str, str],
    ) -> str:

        if sale_line.level == 0:
            return self._get_service_period_abbreviation(properties=properties)

        item_service_period = self._get_parent_service_period(
            sales_line_list=sales_line_list,
            line_number=int(sale_line.line_number),
        )

        return item_service_period

    def _get_parent_service_period(
        self,
        sales_line_list: List[SaleLineApi],
        line_number: int,
    ) -> Union[str, None]:

        for sale_line in sales_line_list:
            if sale_line.level != 0 or sale_line.line_number != line_number:
                continue

            return self._get_service_period_abbreviation(properties=sale_line.custom_properties)

    @staticmethod
    def _get_user_id_from_session_id(
        session_id: str,
    ) -> Optional[str]:

        for data in session_id.split(","):
            if data.startswith("user="):
                return data.split("=")[1]

        return None

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

        if "servicePeriod" not in properties:
            return None

        props = json.loads(properties["servicePeriod"])
        return props.get("servicePeriodAbbr", None)

    def _get_discounts_from_sale_lines(
        self,
        sale_lines: List[SaleLineApi],
    ) -> Dict[int, float]:

        discount_values: Dict[int, float] = {}
        for sale_line in sale_lines:
            discounts_applied = sale_line.discounts_applied
            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(
                    key=discount_id,
                    value=amount,
                    dict_by_key=discount_values,
                )

        return discount_values

    def _handle_custom_properties(
        self,
        custom_order_properties: List[CustomProperty],
        discounts_tip_excluded: List[int],
        order_tenders: List[Tender],
        payment_by_benefit_key: Dict[str, float],
        table: Table,
    ) -> Tuple[Dict[str, Dict[str, float]], str]:

        split_key = "1"
        split_key_found = False
        item_value_discounts: Dict[str, Dict[str, float]] = {}
        for custom_property in custom_order_properties:

            if not split_key_found and custom_property.key == "SPLIT_KEY":
                split_key = custom_property.value
                split_key_found = True

            if custom_property.key != "BENEFIT_LIST":
                continue

            benefit_list = json.loads(custom_property.value)
            self._parse_order_discounts(
                table=table,
                benefit_list=benefit_list,
            )
            item_value_discounts.update(benefit_list.get("discountItemValues", {}))
            discounts_tip_excluded.extend(benefit_list.get("discountIdTipExcluded", []))
            self._get_promotional_tenders(
                benefit_list=benefit_list,
                table=table,
                payment_by_benefit_key=payment_by_benefit_key,
                order_tenders=order_tenders,
            )

        return item_value_discounts, split_key

    def _get_promotional_tenders(
        self,
        benefit_list: Dict[str, str],
        table: Table,
        payment_by_benefit_key: Dict[str, float],
        order_tenders: List[Tender],
    ) -> 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(
                    key=benefit_key,
                    value=amount,
                    dict_by_key=payment_by_benefit_key,
                )

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

        tenders = {}
        for order_tender in order_tenders:
            self._add_value_to_dict(
                key=order_tender.tender_id,
                value=order_tender.amount,
                dict_by_key=tenders,
            )

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

    @staticmethod
    def _parse_order_discounts(
        table: Table,
        benefit_list: Dict,
    ) -> None:

        table_discount_groups: Dict[str, Dict[str, float]] = {}
        detailed_discounts = json.loads(benefit_list.get("benefitList", "[]"))
        for discount in detailed_discounts:
            group = discount.get("benefit", {}).get("group", "$OTHER_DISCOUNTS")
            discount_values = discount.get("discount_item_values", {})
            for discount_id, sale_lines in discount_values.items():
                for sale_line, discount_value in sale_lines.items():
                    if discount_value == 0:
                        continue

                    table_discount_groups.setdefault(group, {})
                    table_discount_groups[group].setdefault(sale_line, 0)
                    table_discount_groups[group][sale_line] += discount_value

        table.discount_groups = table_discount_groups

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

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

    @staticmethod
    def _set_sale_line_custom_property(
        custom_properties: Dict[str, str],
        key: str,
        value: str,
    ) -> None:

        custom_properties.setdefault(key, value)
