import zeep
import zeep.plugins
import zeep.helpers
import zeep.exceptions
import zeep.transports
from requests import RequestException

from .soap_client import SoapClient
from .wsse import sign_envelope, verify_envelope
from .exceptions import (
    InvalidSignatureResponse,
    SoapServerException,
    MethodDoesNotExist,
    TypeDoesNotExist,
    SoapRequestException,
)
from .utils import load_key_from_data, parse_tbk_error_message, xml_to_string


class ZeepSoapClient(SoapClient):
    def __init__(
        self,
        wsdl_url,
        key_data,
        cert_data,
        tbk_cert_data,
        password=None,
        transport_timeout=300,
    ):
        super(ZeepSoapClient, self).__init__(
            wsdl_url, key_data, cert_data, tbk_cert_data
        )
        self.wsse = ZeepWsseSignature.init_from_data(
            key_data, cert_data, tbk_cert_data, password=password
        )
        self.transport_timeout = transport_timeout
        self.transport = zeep.transports.Transport(timeout=self.transport_timeout)
        self.history = zeep.plugins.HistoryPlugin()
        self.client = zeep.Client(
            wsdl_url, wsse=self.wsse, transport=self.transport, plugins=[self.history]
        )

    def create_object(self, type_name, *args, **kwargs):
        try:
            object_type = self.client.get_type("ns0:{}".format(type_name))
        except zeep.exceptions.LookupError:
            raise TypeDoesNotExist(type_name)
        else:
            return object_type(*args, **kwargs)

    def get_enum_value(self, enum_name, value):
        return self.create_object(enum_name, value)

    def request(self, request, timeout=None):
        try:
            timeout = timeout or self.transport_timeout
            with self.transport.settings(timeout=timeout):
                method = self.get_method(request.method_name)
                result = method(*request.args, **request.kwargs)
        except zeep.exceptions.Fault as fault:
            self.logger.exception("Fault")
            error, code = parse_tbk_error_message(fault.message)
            raise SoapServerException(error, code, request)
        except RequestException as error:
            self.logger.exception("Request exception")
            raise SoapRequestException(error, request)
        else:
            serialized = zeep.helpers.serialize_object(result)
            last_sent = self.get_last_sent_envelope()
            last_received = self.get_last_received_envelope()
            return serialized, last_sent, last_received

    def get_method(self, method_name):
        try:
            return getattr(self.client.service, method_name)
        except AttributeError:
            raise MethodDoesNotExist(method_name)

    def get_last_sent_envelope(self):
        return xml_to_string(self.history.last_sent["envelope"])

    def get_last_received_envelope(self):
        return xml_to_string(self.history.last_received["envelope"])


class ZeepWsseSignature(object):
    def __init__(self, key, tbk_cert):
        self.key = key
        self.tbk_cert = tbk_cert

    @classmethod
    def init_from_data(cls, key_data, cert_data, tbk_cert_data, password=None):
        key = load_key_from_data(key_data, cert_data, password)
        tbk_cert = load_key_from_data(tbk_cert_data, key_format="CERT_PEM")
        return cls(key, tbk_cert)

    def apply(self, envelope, headers):
        sign_envelope(envelope, self.key)
        return envelope, headers

    def verify(self, envelope):
        if not verify_envelope(envelope, self.tbk_cert):
            raise InvalidSignatureResponse(envelope)
        return envelope