import abc
import inspect
import logging


class SpartaCache(abc.ABC):
    def __init__(self):
        self.logger = logging.getLogger(f"{__name__}.{type(self).__name__}")

    @abc.abstractmethod
    def map_key(self, key) -> str:
        pass

    @abc.abstractmethod
    def __getitem__(self, key):
        pass

    @abc.abstractmethod
    def __setitem__(self, key, value):
        pass

    @abc.abstractmethod
    def __delitem__(self, key):
        pass

    @abc.abstractmethod
    def __contains__(self, key):
        pass

    @abc.abstractmethod
    def add(self, key, *args, **kwargs):
        pass

    @abc.abstractmethod
    def append(self, key, *args, **kwargs):
        pass

    @abc.abstractmethod
    def cas(self, key, *args, **kwargs):
        pass

    @abc.abstractmethod
    def decr(self, key, *args, **kwargs):
        pass

    @abc.abstractmethod
    def delete(self, key, *args, **kwargs):
        pass

    @abc.abstractmethod
    def get(self, key, *args, **kwargs):
        pass

    @abc.abstractmethod
    def gets(self, key, *args, **kwargs):
        pass

    @abc.abstractmethod
    def incr(self, key, *args, **kwargs):
        pass

    @abc.abstractmethod
    def prepend(self, key, *args, **kwargs):
        pass

    @abc.abstractmethod
    def replace(self, key, *args, **kwargs):
        pass

    @abc.abstractmethod
    def set(self, key, *args, **kwargs):
        pass

    @abc.abstractmethod
    def touch(self, key, *args, **kwargs):
        pass

    def atomic_append(self, key, value, _retries=3, *args, **kwargs):
        """
        Required cas behavior!
        Assures atomic append to a list of objects (only tuple is supported, not list).
        Solution copied from https://stackoverflow.com/a/27468294/20230162
        """
        try:
            _values, cas_id = self.gets(key)
        except ValueError as e:
            raise RuntimeError("atomic_append requires cas behavior") from e
        if _values is None:
            self.set(key, (value,), *args, **kwargs)
        else:
            if value not in _values:
                ok = self.cas(key, _values + (value,), cas_id, *args, **kwargs)
                if not ok and _retries > 1:
                    self.atomic_append(key, value, _retries - 1, *args, **kwargs)

    def get_or_compute(
        self, func, *args, _key=None, _key_suffix=None, _time=None, **kwargs
    ):
        """
        Returns value from cache. If cache miss, stores result of func() as value to cache and returns result.
        @param func: the function to compute
        @param _key: the cache key, if None the key is autogenerated based on `func` and `args`
        @param _key_suffix: the cache key suffix (useful when key is autogenerated)
        @param _time: the cache ttl for current key

        Example:
            ```
            def get_user(user_id):
                ...

            my_user = cache.get_or_compute(get_user, my_user_id, _time=3600)
            # or ...
            my_user = cache.get_or_compute(get_user, my_user_id, _key=f"MyService:user:{my_user_id}", _time=3600)
            # or ...
            my_user = cache.get_or_compute(get_user, my_user_id, _key_suffix="MyService", _time=3600)
            ```
        """

        if inspect.iscoroutinefunction(func):
            raise TypeError(
                f"Function {func.__name__} is async. Use get_or_compute_async instead."
            )

        if not _key:
            _key = func.__name__
            if args:
                _key += "-"
                _key += "-".join([str(a) for a in args])

        if _key_suffix:
            _key += "-" + str(_key_suffix)

        _result = self.get(_key)
        if _result is None:
            _result = func(*args, **kwargs)
            self.set(_key, _result, time=_time)
            print(_key, _result, _time)
        return _result

    async def get_or_compute_async(
        self, func, *args, _key=None, _key_suffix=None, _time=None, **kwargs
    ):
        """
        Returns value from cache. If cache miss, stores result of func() as value to cache and returns result.
        @param func: the function to compute
        @param _key: the cache key, if None the key is autogenerated based on `func` and `args`
        @param _key_suffix: the cache key suffix (useful when key is autogenerated)
        @param _time: the cache ttl for current key

        Example:
            ```
            async def get_user_async(user_id):
                ...

            my_user = await cache.get_or_compute_async(get_user_async, my_user_id, _time=3600)
            # or ...
            my_user = await cache.get_or_compute_async(get_user_async, my_user_id, _key=f"MyService:user:{my_user_id}", _time=3600)
            # or ...
            my_user = await cache.get_or_compute_async(get_user_async, my_user_id, _key_suffix="MyService", _time=3600)
            ```
        """

        if not inspect.iscoroutinefunction(func):
            raise TypeError(
                f"Function {func.__name__} is not async. Use get_or_compute instead."
            )

        if not _key:
            _key = func.__name__
            if args:
                _key += "-"
                _key += "-".join([str(a) for a in args])

        if _key_suffix:
            _key += "-" + str(_key_suffix)

        _result = self.get(_key)
        if _result is None:
            _result = await func(*args, **kwargs)
            self.set(_key, _result, time=_time)
        return _result
