# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
Familiar Caching Module (Phase 4 - v2.5.0)

Provides performance optimization through intelligent caching:
- LRU cache for sessions with configurable size
- Background persistence for dirty sessions
- Automatic cleanup of stale data
- Pattern compilation cache
- Conversation history management

Usage:
    from familiar.core.caching import (
        LRUCache, SessionCache, PatternCache,
        get_session_cache, get_pattern_cache,
    )

    # LRU cache with size limit
    cache = LRUCache(max_size=1000)
    cache.set("key", value)
    value = cache.get("key")

    # Session cache with background persistence
    session_cache = get_session_cache()
    session = session_cache.get_or_create("user123", "telegram")
"""

import atexit
import hashlib
import logging
import re
import threading
import time
from collections import OrderedDict
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, Generic, List, Optional, Tuple, TypeVar

logger = logging.getLogger(__name__)

T = TypeVar("T")
K = TypeVar("K")
V = TypeVar("V")


# =============================================================================
# LRU CACHE IMPLEMENTATION
# =============================================================================


class LRUCache(Generic[K, V]):
    """
    Thread-safe LRU (Least Recently Used) cache.

    Features:
    - O(1) get/set operations
    - Automatic eviction of least recently used items
    - TTL support for automatic expiration
    - Hit/miss statistics
    - Thread-safe operations

    Usage:
        cache = LRUCache(max_size=100, ttl_seconds=3600)
        cache.set("key", "value")
        value = cache.get("key")  # Returns "value"
        cache.get("missing")  # Returns None
    """

    def __init__(
        self,
        max_size: int = 1000,
        ttl_seconds: Optional[float] = None,
        on_evict: Optional[Callable[[K, V], None]] = None,
    ):
        """
        Initialize LRU cache.

        Args:
            max_size: Maximum number of items to store
            ttl_seconds: Time-to-live for items (None = no expiration)
            on_evict: Callback when items are evicted (for cleanup)
        """
        self.max_size = max_size
        self.ttl_seconds = ttl_seconds
        self.on_evict = on_evict

        self._cache: OrderedDict[K, Tuple[V, float]] = OrderedDict()
        self._lock = threading.RLock()

        # Statistics
        self._hits = 0
        self._misses = 0
        self._evictions = 0

    def get(self, key: K, default: Optional[V] = None) -> Optional[V]:
        """
        Get item from cache.

        Returns None (or default) if not found or expired.
        Moves accessed item to end (most recently used).
        """
        with self._lock:
            if key not in self._cache:
                self._misses += 1
                return default

            value, timestamp = self._cache[key]

            # Check TTL
            if self.ttl_seconds and (time.time() - timestamp) > self.ttl_seconds:
                self._remove(key)
                self._misses += 1
                return default

            # Move to end (most recently used)
            self._cache.move_to_end(key)
            self._hits += 1
            return value

    def set(self, key: K, value: V) -> None:
        """
        Set item in cache.

        If at capacity, evicts least recently used item.
        """
        with self._lock:
            now = time.time()

            if key in self._cache:
                # Update existing
                self._cache[key] = (value, now)
                self._cache.move_to_end(key)
            else:
                # Check capacity
                while len(self._cache) >= self.max_size:
                    self._evict_oldest()

                self._cache[key] = (value, now)

    def delete(self, key: K) -> bool:
        """Remove item from cache. Returns True if item existed."""
        with self._lock:
            return self._remove(key)

    def clear(self) -> None:
        """Clear all items from cache."""
        with self._lock:
            if self.on_evict:
                for key, (value, _) in self._cache.items():
                    try:
                        self.on_evict(key, value)
                    except Exception as e:
                        logger.warning(f"Eviction callback failed: {e}")
            self._cache.clear()

    def contains(self, key: K) -> bool:
        """Check if key exists (without updating access time)."""
        with self._lock:
            if key not in self._cache:
                return False

            # Check TTL without moving
            if self.ttl_seconds:
                _, timestamp = self._cache[key]
                if (time.time() - timestamp) > self.ttl_seconds:
                    self._remove(key)
                    return False

            return True

    def keys(self) -> List[K]:
        """Get all valid keys (excluding expired)."""
        with self._lock:
            self._cleanup_expired()
            return list(self._cache.keys())

    def values(self) -> List[V]:
        """Get all valid values (excluding expired)."""
        with self._lock:
            self._cleanup_expired()
            return [v for v, _ in self._cache.values()]

    def items(self) -> List[Tuple[K, V]]:
        """Get all valid key-value pairs."""
        with self._lock:
            self._cleanup_expired()
            return [(k, v) for k, (v, _) in self._cache.items()]

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

    def stats(self) -> Dict[str, Any]:
        """Get cache statistics."""
        with self._lock:
            total = self._hits + self._misses
            hit_rate = self._hits / total if total > 0 else 0.0

            return {
                "size": len(self._cache),
                "max_size": self.max_size,
                "hits": self._hits,
                "misses": self._misses,
                "evictions": self._evictions,
                "hit_rate": round(hit_rate, 4),
                "ttl_seconds": self.ttl_seconds,
            }

    def _remove(self, key: K) -> bool:
        """Internal remove with eviction callback."""
        if key in self._cache:
            value, _ = self._cache.pop(key)
            if self.on_evict:
                try:
                    self.on_evict(key, value)
                except Exception as e:
                    logger.warning(f"Eviction callback failed: {e}")
            return True
        return False

    def _evict_oldest(self) -> None:
        """Evict the least recently used item."""
        if self._cache:
            key, (value, _) = self._cache.popitem(last=False)
            self._evictions += 1
            if self.on_evict:
                try:
                    self.on_evict(key, value)
                except Exception as e:
                    logger.warning(f"Eviction callback failed: {e}")

    def _cleanup_expired(self) -> None:
        """Remove all expired items."""
        if not self.ttl_seconds:
            return

        now = time.time()
        expired = [
            key
            for key, (_, timestamp) in self._cache.items()
            if (now - timestamp) > self.ttl_seconds
        ]

        for key in expired:
            self._remove(key)


# =============================================================================
# SESSION CACHE
# =============================================================================


@dataclass
class CachedSession:
    """Wrapper for cached session with metadata."""

    session: Any  # SecureSession
    dirty: bool = False
    last_access: float = field(default_factory=time.time)
    last_save: float = field(default_factory=time.time)
    access_count: int = 0


class SessionCache:
    """
    Specialized cache for user sessions with background persistence.

    Features:
    - LRU eviction with configurable size
    - Background persistence of dirty sessions
    - Automatic save on eviction
    - Stale session cleanup
    - Statistics tracking

    Usage:
        cache = SessionCache(max_size=500)
        session = cache.get_or_create("user123", "telegram", session_factory)
        cache.mark_dirty("user123", "telegram")
        cache.start_background_persistence()  # Auto-save dirty sessions
    """

    def __init__(
        self,
        max_size: int = 500,
        persist_interval_seconds: float = 60.0,
        stale_threshold_seconds: float = 3600.0,  # 1 hour
    ):
        self.max_size = max_size
        self.persist_interval = persist_interval_seconds
        self.stale_threshold = stale_threshold_seconds

        self._cache: Dict[str, CachedSession] = {}
        self._access_order: OrderedDict[str, float] = OrderedDict()
        self._lock = threading.RLock()

        # Background persistence
        self._persist_thread: Optional[threading.Thread] = None
        self._stop_event = threading.Event()
        self._executor = ThreadPoolExecutor(max_workers=2, thread_name_prefix="session_persist")

        # Statistics
        self._hits = 0
        self._misses = 0
        self._evictions = 0
        self._background_saves = 0

        # Register cleanup on exit
        atexit.register(self.shutdown)

    def _session_key(self, user_id: str, channel: str) -> str:
        """Generate cache key for session."""
        return f"{channel}:{user_id}"

    def get(self, user_id: str, channel: str) -> Optional[Any]:
        """
        Get session from cache.

        Returns None if not found. Updates access time.
        """
        key = self._session_key(user_id, channel)

        with self._lock:
            if key not in self._cache:
                self._misses += 1
                return None

            cached = self._cache[key]
            cached.last_access = time.time()
            cached.access_count += 1

            # Update access order
            self._access_order.move_to_end(key)

            self._hits += 1
            return cached.session

    def get_or_create(
        self,
        user_id: str,
        channel: str,
        factory: Callable[[], Any],
        loader: Optional[Callable[[], Optional[Any]]] = None,
    ) -> Any:
        """
        Get session from cache, or create/load if not present.

        Args:
            user_id: User identifier
            channel: Channel identifier
            factory: Function to create new session
            loader: Optional function to load existing session from disk

        Returns:
            Session object
        """
        key = self._session_key(user_id, channel)

        with self._lock:
            # Check cache
            if key in self._cache:
                cached = self._cache[key]
                cached.last_access = time.time()
                cached.access_count += 1
                self._access_order.move_to_end(key)
                self._hits += 1
                return cached.session

            self._misses += 1

            # Try to load from disk
            session = None
            if loader:
                try:
                    session = loader()
                except Exception as e:
                    logger.warning(f"Failed to load session: {e}")

            # Create new if not loaded
            if session is None:
                session = factory()

            # Evict if at capacity
            while len(self._cache) >= self.max_size:
                self._evict_oldest()

            # Add to cache
            now = time.time()
            self._cache[key] = CachedSession(
                session=session,
                dirty=session is None,  # New sessions are dirty
                last_access=now,
                last_save=now,
            )
            self._access_order[key] = now

            return session

    def put(self, user_id: str, channel: str, session: Any, dirty: bool = True) -> None:
        """
        Put session in cache.

        Args:
            user_id: User identifier
            channel: Channel identifier
            session: Session object
            dirty: Whether session needs saving
        """
        key = self._session_key(user_id, channel)

        with self._lock:
            # Evict if at capacity and not updating existing
            if key not in self._cache:
                while len(self._cache) >= self.max_size:
                    self._evict_oldest()

            now = time.time()
            self._cache[key] = CachedSession(
                session=session,
                dirty=dirty,
                last_access=now,
                last_save=now if not dirty else 0,
            )
            self._access_order[key] = now

    def mark_dirty(self, user_id: str, channel: str) -> bool:
        """Mark session as needing persistence."""
        key = self._session_key(user_id, channel)

        with self._lock:
            if key in self._cache:
                self._cache[key].dirty = True
                return True
            return False

    def mark_clean(self, user_id: str, channel: str) -> bool:
        """Mark session as persisted."""
        key = self._session_key(user_id, channel)

        with self._lock:
            if key in self._cache:
                self._cache[key].dirty = False
                self._cache[key].last_save = time.time()
                return True
            return False

    def remove(self, user_id: str, channel: str, save: bool = True) -> bool:
        """
        Remove session from cache.

        Args:
            save: If True, saves dirty session before removal
        """
        key = self._session_key(user_id, channel)

        with self._lock:
            if key not in self._cache:
                return False

            cached = self._cache.pop(key)
            self._access_order.pop(key, None)

            # Save if dirty
            if save and cached.dirty and hasattr(cached.session, "save"):
                try:
                    cached.session.save()
                except Exception as e:
                    logger.error(f"Failed to save session on removal: {e}")

            return True

    def _evict_oldest(self) -> None:
        """Evict least recently used session."""
        if not self._access_order:
            return

        # Get oldest key
        key = next(iter(self._access_order))

        if key in self._cache:
            cached = self._cache.pop(key)
            self._access_order.pop(key, None)
            self._evictions += 1

            # Save if dirty
            if cached.dirty and hasattr(cached.session, "save"):
                try:
                    cached.session.save()
                    self._background_saves += 1
                except Exception as e:
                    logger.error(f"Failed to save evicted session: {e}")

    # -------------------------------------------------------------------------
    # BACKGROUND PERSISTENCE
    # -------------------------------------------------------------------------

    def start_background_persistence(self) -> None:
        """Start background thread for periodic session persistence."""
        if self._persist_thread and self._persist_thread.is_alive():
            return

        self._stop_event.clear()
        self._persist_thread = threading.Thread(
            target=self._persist_loop,
            name="SessionPersistence",
            daemon=True,
        )
        self._persist_thread.start()
        logger.info("Started background session persistence")

    def stop_background_persistence(self) -> None:
        """Stop background persistence thread."""
        self._stop_event.set()
        if self._persist_thread:
            self._persist_thread.join(timeout=5.0)
            self._persist_thread = None
        logger.info("Stopped background session persistence")

    def _persist_loop(self) -> None:
        """Background loop for periodic persistence."""
        while not self._stop_event.wait(timeout=self.persist_interval):
            try:
                self._persist_dirty_sessions()
                self._cleanup_stale_sessions()
            except Exception as e:
                logger.error(f"Background persistence error: {e}")

    def _persist_dirty_sessions(self) -> None:
        """Save all dirty sessions."""
        with self._lock:
            dirty_sessions = [
                (key, cached.session)
                for key, cached in self._cache.items()
                if cached.dirty and hasattr(cached.session, "save")
            ]

        for key, session in dirty_sessions:
            try:
                session.save()
                with self._lock:
                    if key in self._cache:
                        self._cache[key].dirty = False
                        self._cache[key].last_save = time.time()
                self._background_saves += 1
            except Exception as e:
                logger.error(f"Failed to persist session {key}: {e}")

    def _cleanup_stale_sessions(self) -> None:
        """Remove sessions that haven't been accessed recently."""
        now = time.time()

        with self._lock:
            stale_keys = [
                key
                for key, cached in self._cache.items()
                if (now - cached.last_access) > self.stale_threshold
            ]

        for key in stale_keys:
            with self._lock:
                if key in self._cache:
                    cached = self._cache.pop(key)
                    self._access_order.pop(key, None)

                    # Save if dirty
                    if cached.dirty and hasattr(cached.session, "save"):
                        try:
                            cached.session.save()
                        except Exception as e:
                            logger.debug("Error suppressed: %s", e)
    def shutdown(self) -> None:
        """Shutdown cache, saving all dirty sessions."""
        self.stop_background_persistence()
        self.save_all()
        self._executor.shutdown(wait=True)

    def save_all(self) -> int:
        """Save all dirty sessions. Returns count saved."""
        saved = 0

        with self._lock:
            for key, cached in self._cache.items():
                if cached.dirty and hasattr(cached.session, "save"):
                    try:
                        cached.session.save()
                        cached.dirty = False
                        cached.last_save = time.time()
                        saved += 1
                    except Exception as e:
                        logger.error(f"Failed to save session {key}: {e}")

        return saved

    # -------------------------------------------------------------------------
    # STATISTICS
    # -------------------------------------------------------------------------

    def stats(self) -> Dict[str, Any]:
        """Get cache statistics."""
        with self._lock:
            total = self._hits + self._misses
            hit_rate = self._hits / total if total > 0 else 0.0
            dirty_count = sum(1 for c in self._cache.values() if c.dirty)

            return {
                "size": len(self._cache),
                "max_size": self.max_size,
                "hits": self._hits,
                "misses": self._misses,
                "evictions": self._evictions,
                "hit_rate": round(hit_rate, 4),
                "dirty_count": dirty_count,
                "background_saves": self._background_saves,
                "persist_interval": self.persist_interval,
                "stale_threshold": self.stale_threshold,
            }

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


