#!/usr/bin/env python
# ~
# ~ PAC
# ~ Copyright (C) 2018-2019  Mauricio Baeza Servin - public [AT] elmau [DOT] net
# ~
# ~ This program is free software: you can redistribute it and/or modify
# ~ it under the terms of the GNU General Public License as published by
# ~ the Free Software Foundation, either version 3 of the License, or
# ~ (at your option) any later version.
# ~
# ~ This program is distributed in the hope that it will be useful,
# ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
# ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# ~ GNU General Public License for more details.
# ~
# ~ You should have received a copy of the GNU General Public License
# ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.


import base64
import logging

import lxml.etree as ET
import requests
from requests.exceptions import ConnectionError

from .conf import DEBUG
# ~ , AUTH


LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s'
LOG_DATE = '%d/%m/%Y %H:%M:%S'
logging.addLevelName(logging.ERROR, '\033[1;41mERROR\033[1;0m')
logging.addLevelName(logging.DEBUG, '\x1b[33mDEBUG\033[1;0m')
logging.addLevelName(logging.INFO, '\x1b[32mINFO\033[1;0m')
logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT, datefmt=LOG_DATE)
log = logging.getLogger(__name__)

logging.getLogger('requests').setLevel(logging.ERROR)


TIMEOUT = 10

NAMESPACES = {
    '3.3': 'http://www.sat.gob.mx/cfd/3',
    '4.0': 'http://www.sat.gob.mx/cfd/4',
    'tdf': 'http://www.sat.gob.mx/TimbreFiscalDigital',
}


def pretty_print_POST(req):
    """
    At this point it is completely built and ready
    to be fired; it is "prepared".

    However pay attention at the formatting used in
    this function because it is programmed to be pretty
    printed and may differ from the actual request.
    """
    print('{}\n{}\r\n{}\r\n\r\n{}'.format(
        '-----------START-----------',
        req.method + ' ' + req.url,
        '\r\n'.join('{}: {}'.format(k, v) for k, v in req.headers.items()),
        req.body,
    ))


