import re
from typing import Any, ClassVar, Dict, List, NoReturn, Union

from cryptography.exceptions import UnsupportedAlgorithm
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from requests import Response, Session

from .exc import (
    AccountDoesNotExist,
    BankCodeClabeMismatch,
    ClaveRastreoAlreadyInUse,
    DuplicatedAccount,
    InvalidAccountType,
    InvalidAmount,
    InvalidField,
    InvalidInstitution,
    InvalidPassphrase,
    InvalidRfcOrCurp,
    InvalidTrackingKey,
    MandatoryField,
    NoOrdenesEncontradas,
    NoServiceResponse,
    PldRejected,
    SameAccount,
    SignatureValidationError,
    StpmexException,
)
from .resources import CuentaFisica, Orden, Resource, Saldo
from .version import __version__ as client_version

DEMO_HOST = 'https://demo.stpmex.com:7024'
PROD_HOST = 'https://prod.stpmex.com'


class Client:
    base_url: str
    soap_url: str
    session: Session

    # resources
    cuentas: ClassVar = CuentaFisica
    ordenes: ClassVar = Orden
    saldos: ClassVar = Saldo

    def __init__(
        self,
        empresa: str,
        priv_key: str,
        priv_key_passphrase: str,
        demo: bool = False,
        base_url: str = None,
        soap_url: str = None,
        timeout: tuple = None,
    ):
        self.timeout = timeout
        self.session = Session()
        self.session.headers['User-Agent'] = f'stpmex-python/{client_version}'
        if demo:
            host_url = DEMO_HOST
            self.session.verify = False
        else:
            host_url = PROD_HOST
            self.session.verify = True
        self.base_url = base_url or f'{host_url}/speiws/rest'
        self.soap_url = (
            soap_url or f'{host_url}/spei/webservices/SpeiConsultaServices'
        )

        try:
            self.pkey = serialization.load_pem_private_key(
                priv_key.encode('utf-8'),
                priv_key_passphrase.encode('ascii'),
                default_backend(),
            )
        except (ValueError, TypeError, UnsupportedAlgorithm):
            raise InvalidPassphrase
        Resource.empresa = empresa
        Resource._client = self

    def post(
        self, endpoint: str, data: Dict[str, Any]
    ) -> Union[Dict[str, Any], List[Any]]:
        return self.request('post', endpoint, data)

    def put(
        self, endpoint: str, data: Dict[str, Any]
    ) -> Union[Dict[str, Any], List[Any]]:
        return self.request('put', endpoint, data)

    def delete(
        self, endpoint: str, data: Dict[str, Any]
    ) -> Union[Dict[str, Any], List[Any]]:
        return self.request('delete', endpoint, data)

    def request(
        self, method: str, endpoint: str, data: Dict[str, Any], **kwargs: Any
    ) -> Union[Dict[str, Any], List[Any]]:
        url = self.base_url + endpoint
        response = self.session.request(
            method,
            url,
            json=data,
            timeout=self.timeout,
            **kwargs,
        )
        self._check_response(response)
        resultado = response.json()
        if 'resultado' in resultado:  # Some responses are enveloped
            resultado = resultado['resultado']
        return resultado

    @staticmethod
    def _check_response(response: Response) -> None:
        if not response.ok:
            response.raise_for_status()
        resp = response.json()
        if isinstance(resp, dict):
            try:
                _raise_description_error_exc(resp)
            except KeyError:
                ...
            try:
                assert resp['descripcion']
                _raise_description_exc(resp)
            except (AssertionError, KeyError):
                ...
        response.raise_for_status()


def _raise_description_error_exc(resp: Dict) -> NoReturn:
    id = resp['resultado']['id']
    error = resp['resultado']['descripcionError']

    if id == 0 and error == 'No se recibió respuesta del servicio':
        raise NoServiceResponse(**resp['resultado'])
    elif id == 0 and error == 'Error validando la firma':
        raise SignatureValidationError(**resp['resultado'])
    elif id == 0 and re.match(r'El campo .+ es obligatorio', error):
        raise MandatoryField(**resp['resultado'])
    elif id == -1 and re.match(
        r'La clave de rastreo .+ ya fue utilizada', error
    ):
        raise ClaveRastreoAlreadyInUse(**resp['resultado'])
    elif id == -7 and re.match(r'La cuenta .+ no existe', error):
        raise AccountDoesNotExist(**resp['resultado'])
    elif id == -9 and re.match(r'La Institucion \d+ no es valida', error):
        raise InvalidInstitution(**resp['resultado'])
    elif id == -11 and re.match(r'El tipo de cuenta \d+ es invalido', error):
        raise InvalidAccountType(**resp['resultado'])
    elif id == -20 and re.match(r'El monto {.+} no es válido', error):
        raise InvalidAmount(**resp['resultado'])
    elif id == -22 and 'no coincide para la institucion operante' in error:
        raise BankCodeClabeMismatch(**resp['resultado'])
    elif id == -24 and re.match(r'Cuenta {\d+} - {MISMA_CUENTA}', error):
        raise SameAccount(**resp['resultado'])
    elif id == -34 and 'Clave rastreo invalida' in error:
        raise InvalidTrackingKey(**resp['resultado'])
    elif id == -100 and error.startswith('No se encontr'):
        raise NoOrdenesEncontradas
    elif id == -200 and 'Se rechaza por PLD' in error:
        raise PldRejected(**resp['resultado'])
    else:
        raise StpmexException(**resp['resultado'])


