"""Query Result Caching for Database Nodes.

This module provides Redis-based caching for database query results
with TTL management and cache invalidation strategies.

Key Features:
- Redis-based query result caching
- Cache key generation from queries
- TTL management
- Cache invalidation strategies
- Support for different cache patterns
"""

import hashlib
import json
import logging
from datetime import datetime, timedelta
from enum import Enum
from typing import Any, Dict, List, Optional, Union

try:
    import redis
    from redis.exceptions import RedisError
except ImportError:
    redis = None  # type: ignore[assignment]
    RedisError = Exception  # type: ignore[misc,assignment]

from kailash.sdk_exceptions import NodeExecutionError

logger = logging.getLogger(__name__)


class CachePattern(Enum):
    """Cache patterns for different use cases."""

    WRITE_THROUGH = "write_through"  # Update cache on write
    WRITE_BEHIND = "write_behind"  # Async cache updates
    CACHE_ASIDE = "cache_aside"  # Manual cache management
    REFRESH_AHEAD = "refresh_ahead"  # Proactive cache refresh


class CacheInvalidationStrategy(Enum):
    """Cache invalidation strategies."""

    TTL = "ttl"  # Time-based expiration
    MANUAL = "manual"  # Manual invalidation
    WRITE_THROUGH = "write_through"  # Invalidate on write
    PATTERN_BASED = "pattern_based"  # Invalidate by pattern
    EVENT_BASED = "event_based"  # Invalidate on events


class QueryCacheKey:
    """Generates cache keys for database queries."""

    def __init__(self, prefix: str = "kailash:query"):
        """Initialize cache key generator.

        Args:
            prefix: Prefix for cache keys
        """
        self.prefix = prefix

    def generate(
        self, query: str, parameters: List[Any], tenant_id: Optional[str] = None
    ) -> str:
        """Generate cache key for a query.

        Args:
            query: SQL query string
            parameters: Query parameters
            tenant_id: Optional tenant ID for multi-tenant caching

        Returns:
            Cache key string
        """
        # Create a consistent representation of the query and parameters
        query_data = {
            "query": query.strip(),
            "parameters": self._normalize_parameters(parameters),
            "tenant_id": tenant_id,
        }

        # Create hash of the query data
        query_json = json.dumps(query_data, sort_keys=True)
        query_hash = hashlib.sha256(query_json.encode()).hexdigest()[:16]

        # Build cache key
        key_parts = [self.prefix]
        if tenant_id:
            key_parts.append(f"tenant:{tenant_id}")
        key_parts.append(query_hash)

        return ":".join(key_parts)

    def generate_pattern(self, table_name: str, tenant_id: Optional[str] = None) -> str:
        """Generate cache key pattern for invalidation.

        Args:
            table_name: Database table name
            tenant_id: Optional tenant ID

        Returns:
            Cache key pattern
        """
        pattern_parts = [self.prefix]
        if tenant_id:
            pattern_parts.append(f"tenant:{tenant_id}")
        pattern_parts.append(f"table:{table_name}")
        pattern_parts.append("*")

        return ":".join(pattern_parts)

    def _normalize_parameters(self, parameters: List[Any]) -> List[Any]:
        """Normalize parameters for consistent hashing."""
        normalized = []
        for param in parameters:
            if isinstance(param, datetime):
                normalized.append(param.isoformat())
            elif isinstance(param, (dict, list)):
                normalized.append(json.dumps(param, sort_keys=True))
            else:
                normalized.append(param)
        return normalized


