import functools
import inspect


class ProxyAttr:
    """
    Descriptor class for wrapping attributes of the wrapped instance.

    Used with the `Proxy` class, this class is used to set exposed attributes of the wrapped instance while providing
    optional type casting.
    """
    def __init__(self, cast=False):
        self._cast__ = cast

    def __set_name__(self, owner, name):
        cast = self._cast__
        if cast:
            def getter(self):
                value = getattr(self._wrapped__, name)
                return cast(value) if value is not None else None
        else:
            def getter(self):
                return getattr(self._wrapped__, name)

        def setter(self, value):
            return setattr(self._wrapped__, name, value)

        setattr(owner, name, property(getter, setter))


class ProxyFunc:
    """
    Descriptor class for wrapping functions of the wrapped instance.

    Used with the `Proxy` class, this class is used to set exposed functions of the wrapped instance
    while also allowing optional type casting on return values.
    """
    def __init__(self, cast=False):
        self._cast__ = cast

    def __set_name__(self, owner, name):
        func = getattr(owner._wrapped__, name)
        descriptor = inspect.getattr_static(owner._wrapped__, name)
        cast = self._cast__

        if isinstance(descriptor, staticmethod):
            if cast:
                def wrap_func(*args, **kwargs):
                    result = func(*args, **kwargs)
                    return cast(result) if result is not None else None
            elif cast is None:
                def wrap_func(*args, **kwargs):
                    func(*args, **kwargs)
            else:
                def wrap_func(*args, **kwargs):
                    return func(*args, **kwargs)

            functools.update_wrapper(wrap_func, func)
            wrap_func = staticmethod(wrap_func)

        elif isinstance(descriptor, classmethod):
            if cast:
                def wrap_func(cls, *args, **kwargs):
                    result = func(*args, **kwargs)
                    return cast(result) if result is not None else None
            elif cast is None:
                def wrap_func(cls, *args, **kwargs):
                    func(*args, **kwargs)
            else:
                def wrap_func(cls, *args, **kwargs):
                    return func(*args, **kwargs)

            functools.update_wrapper(wrap_func, func)
            wrap_func = classmethod(wrap_func)

        else:
            if cast:
                def wrap_func(self, *args, **kwargs):
                    result = func(self._wrapped__, *args, **kwargs)
                    return cast(result) if result is not None else None
            elif cast is None:
                def wrap_func(self, *args, **kwargs):
                    func(self._wrapped__, *args, **kwargs)
            else:
                def wrap_func(self, *args, **kwargs):
                    return func(self._wrapped__, *args, **kwargs)

            functools.update_wrapper(wrap_func, func)

        setattr(owner, name, wrap_func)


class ProxyMeta(type):
    def __new__(cls, clsname, bases, attrs):
        attrs.update({func: ProxyFunc() for func in ("__repr__", "__str__") if func not in attrs})
        proxy_class = super().__new__(cls, clsname, bases, attrs)
        # To preserve the docstring, signature, code of the wrapped class
        # `updated` to an emtpy list so it doesn't copy the `__dict__`
        # See `functools.WRAPPER_ASSIGNMENTS` and `functools.WRAPPER_UPDATES`
        functools.update_wrapper(proxy_class, proxy_class._wrapped__, updated=[])
        return proxy_class


class Proxy(metaclass=ProxyMeta):
    """
    A proxy class implementing the Facade pattern.

    This class delegates to an underlying instance while exposing a curated subset of its attributes and methods.
    Useful for controlling access, simplifying interfaces, or adding cross-cutting concerns.
    """
    _wrapped__ = object

    def __init__(self, instance):
        """
        Initializes the proxy by setting the wrapped instance.

        :param instance: The instance of the class to be wrapped.
        """
        object.__setattr__(self, "_wrapped__", instance)

    @property
    def __class__(self):
        return type(self)._wrapped__
