import zeep

from decimal import Decimal
from datetime import date, datetime, timedelta
from requests import Response
from types import SimpleNamespace, FunctionType


TIMEOUT = 30
SERIALIZABLE_TYPES = (
    type(None), bool, int, float, str, bytes, tuple, list, dict, Decimal, date, datetime, timedelta, Response
)


class Client:
    """A wrapper for Zeep.Client

    * providing a simpler API to pass timeouts and session,
    * restricting its attributes to a few, most-commonly used accross Odoo's modules,
    * serializing the returned values of its methods.
    """
    def __init__(self, *args, **kwargs):
        transport = kwargs.setdefault('transport', zeep.Transport())
        # The timeout for loading wsdl and xsd documents.
        transport.load_timeout = kwargs.pop('timeout', None) or transport.load_timeout or TIMEOUT
        # The timeout for operations (POST/GET)
        transport.operation_timeout = kwargs.pop('operation_timeout', None) or transport.operation_timeout or TIMEOUT
        # The `requests.session` used for HTTP requests
        transport.session = kwargs.pop('session', None) or transport.session

        client = zeep.Client(*args, **kwargs)

        self.__obj = client
        self.__service = None

    @classmethod
    def __serialize_object(cls, obj):
        if isinstance(obj, list):
            return [cls.__serialize_object(sub) for sub in obj]
        if isinstance(obj, (dict, zeep.xsd.valueobjects.CompoundValue)):
            result = SerialProxy(**{key: cls.__serialize_object(obj[key]) for key in obj})
            return result
        if type(obj) in SERIALIZABLE_TYPES:
            return obj
        raise ValueError(f'{obj} is not serializable')

    @classmethod
    def __serialize_object_wrapper(cls, method):
        def wrapper(*args, **kwargs):
            return cls.__serialize_object(method(*args, **kwargs))
        return wrapper

    @property
    def service(self):
        if not self.__service:
            self.__service = ReadOnlyMethodNamespace(**{
                key: self.__serialize_object_wrapper(operation)
                for key, operation in self.__obj.service._operations.items()
            })
        return self.__service

    def type_factory(self, namespace):
        types = self.__obj.wsdl.types
        namespace = namespace if namespace in types.namespaces else types.get_ns_prefix(namespace)
        documents = types.documents.get_by_namespace(namespace, fail_silently=True)
        types = {
            key[len(f'{{{namespace}}}'):]: type_
            for document in documents
            for key, type_ in document._types.items()
        }
        return ReadOnlyMethodNamespace(**{key: self.__serialize_object_wrapper(type_) for key, type_ in types.items()})

    def get_type(self, name):
        return self.__serialize_object_wrapper(self.__obj.wsdl.types.get_type(name))

    def create_service(self, binding_name, address):
        service = self.__obj.create_service(binding_name, address)
        return ReadOnlyMethodNamespace(**{
            key: self.__serialize_object_wrapper(operation)
            for key, operation in service._operations.items()
        })

    def bind(self, service_name, port_name):
        service = self.__obj.bind(service_name, port_name)
        operations = {
            key: self.__serialize_object_wrapper(operation)
            for key, operation in service._operations.items()
        }
        operations['_binding_options'] = service._binding_options
        return ReadOnlyMethodNamespace(**operations)


class ReadOnlyMethodNamespace(SimpleNamespace):
    """A read-only attribute-based namespace not prefixed by `_` and restricted to functions.

    By default, `types.SympleNamespace` doesn't implement `__setitem__` and `__delitem__`,
    no need to implement them to ensure the read-only property of this class.
    """
    def __init__(self, **kwargs):
        assert all(
            (not key.startswith('_') and isinstance(value, FunctionType))
            or
            (key == '_binding_options' and isinstance(value, dict))
            for key, value in kwargs.items()
        )
        super().__init__(**kwargs)

    def __getitem__(self, key):
        return self.__dict__[key]

    def __setattr__(self, key, value):
        raise NotImplementedError

    def __delattr__(self, key):
        raise NotImplementedError


class SerialProxy(SimpleNamespace):
    """An attribute-based namespace not prefixed by `_` and restricted to few types.

    It pretends to be a zeep `CompoundValue` so zeep.helpers.serialize_object threats it as such.

    `__getitem__` and `__delitem__` are supported, but `__setitem__` is prevented,
    e.g.
    ```py
    proxy = SerialProxy(foo='foo')
    proxy.foo  # Allowed
    proxy['foo']  # Allowed
    proxy.foo = 'bar'  # Allowed
    proxy['foo'] = 'bar'  # Prevented
    del proxy.foo  # Allowed
    del proxy['foo']  # Allowed
    ```
    """

    # Pretend to be a CompoundValue so zeep can serialize this when sending a request with this object in the payload
    # https://stackoverflow.com/a/42958013
    # https://github.com/mvantellingen/python-zeep/blob/a65b4363c48b5c3f687b8df570bcbada8ba66b9b/src/zeep/helpers.py#L15
    @property
    def __class__(self):
        return zeep.xsd.valueobjects.CompoundValue

    def __init__(self, **kwargs):
        for key, value in kwargs.items():
            self.__check(key, value)
        super().__init__(**kwargs)

    def __setattr__(self, key, value):
        self.__check(key, value)
        return super().__setattr__(key, value)

    def __getitem__(self, key):
        self.__check(key, None)
        return self.__getattribute__(key)

    # Not required as SimpleNamespace doesn't implement it by default, but this makes it explicit.
    def __setitem__(self, key, value):
        raise NotImplementedError

    def __delitem__(self, key):
        self.__check(key, None)
        self.__delattr__(key)

    def __iter__(self):
        return iter(self.__dict__)

    def __repr__(self):
        return repr(self.__dict__)

    def __str__(self):
        return str(self.__dict__)

    def keys(self):
        return self.__dict__.keys()

    def values(self):
        return self.__dict__.values()

    def items(self):
        return self.__dict__.items()

    @classmethod
    def __check(cls, key, value):
        assert not key.startswith('_') or key.startswith('_value_')
        assert type(value) in SERIALIZABLE_TYPES + (SerialProxy,)