from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from typing import (
    List,
    Optional,
)
from xml.etree import ElementTree
from xml.etree.ElementTree import Element

import iso8601
from ._UpdatableEntity import UpdatableEntity

@dataclass
class Product(object):
    product_code: int
    cfop: str
    ncm: str
    cest: Optional[str]
    description: str
    measure_unit: str
    unit_value: float
    quantity: float
    total_value: float
    discount: float
    increase: float
    c_benef: Optional[str]

class SimpleTaxType(Enum):
    Normal = 1
    NT = 2

@dataclass
class SimpleTax:
    type: SimpleTaxType
    cst: int
    base_amount: float
    rate: float
    value: float

class IcmsType(Enum):
    Icms00 = 0
    Icms20 = 20
    Icms40 = 20
    Icms41 = 20
    Icms60 = 20

@dataclass
class Icms:
    type: IcmsType
    origin: str
    cst: int
    base_amount: float
    rate: float
    value: float
    fcp_rate: Optional[float] = None
    fcp_value: Optional[float] = None
    fcp_base_amount: Optional[float] = None
    base_reduction_rate: Optional[float] = None

@dataclass
class Taxes:
    icms: Icms
    pis: SimpleTax
    cofins: SimpleTax


class Det(object):
    def __init__(
        self,
        item_number: int,
        product: Product,
        taxes: Taxes,
    ):
        self.item_number = item_number
        self.product = product
        self.taxes = taxes

@dataclass
class PaymentCard:
    tp_integra: int

@dataclass
class Payment:
    t_pag: int
    v_pag: float
    x_pag: Optional[str] = None
    card: Optional[PaymentCard] = None