# =============================================================================
# PATTERN CACHE
# =============================================================================


class PatternCache:
    r"""
    Cache for compiled regex patterns.

    Features:
    - Pre-compilation of frequently used patterns
    - Lazy compilation for on-demand patterns
    - LRU eviction for dynamic patterns
    - Pattern validation
    - Statistics tracking

    Usage:
        cache = PatternCache()
        pattern = cache.get_compiled(r"\d{3}-\d{4}")
        match = pattern.search("123-4567")
    """

    def __init__(
        self,
        max_dynamic_patterns: int = 500,
        precompile_patterns: Optional[Dict[str, str]] = None,
    ):
        """
        Initialize pattern cache.

        Args:
            max_dynamic_patterns: Max patterns to cache dynamically
            precompile_patterns: Dict of name -> pattern to pre-compile
        """
        self.max_dynamic = max_dynamic_patterns

        # Pre-compiled patterns (never evicted)
        self._static: Dict[str, re.Pattern] = {}

        # Dynamic LRU cache
        self._dynamic: OrderedDict[str, re.Pattern] = OrderedDict()
        self._lock = threading.RLock()

        # Statistics
        self._hits = 0
        self._misses = 0
        self._compilations = 0
        self._errors = 0

        # Pre-compile if provided
        if precompile_patterns:
            for name, pattern in precompile_patterns.items():
                try:
                    self._static[name] = re.compile(pattern)
                    self._compilations += 1
                except re.error as e:
                    logger.warning(f"Failed to pre-compile pattern '{name}': {e}")
                    self._errors += 1

    def get_compiled(
        self,
        pattern: str,
        flags: int = 0,
        name: Optional[str] = None,
    ) -> Optional[re.Pattern]:
        """
        Get compiled pattern, using cache if available.

        Args:
            pattern: Regex pattern string
            flags: Regex flags (re.IGNORECASE, etc.)
            name: Optional name for static caching

        Returns:
            Compiled pattern or None if invalid
        """
        # Create cache key
        cache_key = f"{pattern}:{flags}"

        with self._lock:
            # Check static cache by name
            if name and name in self._static:
                self._hits += 1
                return self._static[name]

            # Check dynamic cache
            if cache_key in self._dynamic:
                self._dynamic.move_to_end(cache_key)
                self._hits += 1
                return self._dynamic[cache_key]

            self._misses += 1

            # Compile pattern
            try:
                compiled = re.compile(pattern, flags)
                self._compilations += 1

                # Add to static or dynamic cache
                if name:
                    self._static[name] = compiled
                else:
                    # Evict if at capacity
                    while len(self._dynamic) >= self.max_dynamic:
                        self._dynamic.popitem(last=False)

                    self._dynamic[cache_key] = compiled

                return compiled

            except re.error as e:
                logger.warning(f"Invalid regex pattern: {e}")
                self._errors += 1
                return None

    def get_static(self, name: str) -> Optional[re.Pattern]:
        """Get pre-compiled static pattern by name."""
        with self._lock:
            if name in self._static:
                self._hits += 1
                return self._static[name]
            self._misses += 1
            return None

    def add_static(self, name: str, pattern: str, flags: int = 0) -> bool:
        """
        Add a pattern to static cache (never evicted).

        Returns True if successfully compiled and added.
        """
        with self._lock:
            try:
                self._static[name] = re.compile(pattern, flags)
                self._compilations += 1
                return True
            except re.error as e:
                logger.warning(f"Failed to compile pattern '{name}': {e}")
                self._errors += 1
                return False

    def remove_static(self, name: str) -> bool:
        """Remove pattern from static cache."""
        with self._lock:
            if name in self._static:
                del self._static[name]
                return True
            return False

    def clear_dynamic(self) -> None:
        """Clear dynamic pattern cache."""
        with self._lock:
            self._dynamic.clear()

    def validate_pattern(self, pattern: str) -> Tuple[bool, Optional[str]]:
        """
        Validate a regex pattern without caching.

        Returns (is_valid, error_message)
        """
        try:
            re.compile(pattern)
            return True, None
        except re.error as e:
            return False, str(e)

    def stats(self) -> Dict[str, Any]:
        """Get cache statistics."""
        with self._lock:
            total = self._hits + self._misses
            hit_rate = self._hits / total if total > 0 else 0.0

            return {
                "static_count": len(self._static),
                "dynamic_count": len(self._dynamic),
                "max_dynamic": self.max_dynamic,
                "hits": self._hits,
                "misses": self._misses,
                "compilations": self._compilations,
                "errors": self._errors,
                "hit_rate": round(hit_rate, 4),
            }


