# app/services/memory/cache/hot_cache.py
"""
Hot Memory Cache.

LRU cache for frequently accessed memories.
Provides fast access to recently used memories without database queries.
"""

import logging
import threading
from collections import OrderedDict
from dataclasses import dataclass
from datetime import UTC, datetime
from typing import Any
from uuid import UUID

logger = logging.getLogger(__name__)


def _utcnow() -> datetime:
    """Get current UTC time as timezone-aware datetime."""
    return datetime.now(UTC)


@dataclass
class CacheEntry[T]:
    """A cached memory entry with metadata."""

    value: T
    created_at: datetime
    last_accessed_at: datetime
    access_count: int = 1
    ttl_seconds: float = 300.0

    def is_expired(self) -> bool:
        """Check if this entry has expired."""
        age = (_utcnow() - self.created_at).total_seconds()
        return age > self.ttl_seconds

    def touch(self) -> None:
        """Update access time and count."""
        self.last_accessed_at = _utcnow()
        self.access_count += 1


@dataclass
class CacheKey:
    """A structured cache key with components."""

    memory_type: str
    memory_id: str
    scope: str | None = None

    def __hash__(self) -> int:
        return hash((self.memory_type, self.memory_id, self.scope))

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, CacheKey):
            return False
        return (
            self.memory_type == other.memory_type
            and self.memory_id == other.memory_id
            and self.scope == other.scope
        )

    def __str__(self) -> str:
        if self.scope:
            return f"{self.memory_type}:{self.scope}:{self.memory_id}"
        return f"{self.memory_type}:{self.memory_id}"


