# This file is part of facho.  The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.

import hashlib
from functools import reduce
import copy
from dataclasses import dataclass
from datetime import datetime

from .data import dian
from . import fe

@dataclass
class Party:
    name: str
    ident: str
    responsability_code: str
    organization_code: str

    phone: str = ''
    address: str = ''
    email: str = ''
    legal_name: str = ''
    legal_company_ident: str = ''
    legal_address: str = ''



@dataclass
class TaxSubTotal:
    percent: float
    tax_scheme_ident: str = '01'

    tax_amount: float = 0.0
    taxable_amount: float = 0.0

    def calculate(self, invline):
        self.tax_amount = invline.total_amount * (self.percent / 100)
        self.taxable_amount = invline.total_amount


@dataclass
class TaxTotal:
    subtotals: list
    tax_amount: float = 0.0
    taxable_amount: float = 0.0

    def calculate(self, invline):
        for subtax in self.subtotals:
            subtax.calculate(invline)
            self.tax_amount += subtax.tax_amount
            self.taxable_amount += subtax.taxable_amount
      

@dataclass
class InvoiceLine:
    quantity: int
    description: str
    item_ident: int
    price_amount: float
    tax: TaxTotal

    @property
    def total_amount(self):
        return self.quantity * self.price_amount

    @property
    def total_tax_inclusive_amount(self):
        return self.tax.taxable_amount

    @property
    def total_tax_exclusive_amount(self):
        return self.tax.tax_amount

    def calculate(self):
        self.tax.calculate(self)


@dataclass
class LegalMonetaryTotal:
    line_extension_amount: float = 0.0
    tax_exclusive_amount: float = 0.0
    tax_inclusive_amount: float = 0.0
    charge_total_amount: float = 0.0
    payable_amount: float = 0.0


class Invoice:
    def __init__(self):
        self.invoice_period_start = None
        self.invoice_period_end = None
        self.invoice_issue = None
        self.invoice_ident = None
        self.invoice_cufe = None
        self.invoice_legal_monetary_total = LegalMonetaryTotal(0, 0, 0, 0, 0)
        self.invoice_customer = None
        self.invoice_supplier = None
        self.invoice_lines = []
        
    def set_period(self, startdate, enddate):
        self.invoice_period_start = startdate
        self.invoice_period_end = enddate

    def set_issue(self, dtime: datetime):
        self.invoice_issue = dtime

    def set_ident(self, ident: str):
        self.invoice_ident = ident

    def set_supplier(self, party: Party):
        self.invoice_supplier = party

    def set_customer(self, party: Party):
        self.invoice_customer = party

    def add_invoice_line(self, line: InvoiceLine):
        self.invoice_lines.append(line)

    def validate(self, validator):
        validator.validate_customer(self.invoice_customer)
        validator.validate_supplier(self.invoice_supplier)
        for invline in self.invoice_lines:
            validator.validate_invoice_line(self, invline)

    def _calculate_legal_monetary_total(self):
        for invline in self.invoice_lines:
            self.invoice_legal_monetary_total.line_extension_amount += invline.total_amount
            self.invoice_legal_monetary_total.tax_exclusive_amount += invline.total_amount
            self.invoice_legal_monetary_total.charge_total_amount += invline.total_amount

        self.invoice_legal_monetary_total.payable_amount = self.invoice_legal_monetary_total.tax_exclusive_amount \
            + self.invoice_legal_monetary_total.line_extension_amount \
            + self.invoice_legal_monetary_total.tax_inclusive_amount

    def calculate(self):
        self._calculate_legal_monetary_total()
        for invline in self.invoice_lines:
            invline.calculate()


class DianResolucion0001Validator:

    def __init__(self):
        self.errors = []

    def _validate_party(self, party):
        try:
            dian.TipoResponsabilidad[party.responsability_code]
        except KeyError:
            self.errors.append(('responsability_code', 'not found'))

        try:
            dian.TipoOrganizacion[party.organization_code]
        except KeyError:
            self.errors.append(('organization_code', 'not found'))

    def validate_customer(self, customer):
        self._validate_party(customer)

    def validate_supplier(self, supplier):
        self._validate_party(supplier)

    def validate_invoice_line(self, invoice, line):
        pass

    def valid(self):
        return not self.errors

    
