"""
Redis-backed permission cache backend.

Requires the ``redis`` package::

    pip install abs-auth-rbac-core[redis]
"""

import json
from typing import Optional

from pydantic import BaseModel

from abs_utils.logger import setup_logger

from .cache import PermissionCacheEntry

logger = setup_logger(__name__)


class RedisCacheConfig(BaseModel):
    """Configuration for the Redis permission cache backend."""
    url: Optional[str] = None
    host: str = "localhost"
    port: int = 6379
    db: int = 0
    password: Optional[str] = None
    ssl: bool = False
    ttl: float = 300.0
    key_prefix: Optional[str] = None


def _serialize_entry(entry: PermissionCacheEntry) -> str:
    """Serialize a PermissionCacheEntry to JSON."""
    return json.dumps({
        "role_permissions": [list(t) for t in entry.role_permissions],
        "direct_permissions": [list(t) for t in entry.direct_permissions],
        "is_super_admin": entry.is_super_admin,
        "created_at": entry.created_at,
        "ttl": entry.ttl,
    })


def _deserialize_entry(data: str) -> PermissionCacheEntry:
    """Deserialize JSON back to a PermissionCacheEntry."""
    obj = json.loads(data)
    return PermissionCacheEntry(
        role_permissions={tuple(t) for t in obj["role_permissions"]},
        direct_permissions={tuple(t) for t in obj["direct_permissions"]},
        is_super_admin=obj["is_super_admin"],
        created_at=obj["created_at"],
        ttl=obj["ttl"],
    )


class RedisCache:
    """
    Redis-backed permission cache implementing the same interface as
    PermissionCache.

    Uses ``setex`` for native TTL — no sweep thread needed.
    Uses ``SCAN`` for ``invalidate_all()`` to avoid blocking the server.
    """

    def __init__(self, config: RedisCacheConfig):
        try:
            import redis as redis_lib
        except ImportError:
            raise ImportError(
                "The 'redis' package is required for RedisCache. "
                "Install it with: pip install abs-auth-rbac-core[redis]"
            )

        self._ttl = config.ttl
        self._prefix = config.key_prefix

        if config.url:
            self._client = redis_lib.Redis.from_url(
                config.url,
                decode_responses=True,
            )
        else:
            self._client = redis_lib.Redis(
                host=config.host,
                port=config.port,
                db=config.db,
                password=config.password,
                ssl=config.ssl,
                decode_responses=True,
            )

        logger.info(
            f"[RBAC] Redis permission cache initialized "
            f"(prefix={self._prefix}, ttl={self._ttl}s)"
        )

    @property
    def ttl(self) -> float:
        return self._ttl

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

    def get_user(self, user_uuid: str) -> Optional[PermissionCacheEntry]:
        data = self._client.get(self._key(user_uuid))
        if data is None:
            return None
        return _deserialize_entry(data)

    def set_user(self, user_uuid: str, entry: PermissionCacheEntry) -> None:
        self._client.setex(
            self._key(user_uuid),
            int(self._ttl),
            _serialize_entry(entry),
        )

    def invalidate_user(self, user_uuid: str) -> None:
        self._client.delete(self._key(user_uuid))

    def invalidate_users(self, user_uuids: list) -> None:
        if not user_uuids:
            return
        keys = [self._key(uid) for uid in user_uuids]
        self._client.delete(*keys)

    def invalidate_all(self) -> None:
        cursor = 0
        pattern = f"{self._prefix}*"
        while True:
            cursor, keys = self._client.scan(cursor=cursor, match=pattern, count=500)
            if keys:
                self._client.delete(*keys)
            if cursor == 0:
                break
        logger.debug("[RBAC] Redis cache: invalidated all entries")

    def ping(self) -> bool:
        """Return True if the Redis server is reachable, False otherwise."""
        try:
            return self._client.ping()
        except Exception:
            return False

    def close(self) -> None:
        try:
            self._client.close()
        except Exception:
            pass