@dataclass
class HotCacheStats:
    """Statistics for the hot memory cache."""

    hits: int = 0
    misses: int = 0
    evictions: int = 0
    expirations: int = 0
    current_size: int = 0
    max_size: int = 0

    @property
    def hit_rate(self) -> float:
        """Calculate cache hit rate."""
        total = self.hits + self.misses
        if total == 0:
            return 0.0
        return self.hits / total

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary."""
        return {
            "hits": self.hits,
            "misses": self.misses,
            "evictions": self.evictions,
            "expirations": self.expirations,
            "current_size": self.current_size,
            "max_size": self.max_size,
            "hit_rate": self.hit_rate,
        }


class HotMemoryCache[T]:
    """
    LRU cache for frequently accessed memories.

    Features:
    - LRU eviction when capacity is reached
    - TTL-based expiration
    - Access count tracking for hot memory identification
    - Thread-safe operations
    - Scoped invalidation

    Performance targets:
    - Cache hit rate > 80% for hot memories
    - Get/put operations < 1ms
    """

    def __init__(
        self,
        max_size: int = 10000,
        default_ttl_seconds: float = 300.0,
    ) -> None:
        """
        Initialize the hot memory cache.

        Args:
            max_size: Maximum number of entries
            default_ttl_seconds: Default TTL for entries (5 minutes)
        """
        self._max_size = max_size
        self._default_ttl = default_ttl_seconds
        self._cache: OrderedDict[CacheKey, CacheEntry[T]] = OrderedDict()
        self._lock = threading.RLock()
        self._stats = HotCacheStats(max_size=max_size)
        logger.info(
            f"Initialized HotMemoryCache with max_size={max_size}, "
            f"ttl={default_ttl_seconds}s"
        )

    def get(self, key: CacheKey) -> T | None:
        """
        Get a memory from cache.

        Args:
            key: Cache key

        Returns:
            Cached value or None if not found/expired
        """
        with self._lock:
            if key not in self._cache:
                self._stats.misses += 1
                return None

            entry = self._cache[key]

            # Check expiration
            if entry.is_expired():
                del self._cache[key]
                self._stats.expirations += 1
                self._stats.misses += 1
                self._stats.current_size = len(self._cache)
                return None

            # Move to end (most recently used)
            self._cache.move_to_end(key)
            entry.touch()

            self._stats.hits += 1
            return entry.value

    def get_by_id(
        self,
        memory_type: str,
        memory_id: UUID | str,
        scope: str | None = None,
    ) -> T | None:
        """
        Get a memory by type and ID.

        Args:
            memory_type: Type of memory (episodic, semantic, procedural)
            memory_id: Memory ID
            scope: Optional scope (project_id, agent_id)

        Returns:
            Cached value or None if not found/expired
        """
        key = CacheKey(
            memory_type=memory_type,
            memory_id=str(memory_id),
            scope=scope,
        )
        return self.get(key)

    def put(
        self,
        key: CacheKey,
        value: T,
        ttl_seconds: float | None = None,
    ) -> None:
        """
        Put a memory into cache.

        Args:
            key: Cache key
            value: Value to cache
            ttl_seconds: Optional TTL override
        """
        with self._lock:
            # Evict if at capacity
            self._evict_if_needed()

            now = _utcnow()
            entry = CacheEntry(
                value=value,
                created_at=now,
                last_accessed_at=now,
                access_count=1,
                ttl_seconds=ttl_seconds or self._default_ttl,
            )

            self._cache[key] = entry
            self._cache.move_to_end(key)
            self._stats.current_size = len(self._cache)

    def put_by_id(
        self,
        memory_type: str,
        memory_id: UUID | str,
        value: T,
        scope: str | None = None,
        ttl_seconds: float | None = None,
    ) -> None:
        """
        Put a memory by type and ID.

        Args:
            memory_type: Type of memory
            memory_id: Memory ID
            value: Value to cache
            scope: Optional scope
            ttl_seconds: Optional TTL override
        """
        key = CacheKey(
            memory_type=memory_type,
            memory_id=str(memory_id),
            scope=scope,
        )
        self.put(key, value, ttl_seconds)

    def _evict_if_needed(self) -> None:
        """Evict entries if cache is at capacity."""
        while len(self._cache) >= self._max_size:
            # Remove least recently used (first item)
            if self._cache:
                self._cache.popitem(last=False)
                self._stats.evictions += 1

    def invalidate(self, key: CacheKey) -> bool:
        """
        Invalidate a specific cache entry.

        Args:
            key: Cache key to invalidate

        Returns:
            True if entry was found and removed
        """
        with self._lock:
            if key in self._cache:
                del self._cache[key]
                self._stats.current_size = len(self._cache)
                return True
            return False

    def invalidate_by_id(
        self,
        memory_type: str,
        memory_id: UUID | str,
        scope: str | None = None,
    ) -> bool:
        """
        Invalidate a memory by type and ID.

        Args:
            memory_type: Type of memory
            memory_id: Memory ID
            scope: Optional scope

        Returns:
            True if entry was found and removed
        """
        key = CacheKey(
            memory_type=memory_type,
            memory_id=str(memory_id),
            scope=scope,
        )
        return self.invalidate(key)

    def invalidate_by_type(self, memory_type: str) -> int:
        """
        Invalidate all entries of a memory type.

        Args:
            memory_type: Type of memory to invalidate

        Returns:
            Number of entries invalidated
        """
        with self._lock:
            keys_to_remove = [
                k for k in self._cache.keys() if k.memory_type == memory_type
            ]
            for key in keys_to_remove:
                del self._cache[key]

            self._stats.current_size = len(self._cache)
            return len(keys_to_remove)

    def invalidate_by_scope(self, scope: str) -> int:
        """
        Invalidate all entries in a scope.

        Args:
            scope: Scope to invalidate (e.g., project_id)

        Returns:
            Number of entries invalidated
        """
        with self._lock:
            keys_to_remove = [k for k in self._cache.keys() if k.scope == scope]
            for key in keys_to_remove:
                del self._cache[key]

            self._stats.current_size = len(self._cache)
            return len(keys_to_remove)

    def invalidate_pattern(self, pattern: str) -> int:
        """
        Invalidate entries matching a pattern.

        Pattern can include * as wildcard.

        Args:
            pattern: Pattern to match (e.g., "episodic:*")

        Returns:
            Number of entries invalidated
        """
        import fnmatch

        with self._lock:
            keys_to_remove = [
                k for k in self._cache.keys() if fnmatch.fnmatch(str(k), pattern)
            ]
            for key in keys_to_remove:
                del self._cache[key]

            self._stats.current_size = len(self._cache)
            return len(keys_to_remove)

    def clear(self) -> int:
        """
        Clear all cache entries.

        Returns:
            Number of entries cleared
        """
        with self._lock:
            count = len(self._cache)
            self._cache.clear()
            self._stats.current_size = 0
            logger.info(f"Cleared {count} entries from hot cache")
            return count

    def cleanup_expired(self) -> int:
        """
        Remove all expired entries.

        Returns:
            Number of entries removed
        """
        with self._lock:
            keys_to_remove = [k for k, v in self._cache.items() if v.is_expired()]
            for key in keys_to_remove:
                del self._cache[key]
                self._stats.expirations += 1

            self._stats.current_size = len(self._cache)

            if keys_to_remove:
                logger.debug(f"Cleaned up {len(keys_to_remove)} expired entries")

            return len(keys_to_remove)

    def get_hot_memories(self, limit: int = 10) -> list[tuple[CacheKey, int]]:
        """
        Get the most frequently accessed memories.

        Args:
            limit: Maximum number of memories to return

        Returns:
            List of (key, access_count) tuples sorted by access count
        """
        with self._lock:
            entries = [
                (k, v.access_count)
                for k, v in self._cache.items()
                if not v.is_expired()
            ]
            entries.sort(key=lambda x: x[1], reverse=True)
            return entries[:limit]

    def get_stats(self) -> HotCacheStats:
        """Get cache statistics."""
        with self._lock:
            self._stats.current_size = len(self._cache)
            return self._stats

    def reset_stats(self) -> None:
        """Reset cache statistics."""
        with self._lock:
            self._stats = HotCacheStats(
                max_size=self._max_size,
                current_size=len(self._cache),
            )

    @property
    def size(self) -> int:
        """Get current cache size."""
        return len(self._cache)

    @property
    def max_size(self) -> int:
        """Get maximum cache size."""
        return self._max_size


# Factory function for typed caches
def create_hot_cache(
    max_size: int = 10000,
    default_ttl_seconds: float = 300.0,
) -> HotMemoryCache[Any]:
    """
    Create a hot memory cache.

    Args:
        max_size: Maximum number of entries
        default_ttl_seconds: Default TTL for entries

    Returns:
        Configured HotMemoryCache instance
    """
    return HotMemoryCache(
        max_size=max_size,
        default_ttl_seconds=default_ttl_seconds,
    )