class PACComercioDigital(object):
    ws = 'https://{}.comercio-digital.mx/{}'
    api = 'https://app2.comercio-digital.mx/{}'
    URL = {
        'timbra': ws.format('ws', 'timbre4/timbrarV5'),
        'cancel': ws.format('cancela', 'cancela4/cancelarUuid'),
        'cancelxml': ws.format('cancela', 'cancela4/cancelarXml'),
        'status': ws.format('cancela', 'arws/consultaEstatus'),
        'client': api.format('x3/altaEmpresa'),
        'saldo': api.format('x3/saldo'),
        'timbres': api.format('x3/altaTimbres'),
    }
    CODES = {
        '000':  '000 Exitoso',
        '004':  '004 RFC {} ya esta dado de alta con Estatus=A',
        '704':  '704 Usuario Invalido',
        '702':  '702  Error rfc/empresa invalido',
    }
    NS_CFDI = {
        'cfdi': 'http://www.sat.gob.mx/cfd/4',
        'tdf': 'http://www.sat.gob.mx/TimbreFiscalDigital',
    }

    if DEBUG:
        ws = 'https://pruebas.comercio-digital.mx/{}'
        ws6 = 'https://pruebas6.comercio-digital.mx/arws/{}'
        URL = {
            'timbra': ws.format('timbre4/timbrarV5'),
            'cancel': ws.format('cancela4/cancelarUuid'),
            'cancelxml': ws.format('cancela4/cancelarXml'),
            'status': ws6.format('consultaEstatus'),
            'client': api.format('x3/altaEmpresa'),
            'saldo': api.format('x3/saldo'),
            'timbres': api.format('x3/altaTimbres'),
        }

    def __init__(self):
        self.error = ''

    def _error(self, msg):
        self.error = str(msg)
        log.error(msg)
        return

    def _post(self, url, data, headers={}):
        result = None
        headers['host'] = url.split('/')[2]
        headers['Content-type'] = 'text/plain'
        headers['Connection'] = 'Keep-Alive'
        headers['Expect'] = '100-continue'

        if DEBUG:
            req = requests.Request('POST', url, headers=headers, data=data)
            prepared = req.prepare()
            pretty_print_POST(prepared)

        try:
            result = requests.post(url, data=data, headers=headers, timeout=TIMEOUT)
        except ConnectionError as e:
            self._error(e)

        return result

    def _validate_cfdi(self, xml):
        """
            Comercio Digital solo soporta la declaración con doble comilla
        """
        # ~ tree = ET.fromstring(xml.encode())
        # ~ xml = ET.tostring(tree,
            # ~ pretty_print=True, doctype='<?xml version="1.0" encoding="utf-8"?>')
        return xml.encode('utf-8')

    def stamp(self, cfdi, auth):
        # ~ if DEBUG or not auth:
            # ~ auth = AUTH

        url = self.URL['timbra']
        headers = {
            'usrws': auth['user'],
            'pwdws': auth['pass'],
            'tipo': 'XML',
        }
        cfdi = self._validate_cfdi(cfdi)
        result = self._post(url, cfdi, headers)

        if result is None:
            return ''

        if result.status_code != 200:
            return ''

        if 'errmsg' in result.headers:
            self._error(result.headers['errmsg'])
            return ''

        xml = result.content
        tree = ET.fromstring(xml)
        cfdi_uuid = tree.xpath(
            'string(//cfdi:Complemento/tdf:TimbreFiscalDigital/@UUID)',
            namespaces=self.NS_CFDI)
        date_stamped = tree.xpath(
            'string(//cfdi:Complemento/tdf:TimbreFiscalDigital/@FechaTimbrado)',
            namespaces=self.NS_CFDI)

        data = {
            'xml': xml.decode(),
            'uuid': cfdi_uuid,
            'date': date_stamped,
        }
        return data

    def _get_data_cancel(self, cfdi, info, auth):
        info['tipo'] = 'cfdi'
        info['key'] = base64.b64encode(info['key_enc']).decode()
        info['cer'] = base64.b64encode(info['cer_ori']).decode()

        tree = ET.fromstring(cfdi.encode())
        version = tree.attrib['Version']

        namespaces = {
            'cfdi': NAMESPACES[version],
            'tdf': NAMESPACES['tdf'],
        }

        tipo = tree.xpath(
            'string(//cfdi:Comprobante/@TipoDeComprobante)',
            namespaces=namespaces)
        total = tree.xpath(
            'string(//cfdi:Comprobante/@Total)',
            namespaces=namespaces)
        rfc_emisor = tree.xpath(
            'string(//cfdi:Comprobante/cfdi:Emisor/@Rfc)',
            namespaces=namespaces)
        rfc_receptor = tree.xpath(
            'string(//cfdi:Comprobante/cfdi:Receptor/@Rfc)',
            namespaces=namespaces)
        uid = tree.xpath(
            'string(//cfdi:Complemento/tdf:TimbreFiscalDigital/@UUID)',
            namespaces=namespaces)
        data = (
            f"USER={auth['user']}",
            f"PWDW={auth['pass']}",
            f"RFCE={rfc_emisor}",
            f"UUID={uid}",
            f"PWDK={info['pass']}",
            f"KEYF={info['key']}",
            f"CERT={info['cer']}",
            f"TIPO1={info['tipo']}",
            f"ACUS=SI",
            f"RFCR={rfc_receptor}",
            f"TIPOC={tipo}",
            f"TOTAL={total}",
            f"UUIDREL={info['args']['uuid']}",
            f"MOTIVO={info['args']['reason']}",
        )
        return '\n'.join(data)

    def cancel(self, cfdi, info, auth):
        # ~ if DEBUG or not auth:
            # ~ auth = AUTH
        url = self.URL['cancel']
        data = self._get_data_cancel(cfdi, info, auth)

        result = self._post(url, data)

        if result is None:
            return ''

        if result.status_code != 200:
            return ''

        if result.headers['codigo'] != '000':
            self._error(result.headers['errmsg'])
            return ''

        tree = ET.fromstring(result.text)
        date_cancel = tree.xpath('string(//Acuse/@Fecha)')[:19]

        data = {
            'acuse': result.text,
            'date': date_cancel,
        }

        return data

    def _get_headers_cancel_xml(self, cfdi, info, auth):
        NS_CFDI = {
            'cfdi': 'http://www.sat.gob.mx/cfd/3',
            'tdf': 'http://www.sat.gob.mx/TimbreFiscalDigital',
        }
        tree = ET.fromstring(cfdi.encode())
        tipocfdi = tree.xpath(
            'string(//cfdi:Comprobante/@TipoDeComprobante)',
            namespaces=NS_CFDI)
        total = tree.xpath(
            'string(//cfdi:Comprobante/@Total)',
            namespaces=NS_CFDI)
        rfc_receptor = tree.xpath(
            'string(//cfdi:Comprobante/cfdi:Receptor/@Rfc)',
            namespaces=NS_CFDI)

        headers = {
            'usrws': auth['user'],
            'pwdws': auth['pass'],
            'rfcr': rfc_receptor,
            'total': total,
            'tipocfdi': tipocfdi,
        }
        headers.update(info)

        return headers

    def cancel_xml(self, xml, auth, cfdi='', info={'tipo': 'cfdi'}):
        # ~ if DEBUG or not auth:
            # ~ auth = AUTH

        url = self.URL['cancelxml']
        headers = self._get_headers_cancel_xml(cfdi, info, auth)
        result = self._post(url, xml, headers)

        if result is None:
            return ''

        if result.status_code != 200:
            return ''

        if result.headers['codigo'] != '000':
            self._error(result.headers['errmsg'])
            return ''

        tree = ET.fromstring(result.text)
        date_cancel = tree.xpath('string(//Acuse/@Fecha)')[:19]

        data = {
            'acuse': result.text,
            'date': date_cancel,
        }
        return data

    def status(self, data, auth):
        # ~ if not auth:
            # ~ auth = AUTH
        url = self.URL['status']

        data = (
            f"USER={auth['user']}",
            f"PWDW={auth['pass']}",
            f"RFCR={data['rfc_receptor']}",
            f"RFCE={data['rfc_emisor']}",
            f"TOTAL={data['total']}",
            f"UUID={data['uuid']}",
        )
        data = '\n'.join(data)
        result = self._post(url, data)

        if result is None:
            return ''

        if result.status_code != 200:
            self._error(result.status_code)
            return self.error

        return result.text

    def _get_data_client(self, auth, values):
        data = [f"usr_ws={auth['user']}", f"pwd_ws={auth['pass']}"]
        fields = (
            'rfc_contribuyente',
            'nombre_contribuyente',
            'calle',
            'noExterior',
            'noInterior',
            'colonia',
            'localidad',
            'municipio',
            'estado',
            'pais',
            'cp',
            'contacto',
            'telefono',
            'email',
            'rep_nom',
            'rep_rfc',
            'email_fact',
            'pwd_asignado',
        )
        data += [f"{k}={values[k]}" for k in fields]

        return '\n'.join(data)

    def client_add(self, data, auth):
        # ~ auth = AUTH
        url = self.URL['client']
        data = self._get_data_client(auth, data)

        result = self._post(url, data)

        if result is None:
            return False

        if result.status_code != 200:
            self._error(f'Code: {result.status_code}')
            return False

        if result.text != self.CODES['000']:
            self._error(result.text)
            return False

        return True

    def client_balance(self, data, rfc=''):
        url = self.URL['saldo']
        host = url.split('/')[2]
        headers = {
            'Content-type': 'text/plain',
            'Host': host,
            'Connection' : 'Keep-Alive',
        }
        data = {'usr': data['user'], 'pwd': data['pass']}
        try:
            result = requests.get(url, params=data, headers=headers, timeout=TIMEOUT)
        except ConnectionError as e:
            self._error(e)
            return ''

        if result.status_code != 200:
            return ''

        if result.text == self.CODES['704']:
            self._error(result.text)
            return ''

        if result.text == self.CODES['702']:
            self._error(result.text)
            return ''

        return result.text

    def client_add_timbres(self, data, auth):
        # ~ if not auth:
            # ~ auth = AUTH
        url = self.URL['timbres']
        data = '\n'.join((
            f"usr_ws={auth['user']}",
            f"pwd_ws={auth['pass']}",
            f"rfc_recibir={data['rfc']}",
            f"num_timbres={data['timbres']}"
        ))

        result = self._post(url, data)

        if result is None:
            return False

        if result.status_code != 200:
            self._error(f'Code: {result.status_code}')
            return False

        if result.text != self.CODES['000']:
            self._error(result.text)
            return False

        return True