# =============================================================================
# CONVERSATION HISTORY MANAGER
# =============================================================================


class ConversationHistoryManager:
    """
    Manages conversation history with memory optimization.

    Features:
    - Per-user message limits
    - Automatic truncation of old messages
    - Token-aware history trimming
    - Summary generation for truncated context

    Usage:
        manager = ConversationHistoryManager(max_messages=50)
        manager.add_message("user123", {"role": "user", "content": "Hello"})
        history = manager.get_history("user123")
    """

    def __init__(
        self,
        max_messages_per_user: int = 50,
        max_tokens_per_user: int = 100000,
        cleanup_interval_seconds: float = 300.0,
    ):
        self.max_messages = max_messages_per_user
        self.max_tokens = max_tokens_per_user
        self.cleanup_interval = cleanup_interval_seconds

        self._histories: Dict[str, List[Dict[str, Any]]] = {}
        self._token_counts: Dict[str, int] = {}
        self._lock = threading.RLock()
        self._last_cleanup = time.time()

    def add_message(
        self,
        user_id: str,
        message: Dict[str, Any],
        token_count: Optional[int] = None,
    ) -> None:
        """
        Add message to user's history.

        Automatically trims if over limits.
        """
        with self._lock:
            if user_id not in self._histories:
                self._histories[user_id] = []
                self._token_counts[user_id] = 0

            self._histories[user_id].append(message)

            # Estimate tokens if not provided
            if token_count is None:
                content = message.get("content", "")
                if isinstance(content, str):
                    token_count = len(content) // 4  # Rough estimate
                else:
                    token_count = 100  # Default for complex content

            self._token_counts[user_id] += token_count

            # Trim if over limits
            self._trim_history(user_id)

            # Periodic cleanup
            self._maybe_cleanup()

    def get_history(
        self,
        user_id: str,
        max_messages: Optional[int] = None,
    ) -> List[Dict[str, Any]]:
        """Get user's conversation history."""
        with self._lock:
            history = self._histories.get(user_id, [])

            if max_messages and len(history) > max_messages:
                return history[-max_messages:]

            return list(history)

    def clear_history(self, user_id: str) -> bool:
        """Clear user's history."""
        with self._lock:
            if user_id in self._histories:
                del self._histories[user_id]
                self._token_counts.pop(user_id, None)
                return True
            return False

    def get_token_count(self, user_id: str) -> int:
        """Get estimated token count for user."""
        with self._lock:
            return self._token_counts.get(user_id, 0)

    def _trim_history(self, user_id: str) -> None:
        """Trim history to fit within limits."""
        history = self._histories.get(user_id, [])

        # Trim by message count
        while len(history) > self.max_messages:
            removed = history.pop(0)
            content = removed.get("content", "")
            if isinstance(content, str):
                self._token_counts[user_id] -= len(content) // 4

        # Trim by token count
        while self._token_counts.get(user_id, 0) > self.max_tokens and history:
            removed = history.pop(0)
            content = removed.get("content", "")
            if isinstance(content, str):
                self._token_counts[user_id] -= len(content) // 4

    def _maybe_cleanup(self) -> None:
        """Periodic cleanup of empty histories."""
        now = time.time()
        if (now - self._last_cleanup) < self.cleanup_interval:
            return

        self._last_cleanup = now

        # Remove empty histories
        empty_users = [uid for uid, history in self._histories.items() if not history]

        for uid in empty_users:
            del self._histories[uid]
            self._token_counts.pop(uid, None)

    def stats(self) -> Dict[str, Any]:
        """Get statistics."""
        with self._lock:
            total_messages = sum(len(h) for h in self._histories.values())
            total_tokens = sum(self._token_counts.values())

            return {
                "user_count": len(self._histories),
                "total_messages": total_messages,
                "total_tokens": total_tokens,
                "max_messages": self.max_messages,
                "max_tokens": self.max_tokens,
            }


