from typing import Any, Optional, TYPE_CHECKING

from .base import CacheClient

if TYPE_CHECKING:
    from ..serializer import Serializer


class RedisCacheClient(CacheClient):
    """Redis-backed cache client.

    Wraps :class:`redis.asyncio.Redis` with ``decode_responses=True`` so that
    all reads return strings (not bytes).  When a :class:`~cache_core.serializer.Serializer`
    is provided, collection-style operations (lists, sets, hashes, sorted sets)
    automatically serialize each member on write and deserialize on read.

    Parameters
    ----------
    url:
        Redis connection URL (e.g. ``"redis://localhost:6379/0"``).
    serializer:
        Optional serializer.  Defaults to :class:`~cache_core.serializer.SmartSerializer`
        when left as ``None``.
    **kwargs:
        Passed through to :class:`redis.asyncio.Redis`.
    """

    def __init__(
        self,
        url: str = "redis://localhost:6379/0",
        serializer: "Serializer | None" = None,
        **kwargs: Any,
    ) -> None:
        self._url = url
        self._kwargs = kwargs
        self._serializer = serializer
        self._client: Any = None

    # -- helpers --------------------------------------------------------------

    def _ensure_client(self) -> Any:
        if self._client is None:
            raise RuntimeError(
                "RedisCacheClient is not connected.  Call await client.connect() first."
            )
        return self._client

    def _maybe_serialize(self, value: Any) -> Any:
        if self._serializer is None:
            return value
        return self._serializer.serialize(value)

    def _maybe_deserialize(self, value: Any) -> Any:
        if self._serializer is None:
            return value
        return self._serializer.deserialize(value)

    # -- Connection lifecycle ------------------------------------------------

    async def connect(self) -> None:
        import redis.asyncio as aioredis  # lazy import — redis is optional

        self._client = aioredis.Redis.from_url(
            self._url, decode_responses=True, **self._kwargs
        )

    async def disconnect(self) -> None:
        if self._client is not None:
            await self._client.close()
            self._client = None

    async def ping(self) -> bool:
        return await self._ensure_client().ping()

    # -- Key-Value -----------------------------------------------------------

    async def get(self, key: str) -> Any:
        value = await self._ensure_client().get(key)
        if value is None:
            return None
        return self._maybe_deserialize(value)

    async def set(self, key: str, value: Any, ttl: int | None = None) -> None:
        serialized = self._maybe_serialize(value)
        if ttl is not None:
            await self._ensure_client().setex(key, ttl, serialized)
        else:
            await self._ensure_client().set(key, serialized)

    async def setnx(self, key: str, value: Any, ttl: int | None = None) -> bool:
        """Set *key* to *value* only if it does **not** already exist.

        Uses Redis ``SET key value NX [EX ttl]`` for atomicity.
        Returns ``True`` if the key was set, ``False`` if it already existed.
        """
        serialized = self._maybe_serialize(value)
        result = await self._ensure_client().set(
            key, serialized, nx=True, ex=ttl,
        )
        return result is True

    async def delete(self, *keys: str) -> int:
        if not keys:
            return 0
        return await self._ensure_client().delete(*keys)

    async def exists(self, *keys: str) -> int:
        if not keys:
            return 0
        return await self._ensure_client().exists(*keys)

    async def expire(self, key: str, ttl: int) -> bool:
        return await self._ensure_client().expire(key, ttl)

    async def ttl(self, key: str) -> int:
        return await self._ensure_client().ttl(key)

    async def incr(self, key: str, amount: int = 1) -> int:
        return await self._ensure_client().incrby(key, amount)

    async def decr(self, key: str, amount: int = 1) -> int:
        return await self._ensure_client().decrby(key, amount)

    # -- Hash ----------------------------------------------------------------

    async def hget(self, key: str, field: str) -> Any:
        value = await self._ensure_client().hget(key, field)
        if value is None:
            return None
        return self._maybe_deserialize(value)

    async def hset(self, key: str, mapping: dict[str, Any]) -> int:
        serialized = {k: self._maybe_serialize(v) for k, v in mapping.items()}
        return await self._ensure_client().hset(key, mapping=serialized)

    async def hgetall(self, key: str) -> dict[str, Any]:
        raw = await self._ensure_client().hgetall(key)
        return {k: self._maybe_deserialize(v) for k, v in raw.items()}

    async def hdel(self, key: str, *fields: str) -> int:
        if not fields:
            return 0
        return await self._ensure_client().hdel(key, *fields)

    async def hexists(self, key: str, field: str) -> bool:
        return await self._ensure_client().hexists(key, field)

    async def hkeys(self, key: str) -> list[str]:
        return await self._ensure_client().hkeys(key)

    async def hlen(self, key: str) -> int:
        return await self._ensure_client().hlen(key)

    # -- List ----------------------------------------------------------------

    async def lpush(self, key: str, *values: Any) -> int:
        serialized = [self._maybe_serialize(v) for v in values]
        return await self._ensure_client().lpush(key, *serialized)

    async def rpush(self, key: str, *values: Any) -> int:
        serialized = [self._maybe_serialize(v) for v in values]
        return await self._ensure_client().rpush(key, *serialized)

    async def lpop(self, key: str) -> Any:
        value = await self._ensure_client().lpop(key)
        if value is None:
            return None
        return self._maybe_deserialize(value)

    async def rpop(self, key: str) -> Any:
        value = await self._ensure_client().rpop(key)
        if value is None:
            return None
        return self._maybe_deserialize(value)

    async def lrange(self, key: str, start: int, stop: int) -> list[Any]:
        values = await self._ensure_client().lrange(key, start, stop)
        return [self._maybe_deserialize(v) for v in values]

    async def llen(self, key: str) -> int:
        return await self._ensure_client().llen(key)

    # -- Set -----------------------------------------------------------------

    async def sadd(self, key: str, *members: Any) -> int:
        serialized = [self._maybe_serialize(m) for m in members]
        return await self._ensure_client().sadd(key, *serialized)

    async def srem(self, key: str, *members: Any) -> int:
        serialized = [self._maybe_serialize(m) for m in members]
        return await self._ensure_client().srem(key, *serialized)

    async def smembers(self, key: str) -> list[Any]:
        values = await self._ensure_client().smembers(key)
        return [self._maybe_deserialize(v) for v in values]

    async def sismember(self, key: str, member: Any) -> bool:
        return await self._ensure_client().sismember(key, self._maybe_serialize(member))

    async def scard(self, key: str) -> int:
        return await self._ensure_client().scard(key)

    # -- Sorted-set ----------------------------------------------------------

    async def zadd(self, key: str, mapping: dict[Any, float]) -> int:
        serialized = {self._maybe_serialize(m): score for m, score in mapping.items()}
        return await self._ensure_client().zadd(key, serialized)

    async def zrem(self, key: str, *members: Any) -> int:
        serialized = [self._maybe_serialize(m) for m in members]
        return await self._ensure_client().zrem(key, *serialized)

    async def zrange(
        self, key: str, start: int, stop: int, withscores: bool = False
    ) -> list[Any]:
        values = await self._ensure_client().zrange(key, start, stop, withscores=withscores)
        if withscores:
            return [(self._maybe_deserialize(m), score) for m, score in values]
        return [self._maybe_deserialize(v) for v in values]

    async def zcard(self, key: str) -> int:
        return await self._ensure_client().zcard(key)

    # -- Utility -------------------------------------------------------------

    async def keys(self, pattern: str) -> list[str]:
        return await self._ensure_client().keys(pattern)

    async def flush(self) -> None:
        await self._ensure_client().flushdb()
