"""Redis client wrapper with async support."""

import json
import logging
import os
from datetime import datetime
from typing import Optional

from bson import ObjectId
from redis.asyncio import Redis
from redis.exceptions import RedisError, ConnectionError as RedisConnectionError

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


class MongoJSONEncoder(json.JSONEncoder):
    """Custom JSON encoder for MongoDB types like ObjectId and datetime."""

    def default(self, o):
        """Encode MongoDB-specific types to JSON-serializable formats."""
        if isinstance(o, datetime):
            return o.isoformat()
        if isinstance(o, ObjectId):
            return str(o)
        return super().default(o)


class RedisClient:
    """Async Redis client for caching user contexts."""

    def __init__(self):
        """Initialize Redis client (connection established on connect())."""
        self._client: Optional[Redis] = None
        self._connected = False

    async def connect(self) -> None:
        """
        Establish connection to Redis.

        Raises:
            RedisConnectionError: If connection fails
        """
        try:
            redis_url = os.getenv("REDIS_URL", "redis://localhost:6379/0")
            self._client = Redis.from_url(
                redis_url,
                encoding="utf-8",
                decode_responses=True,
                socket_connect_timeout=5,
                socket_timeout=5,
            )

            # Test connection
            await self._client.ping()
            self._connected = True

            logger.info("Connected to Redis at %s", redis_url)

        except RedisConnectionError as exc:
            logger.error("Failed to connect to Redis: %s", exc)
            raise
        except Exception as exc:  # pylint: disable=broad-exception-caught
            logger.error("Unexpected error connecting to Redis: %s", exc)
            raise

    async def disconnect(self) -> None:
        """Close Redis connection."""
        if self._client:
            await self._client.aclose()
            self._connected = False
            logger.info("Disconnected from Redis")

    async def health_check(self) -> bool:
        """
        Check if Redis connection is healthy.

        Returns:
            True if connected and responsive, False otherwise
        """
        if not self._client or not self._connected:
            return False

        try:
            await self._client.ping()
            return True
        except RedisError:
            return False

    def _get_key(self, customer_id: str) -> str:
        """
        Generate Redis key for a customer.

        Args:
            customer_id: Customer identifier

        Returns:
            Redis key string
        """
        return f"context:{customer_id}"

    async def get_context(self, customer_id: str) -> Optional[dict]:
        """
        Retrieve user context from Redis cache.

        Args:
            customer_id: Customer identifier

        Returns:
            Context dictionary if found, None if not found

        Raises:
            RedisError: If Redis operation fails
        """
        if not self._client:
            raise RedisError("Redis client not connected. Call connect() first.")

        key = self._get_key(customer_id)

        try:
            data = await self._client.get(key)

            if data is None:
                logger.debug("Cache miss for customer_id: %s", customer_id)
                return None

            context = json.loads(data)
            logger.info(
                "Cache hit for customer_id: %s",
                customer_id,
                extra={"key": key}
            )
            return context

        except json.JSONDecodeError as exc:
            logger.error(
                "Invalid JSON in cache for customer_id: %s",
                customer_id,
                extra={"error": str(exc)}
            )
            # Delete corrupted data
            await self._client.delete(key)
            return None

        except RedisError as exc:
            logger.error(
                "Redis error retrieving context for customer_id: %s",
                customer_id,
                extra={"error": str(exc)}
            )
            raise

    async def set_context(
        self, customer_id: str, context: dict, ttl: Optional[int] = None
    ) -> bool:
        """
        Store user context in Redis cache.

        Args:
            customer_id: Customer identifier
            context: Context data to store
            ttl: Optional time-to-live in seconds

        Returns:
            True if successful, False otherwise

        Raises:
            RedisError: If Redis operation fails
        """
        if not self._client:
            raise RedisError("Redis client not connected. Call connect() first.")
        key = self._get_key(customer_id)

        try:
            # Serialize to JSON
            data = json.dumps(context, ensure_ascii=False, cls=MongoJSONEncoder)

            # Store in Redis
            if ttl:
                await self._client.setex(key, ttl, data)
            else:
                await self._client.set(key, data)

            logger.info(
                "Cached context for customer_id: %s",
                customer_id,
                extra={"key": key, "ttl": ttl, "size_bytes": len(data)}
            )
            return True

        except TypeError as exc:
            logger.error(
                "Failed to serialize context for customer_id: %s",
                customer_id,
                extra={"error": str(exc)}
            )
            return False

        except RedisError as exc:
            logger.error(
                "Redis error storing context for customer_id: %s",
                customer_id,
                extra={"error": str(exc)}
            )
            raise

    async def delete_context(self, customer_id: str) -> bool:
        """
        Delete user context from Redis cache.

        Args:
            customer_id: Customer identifier

        Returns:
            True if deleted, False if not found

        Raises:
            RedisError: If Redis operation fails
        """
        if not self._client:
            raise RedisError("Redis client not connected. Call connect() first.")

        key = self._get_key(customer_id)

        try:
            result = await self._client.delete(key)

            if result > 0:
                logger.info("Deleted context for customer_id: %s", customer_id)
                return True
            logger.debug("No context to delete for customer_id: %s", customer_id)
            return False

        except RedisError as exc:
            logger.error(
                "Redis error deleting context for customer_id: %s",
                customer_id,
                extra={"error": str(exc)}
            )
            raise


# Global client instance
_redis_client: Optional[RedisClient] = None  # pylint: disable=invalid-name


async def get_redis_client() -> RedisClient:
    """
    Get or create the global Redis client instance.

    Returns:
        RedisClient instance

    Note:
        You must call connect() on the client before using it.
    """
    global _redis_client  # pylint: disable=global-statement
    if _redis_client is None:
        _redis_client = RedisClient()
    return _redis_client