# =============================================================================
# GENERIC MEMOIZATION
# =============================================================================


def memoize(
    maxsize: int = 128,
    ttl_seconds: Optional[float] = None,
) -> Callable:
    r"""
    Decorator for memoizing function results.

    Similar to functools.lru_cache but with TTL support.

    Usage:
        @memoize(maxsize=100, ttl_seconds=300)
        def expensive_operation(x, y):
            return compute(x, y)
    """

    def decorator(func: Callable) -> Callable:
        cache = LRUCache(max_size=maxsize, ttl_seconds=ttl_seconds)

        def wrapper(*args, **kwargs):
            # Create cache key from args
            key = (args, tuple(sorted(kwargs.items())))
            key_hash = hashlib.md5(str(key).encode()).hexdigest()

            result = cache.get(key_hash)
            if result is not None:
                return result

            result = func(*args, **kwargs)
            cache.set(key_hash, result)
            return result

        wrapper.cache = cache
        wrapper.cache_clear = cache.clear
        wrapper.cache_stats = cache.stats

        return wrapper

    return decorator


# =============================================================================
# SINGLETONS
# =============================================================================

_session_cache: Optional[SessionCache] = None
_pattern_cache: Optional[PatternCache] = None
_history_manager: Optional[ConversationHistoryManager] = None
_cache_lock = threading.Lock()


