"""Tools for managing Redis clients.

The 'normal' way to get Redis clients is via Django's cache backend:

```
import redis
from django.core.cache import cache

def get_redis_client():
    client = cache.client.get_client()
    assert isinstance(client, redis.Redis)
    return client
```

This works fine if you have a Redis cache backend. The problem is that it only
returns a synchronous client, since Django's cache backend is synchronous. Thus
this module uses the Django cache backend for synchonrous clients, but has a
separate function for getting an asynchronous client.
"""

import asyncio
import logging
import weakref

import redis as sync_redis
import redis.asyncio as async_redis
from django.conf import settings
from django.core.cache import cache
from django.core.cache.backends.redis import RedisCacheClient

log = logging.getLogger(__name__)


def get_sync_client() -> sync_redis.Redis:
    """Get a synchronous Redis client.

    Uses the same mechanism as Django's cache backend, so that it can piggyback
    off Django's settings."""
    django_cache_client = getattr(cache, "_cache")
    assert isinstance(django_cache_client, RedisCacheClient)
    redis_client = django_cache_client.get_client()
    assert isinstance(redis_client, sync_redis.Redis)
    return redis_client


# We need one client per asyncio event loop. This is a pain in tests because
# pytest seems to run with a different event loop for each unit test.
_global_cached_clients: weakref.WeakKeyDictionary[
    asyncio.AbstractEventLoop, async_redis.Redis
] = weakref.WeakKeyDictionary()


def get_async_client() -> async_redis.Redis:
    """Get an asynchronous Redis client.

    This returns a single shared global client. We can't use the same client the
    same one as Django's cache framework because that one is synchronous, but we
    do reuse the same Redis settings. Also, unlike Django, we're caching the
    _client_, rather than the connection pool; I'm doing this because it's
    simpler and I can't see an obvious reason not to."""
    global _global_cached_clients
    current_loop = asyncio.get_event_loop()
    if current_loop not in _global_cached_clients:
        n_old_clients = len(_global_cached_clients)
        if n_old_clients > 0:
            log.warning(
                f"Creating a new Redis client, but {n_old_clients} clients "
                "from old event loops still exist. This may be a bug."
            )

        # construct a Redis connection poolg
        cache_settings = settings.CACHES["default"]
        assert (
            cache_settings["BACKEND"] == "django.core.cache.backends.redis.RedisCache"
        ), f"Expected cache backend to be RedisCache, got {cache_settings['BACKEND']=}"
        assert isinstance(cache_settings["LOCATION"], str), (
            f"Expected cache location to be a string, got {cache_settings['LOCATION']=}."
            " Note that this doesn't yet support Redis clusters (which would use a list)."
        )
        redis_options = cache_settings.get("OPTIONS", {})
        _global_cached_clients[current_loop] = async_redis.Redis.from_url(
            cache_settings["LOCATION"],
            # See issue #148 (Redis client sometimes fails with "Connection
            # reset by peer")
            health_check_interval=30,
            **redis_options,
        )
    return _global_cached_clients[current_loop]
