import re
from datetime import datetime
from typing import (
    Any,
    Dict,
    Optional,
    Callable,
)
from xml.etree.ElementTree import Element

from nreports.builders import DataBuilder
from nreports.model import SaleTypeId
from nreports_api.shared import JSON


class BaseLoyaltyCoupon(DataBuilder):

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

        loyalty_text_pre = self.constants.get("LOYALTY_TEXT_PRE")
        loyalty_qr_code = self.constants.get("LOYALTY_QR_CODE")
        loyalty_text_pos = self.constants.get("LOYALTY_TEXT_POS")

        loyalty_coupon_before_danfe = self.constants.get("LOYALTY_COUPON_DANFE_POSITION", "before").lower() == "before"
        data_to_build["LOYALTY_COUPON_BEFORE_DANFE"] = loyalty_coupon_before_danfe
        data_to_build["LOYALTY_COUPON_AFTER_DANFE"] = not loyalty_coupon_before_danfe

        order_xml = kwargs["orderPicture"]
        fiscal_xml = kwargs["fiscalXml"]

        has_loyalty_info = loyalty_text_pre is not None or loyalty_qr_code is not None or loyalty_text_pos is not None
        should_print_delivery_sale_type = self._should_print_delivery_sale_type(order_xml=order_xml)
        if not has_loyalty_info or not should_print_delivery_sale_type:
            data_to_build["PRINT_LOYALTY_COUPON"] = False
            return data_to_build

        data_to_build["PRINT_LOYALTY_COUPON"] = True
        has_pre = loyalty_text_pre is not None
        data_to_build["HAS_LOYALTY_PRE"] = has_pre
        if has_pre:
            data_to_build["LOYALTY_TEXT_PRE"] = []
            for line in loyalty_text_pre.split("{LINE_BREAK}"):
                data_to_build["LOYALTY_TEXT_PRE"].append(
                    {
                        "PRE_LINE": self._replace_variables(
                            template=line,
                            order_xml=order_xml,
                            fiscal_xml=fiscal_xml,
                        )
                    }
                )

        has_qr = loyalty_qr_code is not None
        data_to_build["HAS_LOYALTY_QRCODE"] = has_qr
        if has_qr:
            data_to_build["LOYALTY_QRCODE"] = self._replace_variables(
                template=loyalty_qr_code,
                order_xml=order_xml,
                fiscal_xml=fiscal_xml,
            )

        has_pos = loyalty_text_pos is not None
        data_to_build["HAS_LOYALTY_POS"] = has_pos
        if has_pos:
            data_to_build["LOYALTY_TEXT_POS"] = []
            for line in loyalty_text_pos.split("{LINE_BREAK}"):
                data_to_build["LOYALTY_TEXT_POS"].append(
                    {
                        "POS_LINE": self._replace_variables(
                            template=line,
                            order_xml=order_xml,
                            fiscal_xml=fiscal_xml,
                        )
                    }
                )

        return data_to_build

    def get_fiscal_key(self, fiscal_xml: Element) -> str:
        raise NotImplementedError()

    def get_fiscal_date(self, fiscal_xml: Element) -> datetime:
        raise NotImplementedError()

    def _should_print_delivery_sale_type(
        self,
        order_xml: Element
    ) -> bool:

        if int(order_xml.get("saleType")) == SaleTypeId.DELIVERY.value:
            loyalty_coupon_for_delivery_order_config = self.constants.get("LOYALTY_COUPON_FOR_DELIVERY_ORDER", "false")
            loyalty_coupon_for_delivery_order = loyalty_coupon_for_delivery_order_config.lower() == "true"
            return loyalty_coupon_for_delivery_order

        return True

    def _replace_variables(
        self,
        template: str,
        order_xml: Element,
        fiscal_xml: Element,
    ):
        constant_variables = {
            "{STORE_CODE}": self._get_store_code,
            "{STORE_NAME}": self._get_store_name,
        }
        order_variables = {
            "{ORDER_ID}": self._get_order_id,
            "{ORDER_ID()}": self._get_order_id,
            "{ORDER_VALUE}": self._get_order_value,
            "{PAID_VALUE}": self._get_paid_value,
            "{POS_TYPE}": self._get_pos_type,
            "{SALE_TYPE}": self._get_sale_type,
        }
        fiscal_variables = {
            "{CHAVE_FISCAL}": self.get_fiscal_key,
            "{FISCAL_DATE_TIME}": self._get_formatted_fiscal_date,
            "{FISCAL_DATE_TIME()}": self._get_formatted_fiscal_date,
        }

        for key, func in constant_variables.items():
            template = template.replace(key, func())

        template = self.process_variable_with_xml(
            template=template,
            variables=order_variables,
            xml=order_xml,
        )
        template = self.process_variable_with_xml(
            template=template,
            variables=fiscal_variables,
            xml=fiscal_xml,
        )

        template = self._replace_custom_properties(
            order_xml=order_xml,
            template=template,
        )

        return template

    @staticmethod
    def process_variable_with_xml(
        template: str,
        variables: Dict[str, Callable],
        xml: Element,
    ):
        for key, func in variables.items():
            if "(" not in key:
                template = template.replace(key, func(xml))
                continue

            key_to_find = key[:key.index("(")]
            start_index = template.find(key_to_find)
            while start_index >= 0:
                end_index = template.find(")}", start_index)
                text_to_replace = template[start_index:end_index + 2]

                output_format = text_to_replace[text_to_replace.index("(") + 1:text_to_replace.index(")")]
                template = template.replace(text_to_replace, func(xml, output_format))

                start_index = template.find(key_to_find)

        return template

    def _replace_custom_properties(
        self,
        order_xml: Element,
        template: str,
    ):

        template_custom_properties = re.findall(r'{PROPERTY\(([^)]+)\)}', template)
        for template_custom_property in template_custom_properties:
            template = template.replace(
                f"{{PROPERTY({template_custom_property})}}",
                self._get_custom_property_value(order_xml, template_custom_property),
            )

        return template

    @staticmethod
    def _get_order_id(
        order_xml: Element,
        last_digits: Optional[str]=None
    ) -> str:
        if last_digits is None:
            return order_xml.attrib["orderId"]

        last_digits = int(last_digits)
        return order_xml.attrib["orderId"][-last_digits:].zfill(last_digits)

    def _get_store_code(self) -> str:
        return self.store_config_repository.get_store_id()

    def _get_store_name(self) -> str:
        return self.store_config_repository.get_store_name()

    @staticmethod
    def _get_order_value(order_xml: Element) -> str:
        return f"{float(order_xml.attrib['totalGross']):.2f}"

    @staticmethod
    def _get_paid_value(order_xml: Element) -> str:
        return f"{(float(order_xml.attrib['totalTender']) - float(order_xml.attrib['change'])):.2f}"

    @staticmethod
    def _get_pos_type(order_xml: Element) -> str:
        return order_xml.attrib["posType"]

    @staticmethod
    def _get_sale_type(order_xml: Element) -> str:
        return order_xml.attrib["saleTypeDescr"]

    @staticmethod
    def _get_custom_property_value(
        order_xml: Element,
        custom_property_key: str,
    ) -> str:

        custom_property = order_xml.findall(f".//OrderProperty[@key='{custom_property_key}']")
        if not custom_property:
            return ""

        return custom_property[0].attrib.get("value")

    def _get_formatted_fiscal_date(self, fiscal_xml, output_format=None):
        if output_format is None:
            output_format = "%d/%m/%Y %H:%M:%S"
        fiscal_date = self.get_fiscal_date(fiscal_xml=fiscal_xml)
        return fiscal_date.strftime(output_format)