class DIANInvoiceXML(fe.FeXML):

    def __init__(self, invoice, TipoAmbiente = 'Pruebas'):
        super().__init__('Invoice', 'http://www.dian.gov.co/contratos/facturaelectronica/v1')
        self.attach_invoice(invoice, TipoAmbiente)
        
    def attach_invoice(self, invoice, TipoAmbiente):
        """adiciona etiquetas a FEXML y retorna FEXML
        en caso de fallar validacion retorna None"""
        fexml = self

        invoice.calculate()

        cufe = self._generate_cufe(invoice, TipoAmbiente)

        fexml.set_element('/fe:Invoice/cbc:ID', invoice.invoice_ident)
        fexml.set_element('/fe:Invoice/cbc:UUID[schemaName="CUFE-SHA384"]', cufe)
        fexml.set_element('/fe:Invoice/cbc:IssueDate', invoice.invoice_issue.strftime('%Y-%m-%d'))
        fexml.set_element('/fe:Invoice/cbc:IssueTime', invoice.invoice_issue.strftime('%H:%M:%S'))
        fexml.set_element('/fe:Invoice/cac:InvoicePeriod/cbc:StartDate', invoice.invoice_period_start.strftime('%Y-%m-%d'))
        fexml.set_element('/fe:Invoice/cac:InvoicePeriod/cbc:EndDate', invoice.invoice_period_end.strftime('%Y-%m-%d'))

        fexml.set_element('/fe:Invoice/cbc:LineCountNumeric', len(invoice.invoice_lines))

        fexml.set_element('/fe:Invoice/fe:AccountingSupplierParty/fe:Party/cac:PartyIdentification/cbc:ID',
                          invoice.invoice_supplier.ident)
        fexml.set_element('/fe:Invoice/fe:AccountingSupplierParty/fe:Party/fe:PartyTaxScheme/cbc:TaxLevelCode',
                          invoice.invoice_supplier.responsability_code)
        fexml.set_element('/fe:Invoice/fe:AccountingSupplierParty/cbc:AdditionalAccountID',
                          invoice.invoice_supplier.organization_code)
        fexml.set_element('/fe:Invoice/fe:AccountingSupplierParty/fe:Party/cac:PartyName/cbc:Name',
                          invoice.invoice_supplier.name)
        fexml.set_element('/fe:Invoice/fe:AccountingSupplierParty/fe:Party/fe:PartyLegalEntity/cbc:RegistrationName',
                          invoice.invoice_supplier.legal_name)
        fexml.set_element('/fe:Invoice/fe:AccountingSupplierParty/fe:Party/fe:PhysicalLocation/fe:Address/cac:AddressLine/cbc:Line',
                          invoice.invoice_supplier.address)
        fexml.set_element('/fe:Invoice/fe:AccountingCustomerParty/fe:Party/cac:PartyIdentification/cbc:ID',
                          invoice.invoice_customer.ident)
        fexml.set_element('/fe:Invoice/fe:AccountingCustomerParty/fe:Party/fe:PartyTaxScheme/cbc:TaxLevelCode',
                          invoice.invoice_customer.responsability_code)
        fexml.set_element('/fe:Invoice/fe:AccountingCustomerParty/cbc:AdditionalAccountID',
                          invoice.invoice_customer.organization_code)
        fexml.set_element('/fe:Invoice/fe:AccountingCustomerParty/fe:Party/cac:PartyName/cbc:Name',
                          invoice.invoice_customer.name)
        fexml.set_element('/fe:Invoice/fe:AccountingCustomerParty/fe:Party/fe:PartyLegalEntity/cbc:RegistrationName',
                          invoice.invoice_customer.legal_name)
        fexml.set_element('/fe:Invoice/fe:AccountingCustomerParty/fe:Party/fe:PhysicalLocation/fe:Address/cac:AddressLine/cbc:Line',
                          invoice.invoice_customer.address)

        fexml.set_element('/fe:Invoice/fe:LegalMonetaryTotal/cbc:LineExtensionAmount',
                          invoice.invoice_legal_monetary_total.line_extension_amount,
                          currencyID='COP')
        fexml.set_element('/fe:Invoice/fe:LegalMonetaryTotal/cbc:TaxExclusiveAmount',
                          invoice.invoice_legal_monetary_total.tax_exclusive_amount,
                          currencyID='COP')
        fexml.set_element('/fe:Invoice/fe:LegalMonetaryTotal/cbc:TaxInclusiveAmount',
                          invoice.invoice_legal_monetary_total.tax_inclusive_amount,
                          currencyID='COP')
        fexml.set_element('/fe:Invoice/fe:LegalMonetaryTotal/cbc:ChargeTotalAmount',
                          invoice.invoice_legal_monetary_total.charge_total_amount,
                          currencyID='COP')
        fexml.set_element('/fe:Invoice/fe:LegalMonetaryTotal/cbc:PayableAmount',
                          invoice.invoice_legal_monetary_total.payable_amount,
                          currencyID='COP')

        next_append = False
        for index, invoice_line in enumerate(invoice.invoice_lines):
            line = fexml.fragment('/fe:Invoice/fe:InvoiceLine', append=next_append)
            next_append = True

            line.set_element('/fe:InvoiceLine/cbc:ID', index + 1)
            line.set_element('/fe:InvoiceLine/cbc:InvoicedQuantity', invoice_line.quantity, unitCode = 'NAR')
            line.set_element('/fe:InvoiceLine/cbc:LineExtensionAmount', invoice_line.total_amount, currencyID="COP")
            line.set_element('/fe:InvoiceLine/fe:Price/cbc:PriceAmount', invoice_line.price_amount, currencyID="COP") 
            line.set_element('/fe:InvoiceLine/fe:Item/cbc:Description', invoice_line.description)

        return fexml


    def _generate_cufe(self, invoice, TipoAmbiente = 'Pruebas'):
        NumFac = invoice.invoice_ident
        FecFac = invoice.invoice_issue.strftime('%Y-%m-%d')
        HoraFac = invoice.invoice_issue.strftime('%H:%H:%S')
        ValorBruto = invoice.invoice_legal_monetary_total.line_extension_amount
        ValorTotalPagar = invoice.invoice_legal_monetary_total.payable_amount
        ValorImpuestoPara = {}
        ValorImpuesto1 = 0.0
        CodImpuesto1 = 1
        ValorImpuesto2 = 0.0
        CodImpuesto2 = 4
        ValorImpuesto3 = 0.0
        CodImpuesto3 = 3
        for invoice_line in invoice.invoice_lines:
            for subtotal in invoice_line.tax.subtotals:
                # TODO cual es la naturaleza de tax_scheme_ident?
                codigo_impuesto = int(subtotal.tax_scheme_ident)
                ValorImpuestoPara.setdefault(codigo_impuesto, 0.0)
                ValorImpuestoPara[codigo_impuesto] += subtotal.tax_amount

        NitOFE = invoice.invoice_supplier.ident
        NumAdq = invoice.invoice_customer.ident
        TipoAmb = int(dian.TipoAmbiente[TipoAmbiente]['code'])

        formatVars = {
            '%s': NumFac,
            '%s': FecFac,
            '%.02f': HoraFac,
            '%.02f': ValorBruto,
            '%.02f': ValorTotalPagar,
            '%.02f': ValorImpuestoPara.get(CodImpuesto1, 0.0),
            '%02d': CodImpuesto1,
            '%.02f': ValorImpuestoPara.get(CodImpuesto2, 0.0),
            '%02d': CodImpuesto2,
            '%.02f': ValorImpuestoPara.get(CodImpuesto3, 0.0),
            '%02d': CodImpuesto3,
            '%s': NitOFE,
            '%s': NumAdq,
            '%d': TipoAmb,
        }
        cufe = "".join(formatVars.keys()) % tuple(formatVars.values())

        # crear hash...
        h = hashlib.sha384()
        h.update(cufe.encode('utf-8'))
        return h.hexdigest()