#!/usr/bin/env python

import datetime
from xml.etree import ElementTree as ET
from xml.dom.minidom import parseString

from logbook import Logger

#~ from settings import DEBUG


log = Logger('XML')
CFDI_ACTUAL = 'cfdi33'
NOMINA_ACTUAL = 'nomina12'

SAT = {
    'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
    'cfdi32': {
        'version': '3.2',
        'prefix': 'cfdi',
        'xmlns': 'http://www.sat.gob.mx/cfd/3',
        'schema': 'http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv32.xsd',
    },
    'cfdi33': {
        'version': '3.3',
        'prefix': 'cfdi',
        'xmlns': 'http://www.sat.gob.mx/cfd/3',
        'schema': 'http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv33.xsd',
    },
    'nomina11': {
        'version': '1.1',
        'prefix': 'nomina',
        'xmlns': 'http://www.sat.gob.mx/nomina',
        'schema': 'http://www.sat.gob.mx/nomina http://www.sat.gob.mx/sitio_internet/cfd/nomina/nomina11.xsd',
    },
    'nomina12': {
        'version': '1.2',
        'prefix': 'nomina',
        'xmlns': 'http://www.sat.gob.mx/nomina12',
        'schema': 'http://www.sat.gob.mx/nomina12 http://www.sat.gob.mx/sitio_internet/cfd/nomina/nomina12.xsd',
    },
    'locales': {
        'version': '1.0',
        'prefix': 'implocal',
        'xmlns': 'http://www.sat.gob.mx/implocal',
        'schema': ' http://www.sat.gob.mx/implocal http://www.sat.gob.mx/sitio_internet/cfd/implocal/implocal.xsd',
    },
    'donativo': {
        'version': '1.1',
        'prefix': 'donat',
        'xmlns': 'http://www.sat.gob.mx/donat',
        'schema': ' http://www.sat.gob.mx/donat http://www.sat.gob.mx/sitio_internet/cfd/donat/donat11.xsd',
        'leyenda': 'Este comprobante ampara un donativo, el cual será destinado por la donataria a los fines propios de su objeto social. En el caso de que los bienes donados hayan sido deducidos previamente para los efectos del impuesto sobre la renta, este donativo no es deducible. La reproducción no autorizada de este comprobante constituye un delito en los términos de las disposiciones fiscales.',
    },
    'ine': {
        'version': '1.1',
        'prefix': 'ine',
        'xmlns': 'http://www.sat.gob.mx/ine',
        'schema': ' http://www.sat.gob.mx/ine http://www.sat.gob.mx/sitio_internet/cfd/ine/ine11.xsd',
    },
}


