# 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 collections import defaultdict
import decimal
from decimal import Decimal


from .data.dian import codelist

DECIMAL_PRECISION = 6

class AmountCurrencyError(TypeError):
    pass

@dataclass
class Currency:
    code: str

    def __eq__(self, other):
        return self.code == other.code

    def __str__(self):
        return self.code

class Collection:

    def __init__(self, array):
        self.array = array

    def filter(self, filterer):
        new_array = filter(filterer, self.array)
        return self.__class__(new_array)

    def map(self, mapper):
        new_array = map(mapper, self.array)
        return self.__class__(new_array)

    def sum(self):
        return sum(self.array)

class AmountCollection(Collection):

    def sum(self):
        total = Amount(0)
        for v in self.array:
            total += v
        return total

class Amount:
    def __init__(self, amount: int or float or Amount, currency: Currency = Currency('COP')):

        if isinstance(amount, Amount):
            self.amount = amount.amount
            self.currency = amount.currency
        else:
            self.amount = Decimal(amount, decimal.Context(prec=DECIMAL_PRECISION, rounding=decimal.ROUND_HALF_DOWN ))
            self.currency = currency

    def __round__(self, prec):
        return round(self.amount, prec)

    def __str__(self):
        return '%.06f' % self.amount

    def __eq__(self, other):
        if not self.is_same_currency(other):
            raise AmountCurrencyError()
        return round(self.amount, DECIMAL_PRECISION) == round(other.amount, DECIMAL_PRECISION)

    def __add__(self, other):
        if not self.is_same_currency(other):
            raise AmountCurrencyError()
        return Amount(self.amount + other.amount, self.currency)

    def __sub__(self, other):
        if not self.is_same_currency(other):
            raise AmountCurrencyError()
        return Amount(self.amount - other.amount, self.currency)

    def __mul__(self, other):
        if not self.is_same_currency(other):
            raise AmountCurrencyError()
        return Amount(self.amount * other.amount, self.currency)

    def is_same_currency(self, other):
        return self.currency == other.currency

@dataclass
class Item:
    description: str
    id: str


@dataclass
class StandardItem(Item):
    pass


@dataclass
class Country:
    code: str
    name: str

@dataclass
class CountrySubentity:
    code: str
    name: str

@dataclass
class City:
    code: str
    name: str

@dataclass
class Address:
    name: str
    street: str = ''
    city: City = City('', '')
    country: Country = Country('CO', 'Colombia')
    countrysubentity: CountrySubentity = CountrySubentity('', '')

@dataclass
class PartyIdentification:
    number: str
    dv: str
    type_fiscal: str

    def __str__(self):
        return self.number

    def __eq__(self, other):
        return str(self) == str(other)

    def full(self):
        return "%s%s" % [self.number, self.dv]

@dataclass
class Responsability:
    codes: list

    def __str__(self):
        return ';'.join(self.codes)

    def __eq__(self, other):
        return str(self) == str(other)

    def __iter__(self):
        return iter(self.codes)

@dataclass
class TaxScheme:
    code: str
    name: str = ''   

    
@dataclass
class Party:
    name: str
    ident: str
    responsability_code: str
    responsability_regime_code: str
    organization_code: str
    tax_scheme: TaxScheme = TaxScheme('')

    phone: str = ''
    address: Address = Address('')
    email: str = ''
    legal_name: str = ''
    legal_company_ident: str = ''
    legal_address: str = ''


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

    tax_amount: Amount = Amount(0.0)
    taxable_amount: Amount = Amount(0.0)

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