class FiscalData(UpdatableEntity):
    def __init__(
        self,
        fiscal_xml: str,
        cancel_xml: Optional[str] = None,
    ):
        self.fiscal_xml = fiscal_xml
        self.cancel_xml = cancel_xml
        self._parsed_xml = None
        self._dets = None
        self._pags = None

    def __getattribute__(self, __name):
        if __name in ("_parsed_xml", "fiscal_xml", "cancel_xml", "_dets", "_pags"):
            return super().__getattribute__(__name)

        if not hasattr(self, "_parsed_xml"):
            return super().__getattribute__(__name)

        if self._parsed_xml is None:
            xml_to_parse = self.fiscal_xml
            if xml_to_parse.strip().startswith("<NFe"):
                xml_to_parse = (
                    '<nfeProc xmlns="http://www.portalfiscal.inf.br/nfe" versao="4.00">'
                    f'{xml_to_parse}'
                    '</nfeProc>'
                )
            self._parsed_xml = ElementTree.XML(xml_to_parse)

        return super().__getattribute__(__name)

    @classmethod
    def create_from_dict(cls, record):
        if "fiscalXml" in record and record["fiscalXml"] is not None:
            return cls(
                fiscal_xml=record["fiscalXml"],
                cancel_xml=record.get("cancel_xml"),
            )

        return None

    def __id__(self):
        return super().__id__()

    def update(self, new_value):
        self.fiscal_xml = new_value.fiscal_xml
        self.cancel_xml = new_value.cancel_xml
        self._parsed_xml = None

    @property
    def fiscal_date(self) -> datetime:
        if self._parsed_xml.tag == "CFe":
            d_emi = self._parsed_xml.find("infCFe/ide/dEmi").text
            h_emi = self._parsed_xml.find("infCFe/ide/hEmi").text
            naive_date = datetime.strptime(f"{d_emi} {h_emi}", "%Y%m%d %H%M%S")
            return naive_date.replace(tzinfo=iso8601.UTC)
        else:
            dh_emi = _get_nfce_text(
                xml=self._parsed_xml,
                path="NFe/infNFe/ide/dhEmi",
            )
            return iso8601.parse_date(dh_emi)

    @property
    def fiscal_key(self) -> str:
        if self._parsed_xml.tag == "CFe":
            inf_cfe_node = self._parsed_xml.find("infCFe")
            return inf_cfe_node.attrib["Id"][3:]
        else:
            inf_nfe_node = _get_nfce_node(
                xml=self._parsed_xml,
                path="NFe/infNFe",
            )
            return inf_nfe_node.attrib["Id"][3:]

    @property
    def fiscal_number(self) -> int:
        if self._parsed_xml.tag == "CFe":
            return int(self._parsed_xml.find("infCFe/ide/cNF").text)
        else:
            return int(
                _get_nfce_text(
                    xml=self._parsed_xml,
                    path="NFe/infNFe/ide/nNF",
                )
            )

    @property
    def fiscal_series(self) -> int:
        if self._parsed_xml.tag == "CFe":
            raise RuntimeError("CFe does not have fiscal series")
        else:
            return int(
                _get_nfce_text(
                    xml=self._parsed_xml,
                    path="NFe/infNFe/ide/serie",
                )
            )

    @property
    def protocol_number(self) -> str:
        if self._parsed_xml.tag == "CFe":
            raise RuntimeError("CFe does not have protocol number")
        else:
            return _get_nfce_text(
                xml=self._parsed_xml,
                path="protNFe/infProt/nProt",
            )


    @property
    def v_prod(self) -> float:
        if self._parsed_xml.tag == "CFe":
            return float(self._parsed_xml.find("infCFe/total/ICMSTot/vProd").text)
        else:
            return float(
                _get_nfce_text(
                    xml=self._parsed_xml,
                    path="NFe/infNFe/total/ICMSTot/vProd",
                )
            )

    @property
    def v_desc(self) -> float:
        if self._parsed_xml.tag == "CFe":
            return float(self._parsed_xml.find("infCFe/total/ICMSTot/vDesc").text)
        else:
            return float(
                _get_nfce_text(
                    xml=self._parsed_xml,
                    path="NFe/infNFe/total/ICMSTot/vDesc",
                )
            )

    @property
    def dest_doc(self) -> Optional[str]:
        if self._parsed_xml.tag == "CFe":
            cpf_node = self._parsed_xml.find("infCFe/dest/CPF")
            if cpf_node is not None:
                return cpf_node.text

            cnpj_node = self._parsed_xml.find("infCFe/dest/CNPJ")
            if cnpj_node is not None:
                return cnpj_node.text

            return None
        else:
            cpf_node = _get_nfce_node(
                xml=self._parsed_xml,
                path="NFe/infNFe/dest/CPF",
            )
            if cpf_node is not None:
                return cpf_node.text

            cnpj_node = _get_nfce_node(
                xml=self._parsed_xml,
                path="NFe/infNFe/dest/CNPJ",
            )
            if cnpj_node is not None:
                return cnpj_node.text

            return None

    @property
    def dets(self) -> List[Det]:
        if self._dets is None:
            self._dets = self._build_dets()

        return self._dets

    @property
    def pags(self) -> List[Payment]:
        if self._pags is None:
            self._pags = self._build_pags()

        return self._pags

    def _build_dets(self):
        if self._parsed_xml.tag == "CFe":
            ret = []
            for det_node in self._parsed_xml.findall("infCFe/det"):
                ret.append(
                    Det(
                        item_number=int(det_node.attrib["nItem"]),
                        product=Product(
                            product_code=int(det_node.find("prod/cProd").text),
                            cfop="",
                            ncm="",
                            cest="",
                            description=det_node.find("prod/xProd").text,
                            measure_unit=det_node.find("prod/uCom").text,
                            unit_value=float(det_node.find("prod/vUnCom").text),
                            quantity=float(det_node.find("prod/qCom").text),
                            total_value=0,
                            discount=0,
                            increase=0,
                            c_benef=None,
                        ),
                        taxes=Taxes(
                            icms=Icms(
                                type=IcmsType.Icms00,
                                origin="",
                                cst=0,
                                base_amount=0,
                                rate=0,
                                value=0,
                                fcp_rate=None,
                                fcp_value=None,
                                fcp_base_amount=None,
                                base_reduction_rate=None,
                            ),
                            pis=SimpleTax(
                                type=SimpleTaxType.Normal,
                                cst=0,
                                base_amount=0,
                                rate=0,
                                value=0
                            ),
                            cofins=SimpleTax(
                                type=SimpleTaxType.Normal,
                                cst=0,
                                base_amount=0,
                                rate=0,
                                value=0
                            )
                        )
                    )
                )

            return ret
        else:
            ret = []
            for det_node in _get_nfce_nodes(
                xml=self._parsed_xml,
                path="NFe/infNFe/det",
            ):
                ret.append(
                    Det(
                        item_number=int(det_node.attrib["nItem"]),
                        product=Product(
                            product_code=int(_get_nfce_node(xml=det_node, path="prod/cProd").text),
                            cfop=_get_nfce_text(xml=det_node, path="prod/CFOP"),
                            ncm=_get_nfce_text(xml=det_node, path="prod/NCM"),
                            cest=_get_optional_nfce_text(xml=det_node, path="prod/CEST"),
                            description=_get_nfce_node(xml=det_node, path="prod/xProd").text,
                            measure_unit=_get_nfce_node(xml=det_node, path="prod/uCom").text,
                            unit_value=float(_get_nfce_node(xml=det_node, path="prod/vUnCom").text),
                            quantity=float(_get_nfce_node(xml=det_node, path="prod/qCom").text),
                            total_value=float(_get_nfce_node(xml=det_node, path="prod/vProd").text),
                            discount=float(_get_optional_nfce_text(xml=det_node, path="prod/vDesc") or 0),
                            increase=0,
                            c_benef=_get_optional_nfce_text(xml=det_node, path="prod/cBenef"),
                        ),
                        taxes=Taxes(
                            icms=self._build_icms(icms_node=_get_nfce_node(xml=det_node, path="imposto/ICMS")),
                            pis=self._build_pis(pis_node=_get_nfce_node(xml=det_node, path="imposto/PIS")),
                            cofins=self._build_cofins(cofins_node=_get_nfce_node(xml=det_node, path="imposto/COFINS"))
                        )
                    )
                )

            return ret

    def _build_icms(self, icms_node: Element) -> Icms:
        inner_node = icms_node[0]

        origin = _get_nfce_text(xml=inner_node, path="orig")
        cst = int(_get_nfce_text(xml=inner_node, path="CST"))

        base_amount = 0
        rate = 0
        value = 0
        fcp_rate = None
        tcp_value = None
        fcp_base_amount = None
        base_reduction_rate = None
        icms_type = {
            _add_nfce_ns("ICMS00"): IcmsType.Icms00,
            _add_nfce_ns("ICMS20"): IcmsType.Icms20,
            _add_nfce_ns("ICMS40"): IcmsType.Icms40,
            _add_nfce_ns("ICMS41"): IcmsType.Icms41,
            _add_nfce_ns("ICMS60"): IcmsType.Icms60,
        }[inner_node.tag]

        if inner_node.tag in (_add_nfce_ns("ICMS00"), _add_nfce_ns("ICMS20")):
            base_amount = float(_get_nfce_text(xml=inner_node, path="vBC"))
            rate = float(_get_nfce_text(xml=inner_node, path="pICMS"))
            value = float(_get_nfce_text(xml=inner_node, path="vICMS"))
            fcp_rate_node = _get_nfce_node(xml=inner_node, path="pFCP")
            if fcp_rate_node is not None:
                fcp_rate = float(fcp_rate_node.text)
                tcp_value = float(_get_nfce_text(xml=inner_node, path="vFCP"))
                fcp_base_amount = base_amount

        if inner_node.tag == _add_nfce_ns("ICMS20"):
            base_reduction_rate = float(_get_nfce_text(xml=inner_node, path="pRedBC"))
            fcp_base_amount = float(_get_nfce_text(xml=inner_node, path="vBCFCP"))

        return Icms(
            type=icms_type,
            origin=origin,
            cst=cst,
            base_amount=base_amount,
            rate=rate,
            value=value,
            fcp_rate=fcp_rate,
            fcp_value=tcp_value,
            fcp_base_amount=fcp_base_amount,
            base_reduction_rate=base_reduction_rate,
        )

    def _build_pis(self, pis_node: Element) -> SimpleTax:
        return self._build_simple_tax(
            xml_node=pis_node,
            tax_name="PIS",
        )

    def _build_cofins(self, cofins_node: Element) -> SimpleTax:
        return self._build_simple_tax(
            xml_node=cofins_node,
            tax_name="COFINS",
        )

    def _build_simple_tax(
        self,
        xml_node: Element,
        tax_name: str,
    ) -> SimpleTax:
        inner_node = xml_node[0]

        tax_type = SimpleTaxType.NT
        rate = 0
        value = 0
        base_amount = 0
        if inner_node.tag == f"{'{http://www.portalfiscal.inf.br/nfe}'}{tax_name}Aliq":
            tax_type = SimpleTaxType.Normal
            base_amount = float(_get_nfce_text(xml=inner_node, path="vBC"))
            rate = float(_get_nfce_text(xml=inner_node, path=f"p{tax_name}"))
            value = float(_get_nfce_text(xml=inner_node, path=f"v{tax_name}"))

        return SimpleTax(
            type=tax_type,
            cst=int(_get_nfce_text(xml=inner_node, path="CST")),
            base_amount=base_amount,
            rate=rate,
            value=value,
        )

    def _build_pags(self):
        ret = []
        if self._parsed_xml.tag == "CFe":

            for mp_node in self._parsed_xml.findall("pgto/MP"):
                ret.append(
                    Payment(
                        t_pag=int(mp_node.find("cMP").text),
                        v_pag=float(mp_node.find("vMP").text),
                    )
                )
        else:
            for det_pag_node in _get_nfce_nodes(
                xml=self._parsed_xml,
                path="NFe/infNFe/pag/detPag",
            ):

                ret.append(
                    Payment(
                        t_pag=int(_get_nfce_text(xml=det_pag_node, path="tPag")),
                        v_pag=float(_get_nfce_text(xml=det_pag_node, path="vPag")),
                        x_pag=_get_optional_nfce_text(xml=det_pag_node, path="xPag"),
                        card=self._build_card(det_pag_node=det_pag_node),
                    )
                )

        return ret

    def _build_card(self, det_pag_node):
        card_node = _get_nfce_node(xml=det_pag_node, path="card")
        if card_node is None:
            return None

        return PaymentCard(
            tp_integra=int(_get_nfce_text(xml=card_node, path="tpIntegra")),
        )


def _add_nfce_ns(value: str) -> str:
    return f"{'{http://www.portalfiscal.inf.br/nfe}'}{value}"

def _get_path_with_ns(path: str) -> str:
    parts = path.split("/")
    full_path = ""
    for part in parts:
        full_path += f"{_add_nfce_ns(part)}/"

    return full_path[:-1]

def _get_nfce_node(xml: ElementTree, path: str) -> Element:
    return xml.find(_get_path_with_ns(path))


def _get_nfce_nodes(xml: ElementTree, path: str) -> Element:
    return xml.findall(_get_path_with_ns(path))

def _get_nfce_text(xml: ElementTree, path: str) -> str:
    return _get_nfce_node(
        xml=xml,
        path=path,
    ).text

def _get_optional_nfce_text(xml: ElementTree, path: str) -> str:
    node = _get_nfce_node(
        xml=xml,
        path=path,
    )
    if node is None:
        return None

    return node.text
