#!/usr/bin/env python3

import base64
import hashlib
import uuid
from datetime import datetime, timedelta

import httpx
import lxml.etree as ET

from conf import TIMEOUT


class SATWebService():
    BASE = 'https://cfdidescargamasivasolicitud.clouda.sat.gob.mx'
    URL = {
        'AUTH': f'{BASE}/Autenticacion/Autenticacion.svc',
        'REQ': f'{BASE}/SolicitaDescargaService.svc',
        'VSD': f'{BASE}/VerificaSolicitudDescargaService.svc',
        'DOWN': 'https://cfdidescargamasiva.clouda.sat.gob.mx/DescargaMasivaService.svc',
    }
    XMLNS = 'http://DescargaMasivaTerceros.gob.mx'
    XMLNS2 = 'http://DescargaMasivaTerceros.sat.gob.mx'
    ACTIONS = {
        'AUTH': f'{XMLNS}/IAutenticacion/Autentica',
        'REQ': f'{XMLNS2}/ISolicitaDescargaService/SolicitaDescarga',
        'VSD': f'{XMLNS2}/IVerificaSolicitudDescargaService/VerificaSolicitudDescarga',
        'DOWN': f'{XMLNS2}/IDescargaMasivaTercerosService/Descargar',
    }
    HEADERS = {
        'Content-type': 'text/xml;charset="utf-8"',
        'Accept': 'text/xml',
        'Cache-Control': 'no-cache',
        'Expect': '100-continue',
        'Accept-Encoding': 'gzip, deflate',
    }
    NS = {
        's': 'http://schemas.xmlsoap.org/soap/envelope/',
        'u': 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd',
        'o': 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd',
        'des': 'http://DescargaMasivaTerceros.sat.gob.mx',
        'xd': 'http://www.w3.org/2000/09/xmldsig#',
    }
    NS_RESULT = {'s': NS['s'], None: XMLNS}
    NS_RESULT2 = {'s': NS['s'], None: XMLNS2}

    def __init__(self, cert):
        self._cert = cert
        self._error = ''
        self._token = self._get_token()

    @property
    def is_authenticate(self):
        return bool(self._token)

    @property
    def error(self):
        return self._error

    def _get_data_auth(self):
        NSMAP = {'s': self.NS['s'], 'u': self.NS['u']}
        FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ'
        UID = str(uuid.uuid4())

        now = datetime.utcnow()
        date_created = now.strftime(FORMAT)
        date_expires = (now + timedelta(seconds=300)).strftime(FORMAT)

        node_name = f"{{{self.NS['s']}}}Envelope"
        root = ET.Element(node_name, nsmap=NSMAP)

        node_name = f"{{{self.NS['s']}}}Header"
        header = ET.SubElement(root, node_name)

        node_name = f"{{{self.NS['o']}}}Security"
        nsmap = {'o': self.NS['o']}
        attr_name = f"{{{self.NS['s']}}}mustUnderstand"
        attr = {attr_name: '1'}
        security = ET.SubElement(header, node_name, attr, nsmap=nsmap)

        node_name = f"{{{self.NS['u']}}}Timestamp"
        attr_name = f"{{{self.NS['u']}}}Id"
        attr = {attr_name: '_0'}
        timestamp = ET.SubElement(security, node_name, attr)
        node_name = f"{{{self.NS['u']}}}Created"
        ET.SubElement(timestamp, node_name).text = date_created
        node_name = f"{{{self.NS['u']}}}Expires"
        ET.SubElement(timestamp, node_name).text = date_expires

        node_name = f"{{{self.NS['o']}}}BinarySecurityToken"
        attr = {
            f"{{{self.NS['u']}}}Id": UID,
            'ValueType': 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3',
            'EncodingType': 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary'
        }
        ET.SubElement(security, node_name, attr).text = self._cert.cer_txt

        nsmap = {None: 'http://www.w3.org/2000/09/xmldsig#'}
        signature = ET.SubElement(security, 'Signature', nsmap=nsmap)
        signedinfo = ET.SubElement(signature, 'SignedInfo')
        attr1 = {'Algorithm': 'http://www.w3.org/2001/10/xml-exc-c14n#'}
        ET.SubElement(signedinfo, 'CanonicalizationMethod', attr1)
        attr = {'Algorithm': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'}
        ET.SubElement(signedinfo, 'SignatureMethod', attr)

        attr = {'URI': '#_0'}
        reference = ET.SubElement(signedinfo, 'Reference', attr)
        transforms = ET.SubElement(reference, 'Transforms')
        ET.SubElement(transforms, 'Transform', attr1)
        attr = {'Algorithm': 'http://www.w3.org/2000/09/xmldsig#sha1'}
        ET.SubElement(reference, 'DigestMethod', attr)

        dvalue = ET.tostring(timestamp, method='c14n', exclusive=1)
        dvalue = base64.b64encode(hashlib.new('sha1', dvalue).digest())
        ET.SubElement(reference, 'DigestValue').text = dvalue

        signature_value = ET.tostring(signedinfo, method='c14n', exclusive=1)
        signature_value = self._cert.sign_sha1(signature_value)
        ET.SubElement(signature, 'SignatureValue').text = signature_value
        keyinfo = ET.SubElement(signature, 'KeyInfo')

        node_name = f"{{{self.NS['o']}}}SecurityTokenReference"
        security_token = ET.SubElement(keyinfo, node_name)

        node_name = f"{{{self.NS['o']}}}Reference"
        attr = {
            'ValueType': 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3',
            'URI': f'#{UID}',
        }
        ET.SubElement(security_token, node_name, attr)

        node_name = f"{{{self.NS['s']}}}Body"
        body = ET.SubElement(root, node_name)
        ET.SubElement(body, 'Autentica', nsmap=self.NS_RESULT)

        # ~ soap = ET.tostring(root, pretty_print=True, encoding='utf-8')
        soap = ET.tostring(root)

        return soap

    def _get_token(self):
        headers = self.HEADERS.copy()
        headers['SOAPAction'] = self.ACTIONS['AUTH']
        data = self._get_data_auth()

        response = httpx.post(self.URL['AUTH'], data=data, headers=headers)
        if response.status_code != httpx.codes.OK:
            self._error = f'Status: {response.status_code} - {response.text}'
            return

        result = ET.fromstring(response.text)
        nsmap = {'s': self.NS['s'], None: self.XMLNS}
        node_name = 's:Body/AutenticaResponse/AutenticaResult'
        token = result.find(node_name, namespaces=nsmap).text
        # ~ print(f'Token: {token}')

        return token

    def _get_data_req(self, args):
        # ~ NSMAP = {'s': self.NS['s'], 'des': self.NS['des'], 'xd': self.NS['xd']}
        # ~ NSMAP = {'s': self.NS['s'], 'xd': self.NS['xd']}
        NSMAP = {'s': self.NS['s']}
        FORMAT = '%Y-%m-%dT%H:%M:%S'

        date_start = args['date_start']
        date_end = args['date_end']
        msg = f'Descarga desde: {date_start} hasta: {date_end}'
        print(msg)

        node_name = f"{{{self.NS['s']}}}Envelope"
        root = ET.Element(node_name, nsmap=NSMAP)

        node_name = f"{{{self.NS['s']}}}Header"
        header = ET.SubElement(root, node_name)

        node_name = 'ActivityId'
        attr = {'CorrelationId': '806aad0d-ef46-443b-9741-040c8e8e8c7d'}
        nsmap = {None: 'http://schemas.microsoft.com/2004/09/ServiceModel/Diagnostics'}
        activity = ET.SubElement(header, node_name, attr, nsmap=nsmap)
        activity.text = 'e906cfb4-f706-43de-94d0-5cc935be1aaa'

        node_name = f"{{{self.NS['s']}}}Body"
        nsmap = {
            'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
            'xsd': 'http://www.w3.org/2001/XMLSchema',
        }
        body = ET.SubElement(root, node_name, nsmap=nsmap)

        # ~ node_name = f"{{{self.NS['des']}}}SolicitaDescarga"
        node_name = "SolicitaDescarga"
        nsmap = {None: 'http://DescargaMasivaTerceros.sat.gob.mx'}
        request_down = ET.SubElement(body, node_name, nsmap=nsmap)

        # ~ node_name = f"{{{self.NS['des']}}}solicitud"
        node_name = "solicitud"

        type_request = 'CFDI'
        if args['metadata']:
            type_request = 'Metadata'
        attr = {
            'FechaInicial': date_start.strftime(FORMAT),
            'FechaFinal': date_end.strftime(FORMAT),
            'RfcSolicitante': self._cert.rfc,
            'TipoSolicitud': type_request,
            # ~ 'RfcACuentaTerceros': '',
        }
        if args['rfc'] == 'RfcEmisor':
            attr['RfcEmisor'] = self._cert.rfc
        request = ET.SubElement(request_down, node_name, attr)

        node_name = 'RfcReceptores'
        node_receptores = ET.SubElement(request, node_name)
        node_name = 'RfcReceptor'
        node_receptor = ET.SubElement(node_receptores, node_name)
        if args['rfc'] == 'RfcReceptor':
            node_receptor.text = self._cert.rfc

        nsmap = {None: self.NS['xd']}
        signature = ET.SubElement(request, 'Signature', nsmap=nsmap)
        signed_info = ET.SubElement(signature, 'SignedInfo', nsmap=nsmap)

        node_name = 'CanonicalizationMethod'
        attr1 = {'Algorithm': 'http://www.w3.org/2001/10/xml-exc-c14n#'}
        canonicalization = ET.SubElement(signed_info, node_name, attr1)

        node_name = 'SignatureMethod'
        attr = {'Algorithm': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'}
        signature_method = ET.SubElement(signed_info, node_name, attr)

        # ~ attr = {'URI': '#_0'}
        attr = {'URI': ''}
        reference = ET.SubElement(signed_info, 'Reference', attr)
        transforms = ET.SubElement(reference, 'Transforms')
        ET.SubElement(transforms, 'Transform', attr1)
        attr = {'Algorithm': 'http://www.w3.org/2000/09/xmldsig#sha1'}
        ET.SubElement(reference, 'DigestMethod', attr)
        digest_value = ET.SubElement(reference, 'DigestValue')
        signature_value = ET.SubElement(signature, 'SignatureValue')

        key_info = ET.SubElement(signature, 'KeyInfo')
        x_data = ET.SubElement(key_info, 'X509Data')
        x_issuer_serial = ET.SubElement(x_data, 'X509IssuerSerial')
        x_issuer = ET.SubElement(x_issuer_serial, 'X509IssuerName')
        x_serial_number = ET.SubElement(x_issuer_serial, 'X509SerialNumber')
        x_cert = ET.SubElement(x_data, 'X509Certificate')

        dvalue = ET.tostring(request_down, method='c14n', exclusive=1)
        dvalue = base64.b64encode(hashlib.new('sha1', dvalue).digest())
        digest_value.text = dvalue

        sign = ET.tostring(signed_info, method='c14n', exclusive=1)
        sign = self._cert.sign_sha1(sign)
        signature_value.text = sign

        x_issuer.text = self._cert.issuer
        x_serial_number.text = str(self._cert.serial_number2)
        x_cert.text = self._cert.cer_txt

        # ~ soap = b'<?xml version="1.0"?>\n' + ET.tostring(root, pretty_print=True, encoding='utf-8')
        soap = ET.tostring(root, pretty_print=True)
        # ~ print(soap.decode())
        return soap

    def request_download(self, args):
        headers = self.HEADERS.copy()
        headers['SOAPAction'] = self.ACTIONS['REQ']
        headers['Host'] = 'srvsolicituddescargamaster.cloudapp.net'
        headers['Authorization'] = f'WRAP access_token="{self._token}"'
        data = self._get_data_req(args)

        try:
            response = httpx.post(self.URL['REQ'],
                data=data, headers=headers, timeout=TIMEOUT)
        except httpx.TimeoutException as exc:
            print(exc)
            return

        if response.status_code != httpx.codes.OK:
            self._error = f'Status: {response.status_code} - {response.text}'
            return

        result = ET.fromstring(response.text)
        node_name = 's:Body/SolicitaDescargaResponse/SolicitaDescargaResult'
        node = result.find(node_name, namespaces=self.NS_RESULT2)
        data = dict(node.attrib)

        return data

    def _get_data_verify(self, args):
        NSMAP = {'soapenv': self.NS['s'], 'des': self.NS['des'], 'xd': self.NS['xd']}

        node_name = f"{{{self.NS['s']}}}Envelope"
        root = ET.Element(node_name, nsmap=NSMAP)

        node_name = f"{{{self.NS['s']}}}Header"
        body = ET.SubElement(root, node_name)

        node_name = f"{{{self.NS['s']}}}Body"
        body = ET.SubElement(root, node_name)

        node_name = f"{{{self.NS['des']}}}VerificaSolicitudDescarga"
        verify_download = ET.SubElement(body, node_name)

        node_name = f"{{{self.NS['des']}}}solicitud"
        attr = {
            'IdSolicitud': args['id'],
            'RfcSolicitante': self._cert.rfc,
        }
        request = ET.SubElement(verify_download, node_name, attr)

        nsmap = {None: self.NS['xd']}
        signature = ET.SubElement(request, 'Signature', nsmap=nsmap)
        signed_info = ET.SubElement(signature, 'SignedInfo', nsmap=nsmap)

        node_name = 'CanonicalizationMethod'
        attr1 = {'Algorithm': 'http://www.w3.org/2001/10/xml-exc-c14n#'}
        canonicalization = ET.SubElement(signed_info, node_name, attr1)

        node_name = 'SignatureMethod'
        attr = {'Algorithm': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'}
        signature_method = ET.SubElement(signed_info, node_name, attr)

        # ~ attr = {'URI': '#_0'}
        attr = {'URI': ''}
        reference = ET.SubElement(signed_info, 'Reference', attr)
        transforms = ET.SubElement(reference, 'Transforms')
        ET.SubElement(transforms, 'Transform', attr1)
        attr = {'Algorithm': 'http://www.w3.org/2000/09/xmldsig#sha1'}
        ET.SubElement(reference, 'DigestMethod', attr)
        digest_value = ET.SubElement(reference, 'DigestValue')
        signature_value = ET.SubElement(signature, 'SignatureValue')

        key_info = ET.SubElement(signature, 'KeyInfo')
        x_data = ET.SubElement(key_info, 'X509Data')
        x_issuer_serial = ET.SubElement(x_data, 'X509IssuerSerial')
        x_issuer = ET.SubElement(x_issuer_serial, 'X509IssuerName')
        x_serial_number = ET.SubElement(x_issuer_serial, 'X509SerialNumber')
        x_cert = ET.SubElement(x_data, 'X509Certificate')

        dvalue = ET.tostring(verify_download, method='c14n', exclusive=1)
        dvalue = base64.b64encode(hashlib.new('sha1', dvalue).digest())
        digest_value.text = dvalue

        sign = ET.tostring(signed_info, method='c14n', exclusive=1)
        sign = self._cert.sign_sha1(sign)
        signature_value.text = sign

        x_issuer.text = self._cert.issuer
        x_serial_number.text = str(self._cert.serial_number2)
        x_cert.text = self._cert.cer_txt

        # ~ soap = ET.tostring(root, pretty_print=True, encoding='utf-8')
        soap = ET.tostring(root)

        return soap

    def verify(self, args):
        headers = self.HEADERS.copy()
        headers['SOAPAction'] = self.ACTIONS['VSD']
        headers['Authorization'] = f'WRAP access_token="{self._token}"'
        data = self._get_data_verify(args)

        response = httpx.post(self.URL['VSD'], data=data, headers=headers)
        if response.status_code != httpx.codes.OK:
            self._error = f'Status: {response.status_code} - {response.text}'
            return

        result = ET.fromstring(response.text)
        node_name = 's:Body/VerificaSolicitudDescargaResponse/VerificaSolicitudDescargaResult'
        node = result.find(node_name, namespaces=self.NS_RESULT2)
        data = dict(node.attrib)
        data['files'] = [n.text for n in node]

        return data

    def _get_data_download(self, args):
        NSMAP = {'soapenv': self.NS['s'], 'des': self.NS['des'], 'xd': self.NS['xd']}

        node_name = f"{{{self.NS['s']}}}Envelope"
        root = ET.Element(node_name, nsmap=NSMAP)

        node_name = f"{{{self.NS['s']}}}Header"
        body = ET.SubElement(root, node_name)

        node_name = f"{{{self.NS['s']}}}Body"
        body = ET.SubElement(root, node_name)

        node_name = f"{{{self.NS['des']}}}PeticionDescargaMasivaTercerosEntrada"
        request_download = ET.SubElement(body, node_name)

        node_name = f"{{{self.NS['des']}}}peticionDescarga"
        attr = {
            'IdPaquete': args['id_file'],
            'RfcSolicitante': self._cert.rfc,
        }
        request = ET.SubElement(request_download, node_name, attr)

        nsmap = {None: self.NS['xd']}
        signature = ET.SubElement(request, 'Signature', nsmap=nsmap)
        signed_info = ET.SubElement(signature, 'SignedInfo', nsmap=nsmap)

        node_name = 'CanonicalizationMethod'
        attr1 = {'Algorithm': 'http://www.w3.org/2001/10/xml-exc-c14n#'}
        canonicalization = ET.SubElement(signed_info, node_name, attr1)

        node_name = 'SignatureMethod'
        attr = {'Algorithm': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'}
        signature_method = ET.SubElement(signed_info, node_name, attr)

        # ~ attr = {'URI': '#_0'}
        attr = {'URI': ''}
        reference = ET.SubElement(signed_info, 'Reference', attr)
        transforms = ET.SubElement(reference, 'Transforms')
        ET.SubElement(transforms, 'Transform', attr1)
        attr = {'Algorithm': 'http://www.w3.org/2000/09/xmldsig#sha1'}
        ET.SubElement(reference, 'DigestMethod', attr)
        digest_value = ET.SubElement(reference, 'DigestValue')
        signature_value = ET.SubElement(signature, 'SignatureValue')

        key_info = ET.SubElement(signature, 'KeyInfo')
        x_data = ET.SubElement(key_info, 'X509Data')
        x_issuer_serial = ET.SubElement(x_data, 'X509IssuerSerial')
        x_issuer = ET.SubElement(x_issuer_serial, 'X509IssuerName')
        x_serial_number = ET.SubElement(x_issuer_serial, 'X509SerialNumber')
        x_cert = ET.SubElement(x_data, 'X509Certificate')

        dvalue = ET.tostring(request_download, method='c14n', exclusive=1)
        dvalue = base64.b64encode(hashlib.new('sha1', dvalue).digest())
        digest_value.text = dvalue

        sign = ET.tostring(request_download, method='c14n', exclusive=1)
        sign = self._cert.sign_sha1(sign)
        signature_value.text = sign

        x_issuer.text = self._cert.issuer
        x_serial_number.text = str(self._cert.serial_number2)
        x_cert.text = self._cert.cer_txt

        # ~ soap = ET.tostring(root, pretty_print=True, encoding='utf-8')
        soap = ET.tostring(root)

        return soap

    def download(self, args):
        headers = self.HEADERS.copy()
        headers['SOAPAction'] = self.ACTIONS['DOWN']
        headers['Authorization'] = f'WRAP access_token="{self._token}"'
        data = self._get_data_download(args)

        try:
            response = httpx.post(self.URL['DOWN'],
                data=data, headers=headers, timeout=TIMEOUT)
        except httpx.TimeoutException as exc:
            print(exc)
            return

        if response.status_code != httpx.codes.OK:
            self._error = f'Status: {response.status_code} - {response.text}'
            return

        result = ET.fromstring(response.text, parser=ET.XMLParser(huge_tree=True))
        namespaces = self.NS_RESULT2.copy()
        namespaces['h'] = 'http://DescargaMasivaTerceros.sat.gob.mx'

        respuesta = result.find('s:Header/h:respuesta', namespaces=namespaces)
        data = dict(respuesta.attrib)

        node_name = 's:Body/RespuestaDescargaMasivaTercerosSalida/Paquete'
        node = result.find(node_name, namespaces=self.NS_RESULT2)
        archivo = None
        if not node.text is None:
            archivo = base64.b64decode(node.text)

        return data, archivo

