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

from nreports.builders import DataBuilder
from nreports.model import GroupedSaleItems
from nreports.services import PreAccountOrdersRetrieverService
from nreports.utils import convert_from_utf_to_localtime
from nreports_api.shared import JSON
from order_api.dto import (
    SaleLine as SaleLineApi,
    Order as OrderApi,
)
from table_manager_api.dto import (
    Table,
    ServiceTender,
)


class PreAccountBuilder(DataBuilder):

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

        self._build_mobile_variables(
            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,
        )

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

        self._build_body(
            pos_id=pos_id,
            data_to_build=data_to_build,
            table=table,
        )

        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 _build_mobile_variables(
        self,
        data_to_build: JSON,
        **kwargs: Any,
    ) -> None:

        is_mobile = str(kwargs.get("isMobile", "false")).lower() == "true"
        data_to_build["IS_MOBILE"] = is_mobile
        data_to_build["NOT_IS_MOBILE"] = not is_mobile
        if is_mobile:
            is_small = self.constants.get("MOBILE_PRE_ACCOUNT_FONT_SIZE", "small").lower() == "small"
            data_to_build["IS_SMALL_FONT"] = is_small
            data_to_build["IS_LARGE_FONT"] = not is_small

    @staticmethod
    def _build_header(
        data_to_build: JSON,
        table: Table,
    ) -> None:

        show_service_seats = True if table.table_id else False
        data_to_build["TABLE_NAME"] = table.table_name
        data_to_build["ORDER_ID"] = table.order_id
        data_to_build["SHOW_ORDER_ID"] = True
        data_to_build["SHOW_SERVICE_SEATS"] = show_service_seats
        data_to_build["USER_NAME"] = table.user_name
        data_to_build["SERVICE_SEATS"] = table.service_seats
        data_to_build["START_DATE"] = convert_from_utf_to_localtime(utc=table.start_date)

    def _build_body(
        self,
        pos_id: int,
        data_to_build: JSON,
        table: Table,
    ) -> None:

        grouped_sale_lines = self._get_simplified_sale_lines(table=table)
        products_lines = self._build_product_lines(sales_line_list=grouped_sale_lines)
        data_to_build["ITEM_PRODUCT"] = products_lines

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

    def _get_simplified_sale_lines(
        self,
        table: Table,
    ) -> 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))

        all_orders_sale_lines = []
        for order in table.orders:
            all_orders_sale_lines.extend(order.sale_lines)

        return GroupedSaleItems().grouped_sale_lines(
            sale_lines=all_orders_sale_lines,
            hide_items_without_price=True,
            fraction_line_numbers=fraction_line_numbers,
        )

    def _build_totals(
        self,
        data_to_build: JSON,
        table: Table,
    ) -> None:

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

        self._build_discount_fields(
            discount_groups=table.discount_groups,
            total_discounts=table.total_discounts,
            data_to_build=data_to_build,
        )

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

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

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

    def _build_footer(
        self,
        data_to_build: JSON,
    ) -> None:

        final_message = self.constants.get("PRE_ACCOUNT_FINAL_MESSAGE")

        if final_message == "":
            final_message = u""
        elif final_message is None:
            final_message = "$SERVICE_TAX_IS_NOT_MANDATORY"

        data_to_build["PRE_ACCOUNT_FINAL_MESSAGE"] = final_message

    def _build_discount_fields(
        self,
        discount_groups: Dict[str, float],
        total_discounts: float,
        data_to_build: JSON,
        list_name: Optional[str] = "INNER_LIST_DISCOUNT",
        condition_name: Optional[str] = "HAS_DISCOUNT_LIST",
    ) -> None:

        data_to_build[condition_name] = False
        if total_discounts <= 0:
            return

        data_to_build[condition_name] = True
        total_discount_groups: Dict[str, float] = {}
        for group in discount_groups:
            if group not in total_discount_groups:
                total_discount_groups[group] = 0

            total_discount_groups[group] += discount_groups[group]

        has_groups = len(total_discount_groups) > 0
        if has_groups:
            list_discount = self._parse_discounts_group(discount_groups=total_discount_groups)
        else:
            list_discount = [{
                "DISCOUNT_LABEL": "$DISCOUNTS",
                "DISCOUNT_VALUE": -total_discounts,
            }]

        data_to_build[list_name] = list_discount

    def _build_payments_fields(
        self,
        table: Table,
        data_to_build: JSON,
    ) -> None:

        data_to_build["HAS_SERVICE_TENDERS"] = False
        if not table.service_tenders:
            return

        data_to_build["HAS_SERVICE_TENDERS"] = True
        service_tenders = table.service_tenders
        data_to_build["INNER_LIST_PARTIAL_PAYMENTS"] = self._parse_partial_payments(service_tenders=service_tenders)
        total_paid = sum([float(service_tender.amount) for service_tender in service_tenders])
        data_to_build["TO_PAY"] = table.total_amount - total_paid

    def _build_tip_selection(
        self,
        table: Table,
        data_to_build: JSON,
    ) -> None:

        data_to_build["HAS_SERVICE_SEATS"] = False
        if not table.service_seats:
            return

        available_tips = self.store_config_repository.get_available_tips()
        if not available_tips:
            return

        data_to_build["HAS_SERVICE_SEATS"] = True
        sub_total_amount = table.sub_total_amount
        tip_fields: List[Dict[str, str]] = []
        for tip in available_tips:
            tip_amount = sub_total_amount * (float(tip) / 100)
            total_amount = sub_total_amount + tip_amount
            tip_fields.append(
                {
                    "TIP": tip,
                    "TIP_AMOUNT": tip_amount,
                    "TOTAL_AMOUNT": total_amount
                }
            )

        data_to_build["ITEM_TIP_SELECTION"] = tip_fields

    @staticmethod
    def _build_amount_fields(
        table: Table,
        data_to_build: JSON,
    ) -> None:

        data_to_build["ORDER_TOTAL_GROSS"] = table.sub_total_amount

        data_to_build["HAS_PROMOTIONAL_TENDERS"] = table.promotional_tenders > 0
        data_to_build["PROMOTIONAL_TENDERS"] = table.promotional_tenders

        data_to_build["HAS_TIP_AMOUNT"] = table.tip > 0
        data_to_build["TOTAL_WITHOUT_TIP"] = table.sub_total_amount - table.total_discounts
        data_to_build["TIP_AMOUNT"] = table.tip
        data_to_build["TOTAL_AMOUNT"] = table.total_amount

    @staticmethod
    def _build_division_suggestion(
        table: Table,
        data_to_build: JSON,
    ) -> None:

        data_to_build["HAS_DIVISION_SUGGESTION"] = False
        if table.service_seats < 1:
            return

        data_to_build["HAS_DIVISION_SUGGESTION"] = True
        division_amount = table.total_amount / table.service_seats
        data_to_build["SERVICE_SEATS"] = table.service_seats
        data_to_build["DIVISION_AMOUNT"] = division_amount

    @staticmethod
    def _build_product_lines(
        sales_line_list: List[SaleLineApi],
    ) -> List[Dict[str, str]]:

        item_products: List[Dict[str, str]] = []
        for item in sales_line_list:
            item_service_period = f"[{item.service_period}]" if item.service_period else " "

            item_price = item.item_price or 0
            item_price = f"#NUMBER({item_price})" if item_price else ""

            comment_description = ""
            if item.comment and item.comment.startswith("["):
                comment_description = "$" + item.comment.replace("[", "").replace("]", "").upper() + " "

            product_description = comment_description + item.part.product_name
            fraction = item.fraction_parent
            if fraction:
                numerator = int(round(item.quantity * fraction))
                qty_description = f" {numerator}/{fraction}" if numerator != fraction else "1.00"
            else:
                qty_description = "{:.2f}".format(item.quantity)

            indentation = " " * (item.level + 1)
            description = f"{indentation}{product_description}"
            item_products.append(
                {
                    "ITEM_PRODUCT_QUANTITY": qty_description,
                    "ITEM_PRODUCT_NAME": description,
                    "ITEM_SERVICE_PERIOD": item_service_period,
                    "ITEM_PRODUCT_TOTAL_PRICE": item_price,
                }
            )

        return item_products

    @staticmethod
    def _get_fraction_sale_line_identifiers(
        sales_line_list: List[SaleLineApi],
    ) -> List[str]:

        fraction_sale_lines_list = []
        for sale_line in sales_line_list:
            if sale_line.fraction_parent:
                order_id = sale_line.get_custom_property(key="orderId")
                line_number_identifier = f"{order_id}-{sale_line.line_number}"
                fraction_sale_lines_list.append(line_number_identifier)

        return fraction_sale_lines_list

    def _get_grouped_sale_lines(
        self,
        table_orders: List[OrderApi],
    ) -> Dict[Union[Union[int, str], Any], List[SaleLineApi]]:

        grouped_sale_lines = {}
        for order in table_orders:
            for sale_line in order.sale_lines:
                if self._is_invalid_sale_line(sale_line=sale_line):
                    continue

                seat = self.get_table_seat(
                   sale_lines=order.sale_lines,
                   sale_line=sale_line,
                )
                if not seat:
                    continue

                if seat not in grouped_sale_lines:
                    grouped_sale_lines[seat] = [sale_line]
                    continue

                grouped_sale_lines[seat].append(sale_line)

        return grouped_sale_lines

    def get_table_seat(
        self,
        sale_lines: List[SaleLineApi],
        sale_line: SaleLineApi,
    ) -> str:

        seat = sale_line.seat
        if sale_line.level != 0:
            seat = self._get_parent_table_seat(
                sale_lines=sale_lines,
                line_number=sale_line.line_number,
            )

        return "$TABLE" if seat == 0 else seat

    @staticmethod
    def _get_parent_table_seat(
        sale_lines: List[SaleLineApi],
        line_number: int,
    ) -> int:
        seat = [x.seat for x in sale_lines if x.level == 0 and x.line_number == line_number]
        return seat[0] if len(seat) > 0 else 0

    @staticmethod
    def _parse_discounts_group(
        discount_groups: Dict[str, float],
    ) -> List[Dict[str, str]]:

        total_discounts = 0
        discount_field: List[Dict[str, str]] = []
        for discount in discount_groups:
            value = float(discount_groups.get(discount))
            if value == 0:
                continue

            discount_field.append({
                "DISCOUNT_LABEL": discount,
                "DISCOUNT_VALUE": -value,
            })
            total_discounts += value

        discount_field.append({
            "DISCOUNT_LABEL": "$TOTAL",
            "DISCOUNT_VALUE": -total_discounts,
        })

        return discount_field

    @staticmethod
    def _parse_partial_payments(
        service_tenders: List[ServiceTender],
    ) -> List[Dict[str, str]]:

        payments_field: List[Dict[str, str]] = []
        for tender in service_tenders:
            payments_field.append({
                "PAYMENT_LABEL": tender.description,
                "PAYMENT_VALUE": tender.amount,
            })

        total_paid = sum([float(tender.amount) for tender in service_tenders])
        payments_field.append({
            "PAYMENT_LABEL": "$PAID",
            "PAYMENT_VALUE": total_paid,
        })

        return payments_field

    @staticmethod
    def _is_invalid_sale_line(
        sale_line: SaleLineApi,
    ) -> bool:

        item_type = sale_line.part.item_type
        return item_type == "COUPON" or (item_type == "COMBO" and sale_line.quantity == 0)