def get_session_cache(
    max_size: int = 500,
    persist_interval: float = 60.0,
) -> SessionCache:
    """Get or create the singleton session cache."""
    global _session_cache

    if _session_cache is None:
        with _cache_lock:
            if _session_cache is None:
                _session_cache = SessionCache(
                    max_size=max_size,
                    persist_interval_seconds=persist_interval,
                )

    return _session_cache


def get_pattern_cache(max_dynamic: int = 500) -> PatternCache:
    """Get or create the singleton pattern cache."""
    global _pattern_cache

    if _pattern_cache is None:
        with _cache_lock:
            if _pattern_cache is None:
                _pattern_cache = PatternCache(max_dynamic_patterns=max_dynamic)

    return _pattern_cache


def get_history_manager(
    max_messages: int = 50,
    max_tokens: int = 100000,
) -> ConversationHistoryManager:
    """Get or create the singleton history manager."""
    global _history_manager

    if _history_manager is None:
        with _cache_lock:
            if _history_manager is None:
                _history_manager = ConversationHistoryManager(
                    max_messages_per_user=max_messages,
                    max_tokens_per_user=max_tokens,
                )

    return _history_manager


def reset_caches() -> None:
    """Reset all caches (for testing)."""
    global _session_cache, _pattern_cache, _history_manager

    with _cache_lock:
        if _session_cache:
            _session_cache.shutdown()
        _session_cache = None
        _pattern_cache = None
        _history_manager = None