class CFDI(object):

    def __init__(self, version=CFDI_ACTUAL):
        self._sat_cfdi = SAT[version]
        self._xsi = SAT['xsi']
        self._pre = self._sat_cfdi['prefix']
        self._cfdi = None
        self._complemento = None
        self._impuestos_locales = False
        self._donativo = False
        self._ine = False
        self.error = ''

    def _now(self):
        return datetime.datetime.now().isoformat()[:19]

    def get_xml(self, datos):
        if not self._validate(datos):
            return ''

        self._comprobante(datos['comprobante'])
        self._relacionados(datos['relacionados'])
        self._emisor(datos['emisor'])
        self._receptor(datos['receptor'])
        self._conceptos(datos['conceptos'])
        self._impuestos(datos['impuestos'])
        self._locales(datos['impuestos'])
        self._donatarias(datos['donativo'])
        self._complementos(datos['complementos'])

        if 'nomina' in datos:
            self._nomina(datos['nomina'])
        #~ if 'complementos' in datos:
            #~ self._complementos(datos['complementos'])

        return self._to_pretty_xml(ET.tostring(self._cfdi, encoding='utf-8'))

    def add_sello(self, sello):
        self._cfdi.attrib['Sello'] = sello
        return self._to_pretty_xml(ET.tostring(self._cfdi, encoding='utf-8'))

    def _to_pretty_xml(self, source):
        tree = parseString(source)
        xml = tree.toprettyxml(encoding='utf-8').decode('utf-8')
        return xml

    def _validate(self, datos):
        if datos['impuestos']['total_locales_trasladados'] or \
            datos['impuestos']['total_locales_retenciones']:
            self._impuestos_locales = True

        if datos['donativo']:
            self._donativo = True

        if 'ine' in datos['complementos']:
            self._ine = True

        if 'nomina' in datos:
            return self._validate_nomina(datos)
        return True

    def _validate_nomina(self, datos):
        comprobante = datos['comprobante']

        validators = (
            ('MetodoDePago', 'NA'),
            ('TipoCambio', '1'),
            ('Moneda', 'MXN'),
            ('TipoDeComprobante', 'egreso'),
        )
        for f, v in validators:
            if f in comprobante:
                if v != comprobante[f]:
                    msg = 'El atributo: {}, debe ser: {}'.format(f, v)
                    self.error = msg
                    return False
        return True

    def _comprobante(self, datos):
        attributes = {}
        attributes['xmlns:{}'.format(self._pre)] = self._sat_cfdi['xmlns']
        attributes['xmlns:xsi'] = self._xsi

        schema_locales = ''
        if self._impuestos_locales:
            name = 'xmlns:{}'.format(SAT['locales']['prefix'])
            attributes[name] = SAT['locales']['xmlns']
            schema_locales = SAT['locales']['schema']

        schema_donativo = ''
        if self._donativo:
            name = 'xmlns:{}'.format(SAT['donativo']['prefix'])
            attributes[name] = SAT['donativo']['xmlns']
            schema_donativo = SAT['donativo']['schema']

        schema_ine = ''
        if self._ine:
            name = 'xmlns:{}'.format(SAT['ine']['prefix'])
            attributes[name] = SAT['ine']['xmlns']
            schema_donativo = SAT['ine']['schema']

        attributes['xsi:schemaLocation'] = self._sat_cfdi['schema'] + \
            schema_locales + schema_donativo +schema_ine
        attributes.update(datos)

        if not 'Version' in attributes:
            attributes['Version'] = self._sat_cfdi['version']
        if not 'Fecha' in attributes:
            attributes['Fecha'] = self._now()

        self._cfdi = ET.Element('{}:Comprobante'.format(self._pre), attributes)
        return

    def _relacionados(self, datos):
        if not datos or not datos['tipo'] or not datos['cfdis']:
            return

        node_name = '{}:CfdiRelacionados'.format(self._pre)
        value = {'TipoRelacion': datos['tipo']}
        node = ET.SubElement(self._cfdi, node_name, value)
        for uuid in datos['cfdis']:
            node_name = '{}:CfdiRelacionado'.format(self._pre)
            value = {'UUID': uuid}
            ET.SubElement(node, node_name, value)
        return

    def _emisor(self, datos):
        node_name = '{}:Emisor'.format(self._pre)
        emisor = ET.SubElement(self._cfdi, node_name, datos)
        return

    def _receptor(self, datos):
        node_name = '{}:Receptor'.format(self._pre)
        emisor = ET.SubElement(self._cfdi, node_name, datos)
        return

    def _conceptos(self, datos):
        conceptos = ET.SubElement(self._cfdi, '{}:Conceptos'.format(self._pre))
        for row in datos:
            complemento = {}
            if 'complemento' in row:
                complemento = row.pop('complemento')

            taxes = {}
            if 'impuestos' in row:
                taxes = row.pop('impuestos')
            node_name = '{}:Concepto'.format(self._pre)
            concepto = ET.SubElement(conceptos, node_name, row)

            if taxes:
                node_name = '{}:Impuestos'.format(self._pre)
                impuestos = ET.SubElement(concepto, node_name)
                if 'traslados' in taxes and taxes['traslados']:
                    node_name = '{}:Traslados'.format(self._pre)
                    traslados = ET.SubElement(impuestos, node_name)
                    for traslado in taxes['traslados']:
                        ET.SubElement(
                            traslados, '{}:Traslado'.format(self._pre), traslado)
                if 'retenciones' in taxes and taxes['retenciones']:
                    node_name = '{}:Retenciones'.format(self._pre)
                    retenciones = ET.SubElement(impuestos, node_name)
                    for retencion in taxes['retenciones']:
                        ET.SubElement(
                            retenciones, '{}:Retencion'.format(self._pre), retencion)

            if 'InformacionAduanera' in row:
                for field in fields:
                    if field in row['InformacionAduanera']:
                        attributes[field] = row['InformacionAduanera'][field]
                if attributes:
                    node_name = '{}:InformacionAduanera'.format(self._pre)
                    ET.SubElement(concepto, node_name, attributes)

            if 'CuentaPredial' in row:
                attributes = {'numero': row['CuentaPredial']}
                node_name = '{}:CuentaPredial'.format(self._pre)
                ET.SubElement(concepto, node_name, attributes)

            if 'autRVOE' in row:
                fields = (
                    'version',
                    'nombreAlumno',
                    'CURP',
                    'nivelEducativo',
                    'autRVOE',
                )
                for field in fields:
                    if field in row['autRVOE']:
                        attributes[field] = row['autRVOE'][field]
                node_name = '{}:ComplementoConcepto'.format(self._pre)
                complemento = ET.SubElement(concepto, node_name)
                ET.SubElement(complemento, 'iedu:instEducativas', attributes)
        return

    def _impuestos(self, datos):
        if not datos:
            node_name = '{}:Impuestos'.format(self._pre)
            ET.SubElement(self._cfdi, node_name)
            return

        attributes = {}
        fields = ('TotalImpuestosTrasladados', 'TotalImpuestosRetenidos')
        for field in fields:
            if field in datos:
                attributes[field] = datos[field]
        node_name = '{}:Impuestos'.format(self._pre)
        impuestos = ET.SubElement(self._cfdi, node_name, attributes)

        if 'retenciones' in datos and datos['retenciones']:
            retenciones = ET.SubElement(impuestos, '{}:Retenciones'.format(self._pre))
            for row in datos['retenciones']:
                ET.SubElement(retenciones, '{}:Retencion'.format(self._pre), row)

        if 'traslados' in datos and datos['traslados']:
            traslados = ET.SubElement(impuestos, '{}:Traslados'.format(self._pre))
            for row in datos['traslados']:
                ET.SubElement(traslados, '{}:Traslado'.format(self._pre), row)
        return

    def _nomina(self, datos):
        sat_nomina = SAT[NOMINA_ACTUAL]
        pre = sat_nomina['prefix']
        complemento = ET.SubElement(self._cfdi, '{}:Complemento'.format(self._pre))

        emisor = datos.pop('Emisor', None)
        receptor = datos.pop('Receptor', None)
        percepciones = datos.pop('Percepciones', None)
        deducciones = datos.pop('Deducciones', None)

        attributes = {}
        attributes['xmlns:{}'.format(pre)] = sat_nomina['xmlns']
        attributes['xsi:schemaLocation'] = sat_nomina['schema']
        attributes.update(datos)

        if not 'Version' in attributes:
            attributes['Version'] = sat_nomina['version']

        nomina = ET.SubElement(complemento, '{}:Nomina'.format(pre), attributes)
        if emisor:
            ET.SubElement(nomina, '{}:Emisor'.format(pre), emisor)
        if receptor:
            ET.SubElement(nomina, '{}:Receptor'.format(pre), receptor)
        if percepciones:
            detalle = percepciones.pop('detalle', None)
            percepciones = ET.SubElement(nomina, '{}:Percepciones'.format(pre), percepciones)
            for row in detalle:
                ET.SubElement(percepciones, '{}:Percepcion'.format(pre), row)
        if deducciones:
            detalle = deducciones.pop('detalle', None)
            deducciones = ET.SubElement(nomina, '{}:Deducciones'.format(pre), deducciones)
            for row in detalle:
                ET.SubElement(deducciones, '{}:Deduccion'.format(pre), row)
        return

    def _locales(self, datos):
        if not self._impuestos_locales:
            return

        if self._complemento is None:
            self._complemento = ET.SubElement(
                self._cfdi, '{}:Complemento'.format(self._pre))

        attributes = {}
        attributes['version'] = SAT['locales']['version']
        attributes['TotaldeTraslados'] = datos['total_locales_trasladados']
        attributes['TotaldeRetenciones'] = datos['total_locales_retenciones']

        node = ET.SubElement(
            self._complemento, 'implocal:ImpuestosLocales', attributes)

        for retencion in datos['locales_retenciones']:
            ET.SubElement(node, 'implocal:RetencionesLocales', retencion)
        for traslado in datos['locales_trasladados']:
            ET.SubElement(node, 'implocal:TrasladosLocales', traslado)
        return

    def _donatarias(self, datos):
        if not datos:
            return

        if self._complemento is None:
            self._complemento = ET.SubElement(
                self._cfdi, '{}:Complemento'.format(self._pre))

        attributes = {}
        attributes['version'] = SAT['donativo']['version']
        attributes['leyenda'] = SAT['donativo']['leyenda']
        attributes.update(datos)

        node = ET.SubElement(self._complemento, 'donat:Donatarias', attributes)

        return

    def _complementos(self, datos):
        if not datos:
            return

        if self._complemento is None:
            self._complemento = ET.SubElement(
                self._cfdi, '{}:Complemento'.format(self._pre))

        if 'ine' in datos:
            atributos = {'Version': SAT['ine']['version']}
            atributos.update(datos['ine'])
            ET.SubElement(self._complemento, 'ine:INE', atributos)

        if 'ce' in datos:
            pre = 'cce11'
            datos = datos.pop('ce')
            emisor = datos.pop('emisor')
            propietario = datos.pop('propietario')
            receptor = datos.pop('receptor')
            destinatario = datos.pop('destinatario')
            conceptos = datos.pop('conceptos')

            attributes = {}
            attributes['xmlns:{}'.format(pre)] = \
                'http://www.sat.gob.mx/ComercioExterior11'
            attributes['xsi:schemaLocation'] = \
                'http://www.sat.gob.mx/ComercioExterior11 ' \
                'http://www.sat.gob.mx/sitio_internet/cfd/ComercioExterior11/ComercioExterior11.xsd'
            attributes.update(datos)
            ce = ET.SubElement(
                complemento, '{}:ComercioExterior'.format(pre), attributes)

            attributes = {}
            if 'Curp' in emisor:
                attributes = {'Curp': emisor.pop('Curp')}
            node = ET.SubElement(ce, '{}:Emisor'.format(pre), attributes)
            ET.SubElement(node, '{}:Domicilio'.format(pre), emisor)

            if propietario:
                ET.SubElement(ce, '{}:Propietario'.format(pre), propietario)

            attributes = {}
            if 'NumRegIdTrib' in receptor:
                attributes = {'NumRegIdTrib': receptor.pop('NumRegIdTrib')}
            node = ET.SubElement(ce, '{}:Receptor'.format(pre), attributes)
            ET.SubElement(node, '{}:Domicilio'.format(pre), receptor)

            attributes = {}
            if 'NumRegIdTrib' in destinatario:
                attributes = {'NumRegIdTrib': destinatario.pop('NumRegIdTrib')}
            if 'Nombre' in destinatario:
                attributes.update({'Nombre': destinatario.pop('Nombre')})
            node = ET.SubElement(ce, '{}:Destinatario'.format(pre), attributes)
            ET.SubElement(node, '{}:Domicilio'.format(pre), destinatario)

            node = ET.SubElement(ce, '{}:Mercancias'.format(pre))
            fields = ('Marca', 'Modelo', 'SubModelo', 'NumeroSerie')
            for row in conceptos:
                detalle = {}
                for f in fields:
                    if f in row:
                        detalle[f] = row.pop(f)
                concepto = ET.SubElement(node, '{}:Mercancia'.format(pre), row)
                if detalle:
                    ET.SubElement(
                        concepto, '{}:DescripcionesEspecificas'.format(pre), detalle)
        return
