"""Cache backend that degrades instead of 500ing when Redis is unreachable.

Django's builtin RedisCache propagates redis.RedisError, and DRF's throttling
calls cache.get() on *every* request before permissions run. An unreachable
Redis therefore takes down the endpoints that need it least — /api/auth/csrf/
and /api/auth/login/ return 500 and nobody can sign in (2026-08-07 incident:
REDIS_URL pointed at a dead localhost:6379, all AllowAny endpoints 500ed).

django-redis solves this with IGNORE_EXCEPTIONS, but that package is not a
dependency here and its option name is passed straight through to redis-py by
the builtin backend, which rejects it as an unknown connection kwarg. Hence
this subclass.

Trade-off, deliberate: with the cache down, throttle history reads return None
so rate limiting stops enforcing until Redis returns. Login brute-force
protection does NOT depend on this — django-axes records attempts in the
database (AXES_* settings) and keeps locking out independently. Degraded
throttling is judged better than a total auth outage, but it IS a real
weakening: alert on `cache_unavailable` in the logs rather than treating a
quiet fallback as fine.
"""

import logging

from django.core.cache.backends.redis import RedisCache
from redis import RedisError

logger = logging.getLogger(__name__)


class ResilientRedisCache(RedisCache):
    """RedisCache that logs and returns a miss on connection failure.

    Read methods degrade to "miss", write methods to "no-op". Callers already
    handle both — that is the normal cold-cache path — so the app behaves as if
    nothing were cached rather than erroring.
    """

    def _fallback(self, op, default, exc):
        # exc_info at WARNING (not ERROR): a cache outage is degraded service,
        # not a failed request. The traceback is what identifies the cause.
        logger.warning("cache_unavailable: %s failed (%s)", op, exc, exc_info=True)
        return default

    def get(self, key, default=None, version=None):
        try:
            return super().get(key, default, version)
        except RedisError as exc:
            return self._fallback("get", default, exc)

    def get_many(self, keys, version=None):
        try:
            return super().get_many(keys, version)
        except RedisError as exc:
            return self._fallback("get_many", {}, exc)

    def set(self, key, value, timeout=None, version=None, client=None):
        try:
            return super().set(key, value, timeout, version)
        except RedisError as exc:
            return self._fallback("set", None, exc)

    def set_many(self, data, timeout=None, version=None):
        try:
            return super().set_many(data, timeout, version)
        except RedisError as exc:
            # Contract is "list of keys that failed" — with the cache down,
            # that is all of them.
            return self._fallback("set_many", list(data), exc)

    def add(self, key, value, timeout=None, version=None):
        try:
            return super().add(key, value, timeout, version)
        except RedisError as exc:
            # False = "not added", which is the truthful answer here.
            return self._fallback("add", False, exc)

    def touch(self, key, timeout=None, version=None):
        try:
            return super().touch(key, timeout, version)
        except RedisError as exc:
            return self._fallback("touch", False, exc)

    def incr(self, key, delta=1, version=None):
        try:
            return super().incr(key, delta, version)
        except RedisError as exc:
            # No sensible degraded value for a counter; callers that increment
            # (rate limits) treat None as "unknown" rather than a wrong count.
            return self._fallback("incr", None, exc)

    def delete(self, key, version=None):
        try:
            return super().delete(key, version)
        except RedisError as exc:
            return self._fallback("delete", False, exc)

    def delete_many(self, keys, version=None):
        try:
            return super().delete_many(keys, version)
        except RedisError as exc:
            return self._fallback("delete_many", None, exc)

    def has_key(self, key, version=None):
        try:
            return super().has_key(key, version)
        except RedisError as exc:
            return self._fallback("has_key", False, exc)

    def clear(self):
        try:
            return super().clear()
        except RedisError as exc:
            return self._fallback("clear", False, exc)