def _raise_description_exc(resp: Dict) -> NoReturn:
    id = resp['id']
    desc = resp['descripcion']

    if id == 0 and 'Cuenta en revisión' in desc:
        # STP regresa esta respuesta cuando se registra
        # una cuenta. No se levanta excepción porque
        # todas las cuentas pasan por este status.
        ...
    elif id == 1 and desc == 'rfc/curp invalido':
        raise InvalidRfcOrCurp(**resp)
    elif id == 1 and re.match(r'El campo \w+ es invalido', desc):
        raise InvalidField(**resp)
    elif id == 3 and desc == 'Cuenta Duplicada':
        raise DuplicatedAccount(**resp)
    elif id == 5 and re.match(r'El campo .* obligatorio \w+', desc):
        raise MandatoryField(**resp)
    else:
        raise StpmexException(**resp)

[((57, 23, 57, 32), 'requests.Session', 'Session', ({}, {}), '()', False, 'from requests import Response, Session\n'), ((74, 16, 74, 33), 'cryptography.hazmat.backends.default_backend', 'default_backend', ({}, {}), '()', False, 'from cryptography.hazmat.backends import default_backend\n'), ((139, 21, 139, 67), 're.match', 're.match', ({(139, 30, 139, 59): '"""El campo .+ es obligatorio"""', (139, 61, 139, 66): 'error'}, {}), "('El campo .+ es obligatorio', error)", False, 'import re\n'), ((178, 21, 178, 64), 're.match', 're.match', ({(178, 30, 178, 57): '"""El campo \\\\w+ es invalido"""', (178, 59, 178, 63): 'desc'}, {}), "('El campo \\\\w+ es invalido', desc)", False, 'import re\n'), ((141, 22, 143, 5), 're.match', 're.match', ({(142, 8, 142, 50): '"""La clave de rastreo .+ ya fue utilizada"""', (142, 52, 142, 57): 'error'}, {}), "('La clave de rastreo .+ ya fue utilizada', error)", False, 'import re\n'), ((145, 22, 145, 64), 're.match', 're.match', ({(145, 31, 145, 56): '"""La cuenta .+ no existe"""', (145, 58, 145, 63): 'error'}, {}), "('La cuenta .+ no existe', error)", False, 'import re\n'), ((182, 21, 182, 67), 're.match', 're.match', ({(182, 30, 182, 60): '"""El campo .* obligatorio \\\\w+"""', (182, 62, 182, 66): 'desc'}, {}), "('El campo .* obligatorio \\\\w+', desc)", False, 'import re\n'), ((147, 22, 147, 73), 're.match', 're.match', ({(147, 31, 147, 65): '"""La Institucion \\\\d+ no es valida"""', (147, 67, 147, 72): 'error'}, {}), "('La Institucion \\\\d+ no es valida', error)", False, 'import re\n'), ((149, 23, 149, 76), 're.match', 're.match', ({(149, 32, 149, 68): '"""El tipo de cuenta \\\\d+ es invalido"""', (149, 70, 149, 75): 'error'}, {}), "('El tipo de cuenta \\\\d+ es invalido', error)", False, 'import re\n'), ((151, 23, 151, 70), 're.match', 're.match', ({(151, 32, 151, 62): '"""El monto {.+} no es válido"""', (151, 64, 151, 69): 'error'}, {}), "('El monto {.+} no es válido', error)", False, 'import re\n'), ((155, 23, 155, 72), 're.match', 're.match', ({(155, 32, 155, 64): '"""Cuenta {\\\\d+} - {MISMA_CUENTA}"""', (155, 66, 155, 71): 'error'}, {}), "('Cuenta {\\\\d+} - {MISMA_CUENTA}', error)", False, 'import re\n')]