@dataclass
class TaxTotal:
    subtotals: list
    tax_amount: Amount = Amount(0.0)
    taxable_amount: Amount = Amount(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 Price:
    amount: Amount
    type_code: str
    type: str


@dataclass
class PaymentMean:
    DEBIT = '01'
    CREDIT = '02'

    def __init__(self, id: str, code: str, due_at: datetime, payment_id: str):
        self.id = id
        self.code = code
        self.due_at = due_at
        self.payment_id = payment_id


@dataclass
class PrePaidPayment:
    #DIAN 1.7.-2020: FBD03
    paid_amount: Amount = Amount(0.0)


@dataclass
class BillingReference:
        def __init__(self, ident: str, uuid: str, date: str):
            self.ident = ident
            self.uuid = uuid
            self.date = date
    
    
@dataclass
class InvoiceLine:
    # RESOLUCION 0004: pagina 155
    quantity: int
    description: str
    item: Item
    price: Price
    # TODO mover a Invoice
    # ya que al reportar los totales es sobre
    # la factura y el percent es unico por type_code
    # de subtotal
    tax: TaxTotal

    @property
    def total_amount(self):
        return Amount(self.quantity) * self.price.amount

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

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

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

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

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


@dataclass
class LegalMonetaryTotal:
    line_extension_amount: Amount = Amount(0.0)
    tax_exclusive_amount: Amount = Amount(0.0)
    tax_inclusive_amount: Amount = Amount(0.0)
    charge_total_amount: Amount = Amount(0.0)
    allowance_total_amount: Amount = Amount(0.0)
    payable_amount: Amount = Amount(0.0)
    prepaid_amount: Amount = Amount(0.0)

@dataclass
class AllowanceCharge:
    #DIAN 1.7.-2020: FAQ03
    charge_indicator: bool = True
    amount: Amount = Amount(0.0)

    def isCharge(self):
        return self.charge_indicator == True

    def isDiscount(self):
        return self.charge_indicator == False

    def asCharge(self):
        self.charge_indicator = True

    def asDiscount(self):
        self.charge_indicator = False



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_operation_type = None
        self.invoice_legal_monetary_total = LegalMonetaryTotal()
        self.invoice_customer = None
        self.invoice_supplier = None
        self.invoice_payment_mean = None
        self.invoice_payments = []
        self.invoice_lines = []
        self.invoice_allowance_charge = []
        self.invoice_prepaid_payment = []

    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 set_payment_mean(self, payment_mean: PaymentMean):
        self.invoice_payment_mean = payment_mean

    def set_operation_type(self, operation):
        self.invoice_operation_type = operation

    def add_allownace_charge(self, charge: AllowanceCharge):
        self.invoice_allowance_charge.append(charge)

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

    def add_prepaid_payment(self, paid: PrePaidPayment):
        self.invoice_prepaid_payment.append(paid)

    def set_billing_reference(self, billing_reference: BillingReference):
        self.invoice_billing_reference = billing_reference
        
    def accept(self, visitor):
        visitor.visit_payment_mean(self.invoice_payment_mean)
        visitor.visit_customer(self.invoice_customer)
        visitor.visit_supplier(self.invoice_supplier)
        for payment in self.invoice_payments:
            visitor.visit_payment(payment)
        for invline in self.invoice_lines:
            visitor.visit_invoice_line(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_tax_exclusive_amount
            #DIAN 1.7.-2020: FAU6
            self.invoice_legal_monetary_total.tax_inclusive_amount += invline.total_tax_inclusive_amount

        #DIAN 1.7.-2020: FAU08
        self.invoice_legal_monetary_total.allowance_total_amount = AmountCollection(self.invoice_allowance_charge)\
            .filter(lambda charge: charge.isDiscount())\
            .map(lambda charge: charge.amount)\
            .sum()

        #DIAN 1.7.-2020: FAU10
        self.invoice_legal_monetary_total.charge_total_amount = AmountCollection(self.invoice_allowance_charge)\
            .filter(lambda charge: charge.isCharge())\
            .map(lambda charge: charge.amount)\
            .sum()

        #DIAN 1.7.-2020: FAU12
        self.invoice_legal_monetary_total.prepaid_amount = AmountCollection(self.invoice_prepaid_payment)\
            .map(lambda paid: paid.paid_amount)\
            .sum()

        #DIAN 1.7.-2020: FAU14
        self.invoice_legal_monetary_total.payable_amount = \
            self.invoice_legal_monetary_total.tax_inclusive_amount \
            + self.invoice_legal_monetary_total.allowance_total_amount \
            + self.invoice_legal_monetary_total.charge_total_amount \
            - self.invoice_legal_monetary_total.prepaid_amount


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


class DianResolucion0001Validator:

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

    def _validate_party(self, model, party):
        for code in party.responsability_code:
            if code not in codelist.TipoResponsabilidad:
                self.errors.append((model,
                                    'responsability_code',
                                    'not found %s' % (code)))

        try:
            codelist.TipoOrganizacion[party.organization_code]
        except KeyError:
            self.errors.append((model, 'organization_code' ,
                                'not found %s' % (party.organization_code)))
        try:
            if isinstance(party.tax_scheme, (str, str)):
                codelist.TipoImpuesto[party.tax_scheme.code]
        except KeyError:
            self.errors.append((model , 'tax_scheme' ,
                                'not found %s' % (party.tax_scheme)))
        try:
            codelist.Departamento[party.address.countrysubentity.code]
        except KeyError:
            self.errors.append((model, 'countrysubentity_code',
                                'not found %s' % (party.address.countrysubentity.code)))
        try:
            codelist.Municipio[party.address.city.code]
        except KeyError:
            self.errors.append((model, 'city_code',
                                'not found %s' % (party.address.city.code)))

    def _validate_invoice(self, invoice):
        try:
            codelist.TipoOperacionF[invoice.invoice_operation_type]
        except KeyError:
            self.errors.append(('invoice', 'operation_type',
                                'not found %s' % (invoice.invoice_operation_type)))

        # MACHETE se espera en zona horario colombia
        if invoice.invoice_issue.tzname() not in ['UTC-05:00', '-05', None]:
            self.errors.append(('invoice', 'invoice_issue',
                                'expected timezone UTC-05:00 or -05 or empty got %s' % (invoice.invoice_issue.tzname())))

    def validate(self, invoice):
        invoice.accept(self)
        self._validate_invoice(invoice)

        return not self.errors

    def visit_payment_mean(self, mean):
        try:
            codelist.MediosPago[mean.code]
        except KeyError:
            self.errors.append(('payment_mean', 'code',
                                'not found %s' % (mean.code)))

    def visit_customer(self, customer):
        self._validate_party('customer', customer)

    def visit_supplier(self, supplier):
        self._validate_party('supplier', supplier)

    def visit_payment(self, payment):
        pass

    def visit_invoice_line(self, line):
        try:
            codelist.CodigoPrecioReferencia[line.price.type_code]
        except KeyError:
            self.errors.append(('invoice_line', 'line.price',
                               'not found %s' % (line.price.type_code)))

    def valid(self):
        return not self.errors