from abc import ABC, abstractmethod
from collections.abc import Awaitable
from typing import Any, Callable, Concatenate, cast

from pydantic import SecretStr

from phederation.utils.base import AccessType


class BaseCache(ABC):
    @abstractmethod
    def get(self, key: str) -> None | Any:
        pass

    @abstractmethod
    def set(self, key: str, value: Any):  # pyright: ignore[reportAny]
        pass

    @abstractmethod
    def delete(self, key: str):
        pass

    @abstractmethod
    def clear(
        self, prefix: str | None = None
    ):
        pass

    @abstractmethod
    def close(self):
        pass


class WithCache(ABC):
    """
    Specifies that the class has a cache property that can be accessed by the decorator.
    """

    cache: BaseCache | None = None


def with_cache(prefix: str):
    def with_cache_internal[**Params, SELF: WithCache, ReturnType](
        function: Callable[Concatenate[SELF, Params], Awaitable[ReturnType]],
    ) -> Callable[Concatenate[SELF, Params], Awaitable[ReturnType]]:
        async def wrapper_method(self: SELF, *args_with_cache: Params.args, **kwargs_with_cache: Params.kwargs):
            cache_or_none = self.cache
            args_joined = "-".join([str(arg) for arg in args_with_cache if isinstance(arg, str) or isinstance(arg, int)])
            sorted_keys = sorted(list(kwargs_with_cache.keys()))
            kwargs_joined_list: list[str] = []
            for arg in sorted_keys:
                value = kwargs_with_cache[arg]
                if isinstance(value, str) or isinstance(value, int) or isinstance(value, AccessType):
                    kwargs_joined_list.append(str(value))

                # this is important for security.
                # If an argument list contains a token or password, the result should not be cached.
                if isinstance(value, SecretStr):
                    cache_or_none = None
                    break
            kwargs_joined = "-".join(kwargs_joined_list)
            cache_key = "-".join([prefix, str(hash(args_joined)), str(hash(kwargs_joined))])
            if cache_or_none:
                if cached := cache_or_none.get(cache_key):
                    return cast(ReturnType, cached)
            result = await function(self, *args_with_cache, **kwargs_with_cache)
            if cache_or_none:
                cache_or_none.set(key=cache_key, value=result)
            return result

        return wrapper_method

    return with_cache_internal