class QueryCache:
    """Redis-based query result cache."""

    def __init__(
        self,
        redis_host: str = "localhost",
        redis_port: int = 6379,
        redis_db: int = 0,
        redis_password: Optional[str] = None,
        default_ttl: int = 3600,
        cache_pattern: CachePattern = CachePattern.CACHE_ASIDE,
        invalidation_strategy: CacheInvalidationStrategy = CacheInvalidationStrategy.TTL,
        key_prefix: str = "kailash:query",
    ):
        """Initialize query cache.

        Args:
            redis_host: Redis server host
            redis_port: Redis server port
            redis_db: Redis database number
            redis_password: Redis password (optional)
            default_ttl: Default TTL in seconds
            cache_pattern: Cache pattern to use
            invalidation_strategy: Cache invalidation strategy
            key_prefix: Prefix for cache keys
        """
        self.redis_host = redis_host
        self.redis_port = redis_port
        self.redis_db = redis_db
        self.redis_password = redis_password
        self.default_ttl = default_ttl
        self.cache_pattern = cache_pattern
        self.invalidation_strategy = invalidation_strategy

        self.key_generator = QueryCacheKey(key_prefix)
        self._redis: Optional[redis.Redis] = None  # type: ignore[reportInvalidTypeForm]

    def _get_redis(self) -> redis.Redis:  # type: ignore[reportInvalidTypeForm]
        """Get Redis connection."""
        if self._redis is None:
            self._redis = redis.Redis(  # type: ignore[reportOptionalMemberAccess]
                host=self.redis_host,
                port=self.redis_port,
                db=self.redis_db,
                password=self.redis_password,
                decode_responses=True,
            )
        return self._redis

    def get(
        self, query: str, parameters: List[Any], tenant_id: Optional[str] = None
    ) -> Optional[Dict[str, Any]]:
        """Get cached query result.

        Args:
            query: SQL query string
            parameters: Query parameters
            tenant_id: Optional tenant ID

        Returns:
            Cached result or None if not found
        """
        try:
            redis_client = self._get_redis()
            cache_key = self.key_generator.generate(query, parameters, tenant_id)

            cached_data = redis_client.get(cache_key)
            if cached_data:
                logger.debug(f"Cache hit for key: {cache_key}")
                return json.loads(cached_data)
            else:
                logger.debug(f"Cache miss for key: {cache_key}")
                return None

        except (RedisError, json.JSONDecodeError) as e:
            logger.warning(f"Cache get error: {e}")
            return None

    def set(
        self,
        query: str,
        parameters: List[Any],
        result: Dict[str, Any],
        tenant_id: Optional[str] = None,
        ttl: Optional[int] = None,
    ) -> bool:
        """Set cached query result.

        Args:
            query: SQL query string
            parameters: Query parameters
            result: Query result to cache
            tenant_id: Optional tenant ID
            ttl: TTL in seconds (uses default if not specified)

        Returns:
            True if cached successfully, False otherwise
        """
        try:
            redis_client = self._get_redis()
            cache_key = self.key_generator.generate(query, parameters, tenant_id)

            # Prepare cache data
            cache_data = {
                "result": result,
                "cached_at": datetime.now().isoformat(),
                "query_hash": hashlib.sha256(query.encode()).hexdigest()[:8],
            }

            # Set with TTL
            actual_ttl = ttl or self.default_ttl
            success = redis_client.setex(cache_key, actual_ttl, json.dumps(cache_data))

            if success:
                logger.debug(f"Cache set for key: {cache_key} (TTL: {actual_ttl}s)")

                # Add to table-based index for pattern invalidation
                if (
                    self.invalidation_strategy
                    == CacheInvalidationStrategy.PATTERN_BASED
                ):
                    self._add_to_table_index(query, cache_key, tenant_id)

            return success

        except (RedisError, TypeError, ValueError) as e:
            logger.warning(f"Cache set error: {e}")
            return False

    def invalidate(
        self, query: str, parameters: List[Any], tenant_id: Optional[str] = None
    ) -> bool:
        """Invalidate specific cached query.

        Args:
            query: SQL query string
            parameters: Query parameters
            tenant_id: Optional tenant ID

        Returns:
            True if invalidated successfully, False otherwise
        """
        try:
            redis_client = self._get_redis()
            cache_key = self.key_generator.generate(query, parameters, tenant_id)

            deleted = redis_client.delete(cache_key)
            if deleted:
                logger.debug(f"Cache invalidated for key: {cache_key}")

            return deleted > 0

        except RedisError as e:
            logger.warning(f"Cache invalidation error: {e}")
            return False

    def invalidate_table(self, table_name: str, tenant_id: Optional[str] = None) -> int:
        """Invalidate all cached queries for a table.

        Args:
            table_name: Database table name
            tenant_id: Optional tenant ID

        Returns:
            Number of keys invalidated
        """
        try:
            redis_client = self._get_redis()

            if self.invalidation_strategy == CacheInvalidationStrategy.PATTERN_BASED:
                # Use index-based invalidation for pattern-based strategy
                index_key = f"{self.key_generator.prefix}:index:table:{table_name}"
                if tenant_id:
                    index_key += f":tenant:{tenant_id}"

                keys = redis_client.smembers(index_key)
                if keys:
                    # Delete the actual cache keys
                    deleted = redis_client.delete(*keys)
                    # Also delete the index
                    redis_client.delete(index_key)
                    logger.debug(
                        f"Cache invalidated {deleted} keys for table: {table_name}"
                    )
                    return deleted
                else:
                    return 0
            else:
                # Use pattern-based invalidation for other strategies
                pattern = self.key_generator.generate_pattern(table_name, tenant_id)
                keys = redis_client.keys(pattern)
                if keys:
                    deleted = redis_client.delete(*keys)
                    logger.debug(
                        f"Cache invalidated {deleted} keys for table: {table_name}"
                    )
                    return deleted
                else:
                    return 0

        except RedisError as e:
            logger.warning(f"Cache table invalidation error: {e}")
            return 0

    def clear_all(self, tenant_id: Optional[str] = None) -> int:
        """Clear all cached queries for a tenant or globally.

        Args:
            tenant_id: Optional tenant ID (clears all if None)

        Returns:
            Number of keys cleared
        """
        try:
            redis_client = self._get_redis()

            if tenant_id:
                pattern = f"{self.key_generator.prefix}:tenant:{tenant_id}:*"
            else:
                pattern = f"{self.key_generator.prefix}:*"

            keys = redis_client.keys(pattern)
            if keys:
                deleted = redis_client.delete(*keys)
                logger.info(f"Cache cleared {deleted} keys for tenant: {tenant_id}")
                return deleted
            else:
                return 0

        except RedisError as e:
            logger.warning(f"Cache clear error: {e}")
            return 0

    def get_stats(self) -> Dict[str, Any]:
        """Get cache statistics.

        Returns:
            Dictionary with cache statistics
        """
        try:
            redis_client = self._get_redis()

            # Get Redis info
            info = redis_client.info()

            # Count our keys
            pattern = f"{self.key_generator.prefix}:*"
            keys = redis_client.keys(pattern)

            stats = {
                "total_keys": len(keys),
                "redis_memory_used": info.get("used_memory_human", "unknown"),
                "redis_connected_clients": info.get("connected_clients", 0),
                "redis_keyspace_hits": info.get("keyspace_hits", 0),
                "redis_keyspace_misses": info.get("keyspace_misses", 0),
                "cache_pattern": self.cache_pattern.value,
                "invalidation_strategy": self.invalidation_strategy.value,
                "default_ttl": self.default_ttl,
            }

            # Calculate hit rate
            hits = stats["redis_keyspace_hits"]
            misses = stats["redis_keyspace_misses"]
            if hits + misses > 0:
                stats["hit_rate"] = hits / (hits + misses)
            else:
                stats["hit_rate"] = 0.0

            return stats

        except RedisError as e:
            logger.warning(f"Cache stats error: {e}")
            return {"error": str(e), "total_keys": 0, "hit_rate": 0.0}

    def health_check(self) -> Dict[str, Any]:
        """Check cache health.

        Returns:
            Dictionary with health status
        """
        try:
            redis_client = self._get_redis()

            # Try to ping Redis
            pong = redis_client.ping()
            if pong:
                # Test basic operations
                test_key = f"{self.key_generator.prefix}:health_check"
                redis_client.setex(test_key, 10, "test")
                test_value = redis_client.get(test_key)
                redis_client.delete(test_key)

                return {
                    "status": "healthy",
                    "redis_ping": True,
                    "read_write_test": test_value == "test",
                    "connection": "active",
                }
            else:
                return {
                    "status": "unhealthy",
                    "redis_ping": False,
                    "error": "Redis ping failed",
                }

        except RedisError as e:
            return {"status": "unhealthy", "redis_ping": False, "error": str(e)}

    def _add_to_table_index(
        self, query: str, cache_key: str, tenant_id: Optional[str] = None
    ) -> None:
        """Add cache key to table-based index for pattern invalidation."""
        try:
            # Extract table name from query (simple heuristic)
            table_name = self._extract_table_name(query)
            if table_name:
                redis_client = self._get_redis()
                index_key = f"{self.key_generator.prefix}:index:table:{table_name}"
                if tenant_id:
                    index_key += f":tenant:{tenant_id}"

                redis_client.sadd(index_key, cache_key)
                redis_client.expire(
                    index_key, self.default_ttl * 2
                )  # Index lives longer

        except Exception as e:
            logger.warning(f"Failed to add to table index: {e}")

    def _extract_table_name(self, query: str) -> Optional[str]:
        """Extract table name from SQL query (simple heuristic)."""
        try:
            query_lower = query.lower().strip()

            # Handle SELECT queries
            if query_lower.startswith("select"):
                from_index = query_lower.find("from")
                if from_index != -1:
                    from_part = query_lower[from_index + 4 :].strip()
                    table_name = from_part.split()[0].strip()
                    return table_name

            # Handle INSERT queries
            elif query_lower.startswith("insert into"):
                into_part = query_lower[11:].strip()
                table_name = into_part.split()[0].strip()
                return table_name

            # Handle UPDATE queries
            elif query_lower.startswith("update"):
                update_part = query_lower[6:].strip()
                table_name = update_part.split()[0].strip()
                return table_name

            # Handle DELETE queries
            elif query_lower.startswith("delete from"):
                from_part = query_lower[11:].strip()
                table_name = from_part.split()[0].strip()
                return table_name

            return None

        except Exception:
            return None


# Factory function for creating query cache
def create_query_cache(config: Dict[str, Any] | None = None) -> QueryCache:
    """Create a query cache instance with configuration.

    Args:
        config: Configuration dictionary

    Returns:
        QueryCache instance
    """
    if config is None:
        config = {}

    return QueryCache(
        redis_host=config.get("redis_host", "localhost"),
        redis_port=config.get("redis_port", 6379),
        redis_db=config.get("redis_db", 0),
        redis_password=config.get("redis_password"),
        default_ttl=config.get("default_ttl", 3600),
        cache_pattern=CachePattern(config.get("cache_pattern", "cache_aside")),
        invalidation_strategy=CacheInvalidationStrategy(
            config.get("invalidation_strategy", "ttl")
        ),
        key_prefix=config.get("key_prefix", "kailash:query"),
    )
