# -*- coding: utf-8 -*-

"""Redis-backed caching decorator."""

import hashlib
import pickle  # nosec B403
from typing import Any, Callable, Dict, Optional, cast

from core_mixins.decorators.cache_memory_based import L2Backend
from core_mixins.decorators.cache_memory_based import _MISS
from core_mixins.decorators.cache_memory_based import cache

from core_redis.client import RedisClient


class _RedisBackend(L2Backend):
    """
    :class:`~core_mixins.decorators.cache.L2Backend` that stores cache
    entries as pickled byte strings in Redis.  Each entry is stored under
    the key ``{key_prefix}{qualname}/{md5_of_cache_key}``.

    TTL is delegated entirely to Redis: when *ttl* is set the key is
    written with ``SET … EX ttl`` so Redis expires it natively, no
    background threads or manual deletion are required.

    **Required Redis setup**

    Any Redis instance reachable by the provided *redis_kwargs* is
    sufficient.  No special key-space configuration is required.
    """

    def __init__(
        self,
        fcn_qualname: str,
        key_prefix: str = "cache/",
        ttl: Optional[float] = None,
        redis_kwargs: Optional[Dict[str, Any]] = None,
    ) -> None:
        """
        :param fcn_qualname:
            ``__qualname__`` of the decorated function; used to derive a
            unique key namespace so different functions never share
            entries, even when using the same Redis instance.

        :param key_prefix:
            String prepended to every Redis key. Default: ``"cache/"``.

        :param ttl:
            Time-to-live in seconds.  The value is forwarded to Redis as
            the ``EX`` argument of ``SET``, so Redis handles expiry
            natively.  ``None`` means keys are stored without an expiry.

        :param redis_kwargs:
            Keyword arguments forwarded verbatim to
            :class:`~core_redis.client.RedisClient`.
            For example ``{"host": "localhost", "port": 6379, "db": 0}``.
        """

        super().__init__(ttl=ttl)
        self.key_prefix = key_prefix
        self._client = RedisClient(**(redis_kwargs or {}))
        self._fcn_qualname = fcn_qualname.replace("<", "_").replace(">", "_")

    def _item_key(self, cache_key: Any) -> str:
        digest = hashlib.md5(
            pickle.dumps(cache_key), usedforsecurity=False
        ).hexdigest()

        return f"{self.key_prefix}{self._fcn_qualname}/{digest}"

    def load(self, cache_key: Any) -> Any:
        """
        Return the cached value, or :data:`~core_mixins.decorators.cache._MISS`
        when the Redis key does not exist, has expired, or is unreachable.

        TTL expiry is handled by Redis natively, no manual timestamp
        comparison or deletion is performed here.

        Any Redis or deserialization error is silently treated as a cache miss
        so that transient failures never break the decorated function.
        """

        try:
            raw = self._client.get(self._item_key(cache_key))
            if raw is None:
                return _MISS

            return pickle.loads(raw)  # noqa: S301  # nosec B301

        except Exception:  # pylint: disable=broad-exception-caught
            return _MISS

    def save(self, cache_key: Any, value: Any) -> None:
        """Persist *value* in Redis under *cache_key*.

        When :attr:`ttl` is set the key is written with ``EX`` so Redis
        expires it automatically.  When :attr:`ttl` is ``None`` the key
        is stored without an expiry.

        Errors are silently suppressed so that a write failure (e.g. Redis
        unavailable) never raises from the decorated function.
        """

        try:
            self._client.set(
                self._item_key(cache_key),
                pickle.dumps(value),
                ex=int(self.ttl) if self.ttl is not None else None,
            )

        except Exception:  # pylint: disable=broad-exception-caught  # nosec B110
            pass


def cache_redis_based(
    *,
    key_prefix: str = "cache/",
    maxsize: Optional[int] = None,
    ttl: Optional[float] = None,
    redis_kwargs: Optional[Dict[str, Any]] = None,
) -> Callable:
    """
    Write-through caching decorator: L1 is a bounded in-memory LRU
    (:class:`~core_mixins.decorators.cache._CacheWrapper`); the fallback
    is a Redis instance (:class:`_RedisBackend`).

    Every new result is written to both L1 and Redis immediately. When L1
    is full the least-recently-used entry is evicted from memory only; the
    Redis key is kept. A subsequent call with the same arguments (from the
    same *or a different* process/machine) reloads the value from Redis
    without invoking the wrapped function.

    TTL is delegated to Redis natively via ``SET … EX``, so no background
    threads or manual expiry management are required.

    :param key_prefix:
        String prepended to every Redis key. Default: ``"cache/"``.

    :param maxsize:
        Maximum number of entries kept in the in-memory L1 cache.
        ``None`` means unbounded.

    :param ttl:
        Time-to-live in seconds applied to both layers.  Expired L1
        entries are evicted from memory lazily on the next access.
        Expired Redis keys are removed by Redis automatically.
        ``None`` (default) means entries never expire.

    :param redis_kwargs:
        Keyword arguments forwarded verbatim to
        :class:`~core_redis.client.RedisClient`
        (e.g. ``{"host": "localhost", "port": 6379, "db": 0}``).

    :return: The wrapped function.

    Example:
        .. code-block:: python

            from core_redis.decorators import cache_redis_based

            @cache_redis_based(
                key_prefix="myapp/",
                ttl=3600,
                redis_kwargs={"host": "localhost", "port": 6379},
            )
            def fetch_reference_data(dataset: str) -> dict:
                ...
        ..
    """

    def decorator(_fcn: Callable) -> Callable:
        return cache(
            _fcn,
            maxsize=maxsize,
            fallback=_RedisBackend(
                fcn_qualname=cast(Any, _fcn).__qualname__,
                key_prefix=key_prefix,
                ttl=ttl,
                redis_kwargs=redis_kwargs,
            ),
            ttl=ttl,
        )

    return decorator
