"""Redis cache backend."""
from __future__ import annotations

import json
from typing import Any

from logos_adk.cache import CacheBackend, CacheEntry, _cosine_similarity


class RedisCacheBackend(CacheBackend):
    """Redis-backed cache with TTL support."""

    def __init__(self, url: str = "redis://localhost:6379", prefix: str = "logos:") -> None:
        self._url = url
        self._prefix = prefix
        self._client: Any = None

    async def _ensure_client(self) -> Any:
        if self._client is None:
            try:
                import redis.asyncio as aioredis
            except ImportError:
                raise ImportError(
                    "Redis cache requires the 'redis' package. "
                    "Install it with: pip install logos-adk[cache-redis]"
                )
            self._client = aioredis.from_url(self._url)
        return self._client

    def _key(self, key: str) -> str:
        return f"{self._prefix}{key}"

    async def get(self, key: str) -> CacheEntry | None:
        client = await self._ensure_client()
        raw = await client.get(self._key(key))
        if raw is None:
            return None

        data = json.loads(raw)
        data["hit_count"] = data.get("hit_count", 0) + 1
        await client.set(self._key(key), json.dumps(data), keepttl=True)

        return CacheEntry(
            response_data=data["response_data"],
            created_at=data["created_at"],
            hit_count=data["hit_count"],
            vector=data.get("vector"),
            scope=data.get("scope"),
        )

    async def put(self, key: str, entry: CacheEntry, ttl: int | None) -> None:
        client = await self._ensure_client()
        data = {
            "response_data": entry.response_data,
            "created_at": entry.created_at,
            "hit_count": entry.hit_count,
            "vector": entry.vector,
            "scope": entry.scope,
        }
        if ttl is not None:
            await client.set(self._key(key), json.dumps(data), ex=ttl)
        else:
            await client.set(self._key(key), json.dumps(data))

    async def invalidate(self, key: str) -> None:
        client = await self._ensure_client()
        await client.delete(self._key(key))

    async def clear(self) -> None:
        client = await self._ensure_client()
        cursor = 0
        while True:
            cursor, keys = await client.scan(
                cursor=cursor, match=f"{self._prefix}*", count=1000
            )
            if keys:
                await client.delete(*keys)
            if cursor == 0:
                break

    async def get_stats(self) -> dict[str, Any]:
        client = await self._ensure_client()
        cursor = 0
        total_entries = 0
        total_hits = 0
        while True:
            cursor, keys = await client.scan(
                cursor=cursor, match=f"{self._prefix}*", count=100
            )
            for key in keys:
                raw = await client.get(key)
                if raw is None:
                    continue
                total_entries += 1
                data = json.loads(raw)
                total_hits += int(data.get("hit_count", 0) or 0)
            if cursor == 0:
                break
        return {
            "backend": "Redis",
            "total_entries": total_entries,
            "total_hits": total_hits,
        }

    async def search_similar(
        self,
        vector: list[float],
        threshold: float,
        limit: int = 1,
        scope: str | None = None,
    ) -> list[CacheEntry]:
        client = await self._ensure_client()
        results: list[tuple[float, CacheEntry]] = []

        cursor = 0
        while True:
            cursor, keys = await client.scan(
                cursor=cursor, match=f"{self._prefix}*", count=100
            )
            for k in keys:
                k_str = k.decode() if isinstance(k, bytes) else k
                if k_str.endswith(":vec"):
                    continue
                raw = await client.get(k)
                if raw is None:
                    continue
                data = json.loads(raw)
                stored_vec = data.get("vector")
                if stored_vec is None:
                    continue
                if scope is not None and data.get("scope") != scope:
                    continue
                sim = _cosine_similarity(vector, stored_vec)
                if sim >= threshold:
                    results.append((sim, CacheEntry(
                        response_data=data["response_data"],
                        created_at=data["created_at"],
                        hit_count=data.get("hit_count", 0),
                        vector=stored_vec,
                        scope=data.get("scope"),
                    )))
            if cursor == 0:
                break

        results.sort(key=lambda x: x[0], reverse=True)
        return [entry for _, entry in results[:limit]]
