# -*- encoding: utf-8 -*-
import json
from abc import abstractmethod, ABCMeta
from base64 import b64encode, b64decode

from xml.dom.minidom import parse
from xml.etree import ElementTree

import xmltodict
from lxml import etree
from lxml.etree import XSLTParseError

from Crypto.Signature import PKCS1_v1_5
from Crypto.PublicKey import RSA

import os
import logging


class AbstractGeneratorXml(object):
    __metaclass__ = ABCMeta

    def __init__(self, data):
        self.data = data
        self.dir_xml = os.path.dirname(os.path.abspath(__file__))
        self.dom = parse(self.dir_xml + '/comprobante.xml')
        self.algoritmoSha = None
        if data:
            self.tipo_comprobante = self.data.get('tipo_comprobante') # I, E, P, T
            self.complemento = self.data.get('complemento', '') # Pago, CartaPorte
            self.tipo_transporte = self.data.get('tipo_transporte', '')
        self.version = ''
        self.__cadena_original = ''
        self.cadena_original_value = ''

    @abstractmethod
    def generate_xml(self):
        pass

    def create_xml(self, certificado_file_cer64, certificado_file_pem64):
        self.generate_xml()
        self.set_certificado(certificado_file_cer64)
        self.set_sello(certificado_file_pem64)

    def get_version(self):
        """
        Obtiene la version del xml
        :return:
        """
        return self.data.get('version')

    def reset_dom(self):
        """
        Resetear el dom a 0 elementos
        :return:
        """
        self.dom = parse(self.dir_xml + '/comprobante.xml')

    def get_xml(self):
        """
        Retorna el xml en utf8
        :return:
        """
        return self.dom.toxml(encoding='UTF-8')

    def get_xml_str(self):
        """
        Retorna el xml en formato string en utf8
        :return:
        """
        return self.dom.toxml(encoding='UTF-8').decode('utf8')

    def set_certificadoMF(self, certificado64):
        """
        Recibe el certificado abierto como archivo
        :param certificado_file:
        :return:
        """
        try:
            logging.info('OBTENIENDO EL CERTIFICADO DEL XML')
            logging.info('CERTIFICADO DEL XML DEL INGRESO_ID')
            certificadoA = [certificado64[i:i + 76] for i in range(0, len(certificado64), 76)]
            certificadoF = ''
            for val in certificadoA:
                if certificadoF != '':
                    certificadoF += ' '
                certificadoF += val.decode("utf-8")
            self.dom.setAttribute('Certificado', certificadoF)
            logging.info('SETEO DEL CERTIFICADO EN EL XML')
        except Exception as e:
            logging.error('Error al generar el certificado. Error: ' + str(e))
            raise Exception(e)

    def set_certificadoFLO(self, certificado64):
        self.dom.setAttribute('Certificado', certificado64)

    def set_sello(self, certificado_file_pem64):
        """
        Genera el el sello y lo agrega al xml
        :param certificado_file_pem64:
        :return:
        """
        try:
            logging.info('OBTENIENDO EL SELLO DEL XML')
            certificado_file_pem_bytes = b64decode(certificado_file_pem64)
            key = RSA.importKey(certificado_file_pem_bytes)
            logging.info('OBTENIENDO LA CADENA ORIGINAL DEL XML')
            self.cadena_original_value = self.get_cadena_original(self.get_xml())
            logging.info('LA CADENA ORIGINAL DEL XML')
            digest = self.algoritmoSha.new()
            digest.update(bytes(self.cadena_original_value))
            signer = PKCS1_v1_5.new(key)
            sign = signer.sign(digest)
            sello = b64encode(sign)
            self.dom.setAttribute('Sello', sello.decode("utf-8"))
        except Exception as e:
            logging.error('Error generar el sello. Error: ' + str(e))
            raise Exception(e)

    def get_cadena_original(self, xml):
        """
        Genera la cadena original y la retorna
        :param xml:
        :return: cadena_original
        """
        try:
            if type(xml) == str:
                xml = xml.encode('utf-8')
            xml = etree.fromstring(xml)
            logging.info('PARSE DE LA CADENA ORIGINAL DEL XML')
            logging.info(self.dir_xml)
            if self.data.get('version') == '3.3':
                xslt = etree.parse(self.dir_xml + '/cadenaoriginal_3_3_local/cadenaoriginal_3_3.xslt')
            if self.data.get('version') == '4.0':
                xslt = etree.parse(self.dir_xml + '/cadenaoriginal_3_3_local/cadenaoriginal_4_0.xslt')
            logging.info('XSLT CADENA ORIGINAL DEL XML')
            parser = etree.XSLT(xslt)
            logging.info('XSLT CADENA ORIGINAL DEL XML 2')
            cadena_original = parser(xml)
            logging.info('XSLT CADENA ORIGINAL DEL XML 3')
            return cadena_original
        except XSLTParseError as e:
            logging.error(u"Hubo un error al generar la cadena original, por favor intente mas tarde: " + str(e))
            raise Exception(u"Hubo un error al generar la cadena original, por favor intente mas tarde: " + str(e))
        except Exception as e:
            logging.error('Error al leer el archivo: cadenaoriginal_' + self.data.get('version') + '.xslt:' + str(e))
            raise Exception(e)

    @property
    def cadena_original(self) -> str:
        """
        Retorna la cadena original en string
        :return: cadena_original_value
        """
        return self.cadena_original_value

    def get_xml_data(self, xml):
        """
        Recibe xml en formato str y retorna un json del xml
        :param xml:
        :return: xml_json
        """
        obj = xmltodict.parse(xml)
        return json.dumps(obj)

    def get_datos(self, xml):
        xml_info = self.get_xml_info(xml)
        return {
            'cadena_original': str(self.cadena_original),
            'serie': xml_info['documento'].attrib['Serie'],
            'fecha_emision': xml_info['documento'].attrib['Fecha'],
            'no_certificado': xml_info['documento'].attrib['NoCertificado'],
            'folio': xml_info['documento'].attrib['Folio'],
            'uuid': xml_info['complemento'].attrib['UUID'],
            'fecha_timbrado': xml_info['complemento'].attrib['FechaTimbrado'],
            'sello_emisor': xml_info['complemento'].attrib['SelloCFD'],
            'sello_sat': xml_info['complemento'].attrib['SelloSAT'],
            'no_certificado_sat': xml_info['complemento'].attrib['NoCertificadoSAT'],
        }

    def get_xml_info(self, xml):
        if type(xml) == str:
            xml = xml.encode('utf-8')
        root = ElementTree.fromstring(xml)
        xmlinfo = {}
        xmlinfo['xml'] = xml
        xmlinfo['documento'] = root
        indice = 0
        for doc in root:
            if doc.tag == '{http://www.sat.gob.mx/cfd/3}Complemento':
                break
            elif doc.tag == '{http://www.sat.gob.mx/cfd/4}Complemento':
                break
            else:
                indice += 1
        for complemento_data in root[indice]:
            xmlinfo['complemento'] = complemento_data
        return xmlinfo

    def get_data_search(self):
        xml = self.dom.toxml(encoding='UTF-8')
        xml_info = self.get_xml_info(xml)
        return {
            'serie':xml_info[ 'documento' ].attrib[ 'Serie' ],
            'folio':xml_info[ 'documento' ].attrib[ 'Folio' ],
            'fecha':xml_info[ 'documento' ].attrib[ 'Fecha' ]
        }
