from typing import (
    Dict,
    Any,
    List,
    Union,
)

from ._PreAccountBuilder import PreAccountBuilder
from nreports_api.shared import JSON
from table_manager_api.dto import Table
from order_api.dto import SaleLine as SaleLineApi
from nreports.services import PreAccountOrdersRetrieverService
from nreports.model import (
    GroupedSaleItems,
    PreAccountSeatTotals,
)
from .._Util import build_util_variables

LABEL_TABLE = "$TABLE"


class PreAccountBySeatsBuilder(PreAccountBuilder):

    def build_data(
        self,
        data_to_build: Dict[str, JSON],
        **kwargs: Any,
    ) -> Dict[str, JSON]:

        pos_id = int(kwargs["posId"])
        table_id = kwargs.get("tableId")
        filter_table_seat = kwargs.get("filterTableSeat", "0")
        printed_by = kwargs.get("printBy", LABEL_TABLE)

        data_to_build = build_util_variables(
            constants=self.constants,
            data_to_build=data_to_build,
            **kwargs,
        )

        order_retriever_service = PreAccountOrdersRetrieverService(
            message_bus=self._message_bus,
            user_repository=self.user_repository,
            store_config_repository=self.store_config_repository,
        )
        table = order_retriever_service.get_table_order(
            pos_id=pos_id,
            table_id=table_id,
        )

        currency_symbol = self.i18n_repository.build_l10n_repository(pos_id=pos_id).get_currency_symbol()
        data_to_build["CURRENCY_SYMBOL"] = currency_symbol.strip()

        self._build_header(
            data_to_build=data_to_build,
            table=table,
        )

        cut_paper_by_seats = self.constants.get("PRE_ACCOUNT_CUT_PAPER_BY_SEAT", "false").lower() == "true"
        self._fill_sale_lines_by_seat(
            table=table,
            filter_table_seat=filter_table_seat,
            printed_by=printed_by,
            cut_paper_by_seats=cut_paper_by_seats,
            data_to_build=data_to_build,
        )

        data_to_build["CUT_PAPER_BEFORE_TOTALS"] = False
        if filter_table_seat == "0" and cut_paper_by_seats:
            data_to_build["CUT_PAPER_BEFORE_TOTALS"] = True

        data_to_build["SHOW_PRE_ACCOUNT_TOTALS"] = False
        if filter_table_seat == "0":
            data_to_build["SHOW_PRE_ACCOUNT_TOTALS"] = True
            self._build_totals(
                data_to_build=data_to_build,
                table=table,
            )

        self._build_footer(data_to_build=data_to_build)

        return data_to_build

    def _fill_sale_lines_by_seat(
        self,
        table: Table,
        filter_table_seat: str,
        printed_by: str,
        cut_paper_by_seats: bool,
        data_to_build: Dict[str, JSON],
    ) -> None:

        grouped_sale_lines = self._get_sale_lines_grouped_by_seats(
            table=table,
            printed_by=printed_by,
        )
        seat_quantities = self._get_seat_quantities(
            grouped_sale_lines=grouped_sale_lines,
            printed_by=printed_by,
        )
        table_sale_lines = grouped_sale_lines[LABEL_TABLE] if LABEL_TABLE in grouped_sale_lines else {}
        table_sub_total = 0.0 + self._get_sub_total_table(sale_lines=table_sale_lines)
        tip_excluded_amount = sum(table.detailed_tip_excluded.values())
        tip_percentage = self._get_percentage(
            amount=table.tip,
            sub_total_amount=(table.sub_total_amount - tip_excluded_amount),
        )
        table_tip = self._get_table_tip(
            table=table,
            table_sub_total=table_sub_total,
            tip_percentage=tip_percentage,
        )
        table_payment = table.detailed_payments[0] if 0 in table.detailed_payments.keys() else 0.0
        table_per_seat = round(table_sub_total / seat_quantities, 2)
        table_discount_per_seats = self._get_table_discount_per_seats(
            discounts_groups=table.discount_groups,
            table_sale_lines=table_sale_lines,
        )

        data_by_seats: Dict[str, JSON] = {}
        sorted_grouped_sale_lines = sorted(grouped_sale_lines, key=lambda g: (1, g) if isinstance(g, int) else (0, g))
        for index, seat in enumerate(sorted_grouped_sale_lines):
            if filter_table_seat != "0" and filter_table_seat != str(seat) and seat != "0":
                continue

            sale_lines = grouped_sale_lines[seat]
            if not sale_lines:
                continue

            is_table = seat == LABEL_TABLE
            seat_sub_total = sum([float(x.item_price) if x.item_price and x.quantity != 0 else 0.0 for x in sale_lines])

            pre_account_seat_totals = PreAccountSeatTotals(
                sale_lines=sale_lines,
                table=table,
                seat=seat,
                table_sub_total=table_sub_total,
                table_tip=table_tip,
                seat_quantities=seat_quantities,
                table_payment=table_payment,
            )
            if not is_table:
                pre_account_seat_totals.build_totals(
                    seat_sub_total=seat_sub_total,
                    seat_quantities=seat_quantities,
                    table_discount_per_seats=table_discount_per_seats,
                    table_per_seat=table_per_seat,
                    tip_percentage=tip_percentage,
                )

            data_by_seats[seat] = data_to_build["template"].copy()
            self._build_separator_seats(
                filter_table_seat=filter_table_seat == "0",
                data_by_seats=data_by_seats[seat],
                cut_paper_by_seats=cut_paper_by_seats,
            )

            data_by_seats[seat]["DIVISION_FOR_TITLE"] = self._get_division_for_title(
                seat=seat,
                is_table=is_table,
                printed_by=printed_by,
            )
            not_is_last_item = (index + 1) < len(sorted_grouped_sale_lines) and filter_table_seat == "0"
            data_by_seats[seat]["NOT_IS_LAST_ITEM"] = not_is_last_item

            self._build_conditions(
                data_by_seats=data_by_seats,
                seat=seat,
                seat_sub_total=seat_sub_total,
                table_per_seat=table_per_seat,
            )

            self._build_discount_fields(
                discount_groups=pre_account_seat_totals.discounts_groups_sale_lines,
                total_discounts=pre_account_seat_totals.total_seat_discount,
                data_to_build=data_by_seats[seat],
                list_name="LIST_DISCOUNT_SEAT",
                condition_name="HAS_DISCOUNT_LIST_SEAT"
            )

            self._build_amounts(
                data_by_seats=data_by_seats,
                seat=seat,
                is_table=is_table,
                seat_payment=pre_account_seat_totals.seat_payment,
                seat_tip=pre_account_seat_totals.seat_tip,
                seat_total=pre_account_seat_totals.seat_total,
            )

            products_lines = self._build_product_lines(sales_line_list=sale_lines)
            data_by_seats[seat]["ITEM_PRODUCT"] = products_lines

        data_to_build["saleLinesInfo"].extend(data_by_seats.values())

    @staticmethod
    def _build_separator_seats(
        filter_table_seat: bool,
        data_by_seats: Dict[str, JSON],
        cut_paper_by_seats: bool,
    ) -> None:

        data_by_seats["CUT_PAPER"] = False
        data_by_seats["SEPARATOR_DASH"] = False
        if not filter_table_seat:
            return

        if cut_paper_by_seats:
            data_by_seats["CUT_PAPER"] = True
            return

        data_by_seats["SEPARATOR_DASH"] = True

    def _get_sale_lines_grouped_by_seats(
        self,
        table: Table,
        printed_by: str,
    ) -> Dict[Union[int, str], List[SaleLineApi]]:

        grouped_sales_lines: Dict[str, List[SaleLineApi]] = {}
        grouped_sale_lines = self._get_grouped_sale_lines(table_orders=table.orders)
        fraction_line_numbers = []
        for _, sale_lines in grouped_sale_lines.items():
            fraction_line_numbers.extend(self._get_fraction_sale_line_identifiers(sales_line_list=sale_lines))

        hide_items_without_price = self.constants.get("HIDE_UNPRICED_ITEMS_FROM_PRE_ACOUNT", "true").lower() == "true"
        for seat, sale_lines in grouped_sale_lines.items():
            sale_lines_by_seat = GroupedSaleItems().grouped_sale_lines(
                sale_lines=sale_lines,
                hide_items_without_price=hide_items_without_price,
                fraction_line_numbers=fraction_line_numbers,
            )
            grouped_sales_lines.setdefault(seat, sale_lines_by_seat)

        if printed_by == "TAB":
            return {key: value for key, value in grouped_sales_lines.items() if value}

        return grouped_sales_lines

    @staticmethod
    def _get_table_tip(
        table: Table,
        table_sub_total: float,
        tip_percentage: float,
    ) -> float:

        table_tip_excluded = table.detailed_tip_excluded[0] if 0 in table.detailed_tip_excluded.keys() else 0.0
        table_tip = ((tip_percentage * (table_sub_total - table_tip_excluded)) / 100)
        return table_tip

    @staticmethod
    def _get_seat_quantities(
        grouped_sale_lines: Dict[Union[int, str], List[SaleLineApi]],
        printed_by: str,
    ) -> int:

        seat_reduction = 1 if LABEL_TABLE in grouped_sale_lines and printed_by != "TAB" else 0
        seat_quantities = len(grouped_sale_lines.keys()) - seat_reduction
        return seat_quantities

    @staticmethod
    def _build_amounts(
        data_by_seats: Dict[str, JSON],
        seat: Union[int, str],
        is_table: bool,
        seat_payment: float,
        seat_tip: float,
        seat_total: float,
    ) -> None:

        data_by_seats[seat]["HAS_PROMOTIONAL_TENDERS_SEAT"] = seat_payment > 0 and not is_table
        data_by_seats[seat]["PROMOTIONAL_TENDERS_SEAT"] = -seat_payment
        data_by_seats[seat]["HAS_TIP_AMOUNT"] = seat_tip > 0
        data_by_seats[seat]["TOTAL_WITHOUT_TIP_SEAT"] = seat_total - seat_tip
        data_by_seats[seat]["TIP_SEAT"] = seat_tip
        data_by_seats[seat]["HAS_TOTAL_SEAT"] = not is_table
        data_by_seats[seat]["TOTAL_SEAT"] = seat_total

    @staticmethod
    def _build_conditions(
        data_by_seats: Dict[str, JSON],
        seat: Union[int, str],
        seat_sub_total: float,
        table_per_seat: float,
    ) -> None:

        is_table = seat == LABEL_TABLE
        data_by_seats[seat]["IS_DIVISION_FOR_SEATS"] = table_per_seat > 0 and is_table
        data_by_seats[seat]["DIVISION_FOR_SEATS"] = table_per_seat
        data_by_seats[seat]["IS_DIVISION_FOR_TABLE"] = table_per_seat > 0 and not is_table
        data_by_seats[seat]["DIVISION_FROM_TABLE"] = table_per_seat
        data_by_seats[seat]["SUB_TOTAL_SEAT"] = seat_sub_total

    @staticmethod
    def _get_sub_total_table(
        sale_lines: List[SaleLineApi],
    ) -> float:

        return sum([float(x.item_price) if x.item_price and x.quantity != 0 else 0.0 for x in sale_lines])

    @staticmethod
    def _get_division_for_title(
        seat: str,
        is_table: bool,
        printed_by: str,
    ) -> str:

        if is_table:
            return seat

        name_by = "$SEAT_NUMBER" if printed_by == "SEATS" else "$TAB_NAME"
        return seat if is_table else f"{name_by} {seat}"

    @staticmethod
    def _get_table_discount_per_seats(
        discounts_groups: Dict[str, Dict[str, float]],
        table_sale_lines: List[SaleLineApi],
    ) -> Dict[str, float]:

        table_discount_per_seats = {}
        for table_sale_lines in table_sale_lines:
            for group in discounts_groups:
                if group not in table_discount_per_seats:
                    table_discount_per_seats[group] = 0

                line_id = "{}.{}-{}.{}".format(
                    table_sale_lines.line_number,
                    table_sale_lines.level,
                    table_sale_lines.item_id,
                    table_sale_lines.part.part_code,
                )
                table_discount_per_seats[group] = discounts_groups[group].get(line_id, 0)

        return table_discount_per_seats

    @staticmethod
    def _get_sale_lines(
        sale_lines: List[SaleLineApi],
    ) -> List[JSON]:

        item_products = []
        for sale_line in sale_lines:
            item_products.append({
                "ITEM_PRODUCT_QUANTITY": sale_line.quantity,
                "ITEM_PRODUCT_NAME": sale_line.part.product_name,
                "ITEM_PRODUCT_TOTAL_PRICE": sale_line.item_price,
            })

        return item_products

    @staticmethod
    def _get_percentage(
        amount: float,
        sub_total_amount: float,
    ) -> float:

        if amount > 0:
            return round((amount * 100) / sub_total_amount, 2)

        return 0
