# File: src/noveler/infrastructure/json/mcp/servers/cache_manager.py
# Purpose: Centralized cache management for JSONConversionServer with unified lifecycle control
# Context: Extracted from JSONConversionServer to improve single responsibility principle (Tier 1b Phase 2)

"""Unified cache management system for the JSON conversion MCP server.

This module provides a centralized cache manager that coordinates multiple cache
systems (FileIOCache, JSON conversion cache, path resolution cache) with a unified
interface for initialization, cleanup, and monitoring.

Components:
- CacheManager: Central coordinator for all caches
- Cache lifecycle management: initialization, cleanup, eviction
- Cache statistics and monitoring
"""

from __future__ import annotations

import logging
from collections.abc import Callable
from dataclasses import dataclass
from pathlib import Path
from typing import Any


@dataclass
class CacheStatistics:
    """Cache performance metrics and status.

    Attributes:
        file_cache_size: Current number of entries in FileIOCache
        json_cache_size: Current number of entries in JSON conversion cache
        path_cache_size: Current number of entries in path resolution cache
        total_entries: Total cached entries across all caches
        file_cache_evictions: Number of FileIOCache LRU evictions
        json_cache_evictions: Number of JSON cache LRU evictions
    """

    file_cache_size: int = 0
    json_cache_size: int = 0
    path_cache_size: int = 0
    total_entries: int = 0
    file_cache_evictions: int = 0
    json_cache_evictions: int = 0


class CacheManager:
    """Unified cache management coordinator.

    Purpose:
        Manages lifecycle of all caches used by JSONConversionServer with
        consistent policies for initialization, cleanup, and eviction.

    Responsibilities:
        - Coordinate multiple cache systems (FileIOCache, JSON, path)
        - Implement unified eviction and cleanup policies
        - Provide cache statistics and monitoring
        - Handle emergency cache cleanup
        - Manage cache TTL and LRU eviction

    Context:
        Used by JSONConversionServer to separate cache management logic
        from main server orchestration.

    Design Patterns:
        - Facade Pattern: Unified interface for multiple caches
        - Strategy Pattern: Different cleanup strategies (periodic vs emergency)
        - Observer Pattern: Cache statistics collection
    """

    def __init__(
        self,
        logger: logging.Logger,
        file_cache_max_size: int = 256,
        file_cache_ttl_seconds: int = 600,
        json_cache_max_size: int = 100,
        json_cache_ttl_seconds: int = 300,
    ) -> None:
        """Initialize the unified cache manager.

        Args:
            logger: Logger instance for cache lifecycle events
            file_cache_max_size: Maximum entries in FileIOCache (default 256)
            file_cache_ttl_seconds: FileIOCache TTL in seconds (default 600)
            json_cache_max_size: Maximum entries in JSON cache (default 100)
            json_cache_ttl_seconds: JSON cache TTL in seconds (default 300)

        Preconditions:
            - logger must be a valid Logger instance
            - file_cache_max_size and json_cache_max_size must be > 0
            - TTL values must be >= 0

        Side Effects:
            - Initializes internal cache storage dictionaries
            - Logs cache configuration
        """
        self._logger = logger
        self._file_cache_max_size = file_cache_max_size
        self._file_cache_ttl = file_cache_ttl_seconds
        self._json_cache_max_size = json_cache_max_size
        self._json_cache_ttl = json_cache_ttl_seconds

        # FileIOCache data
        self._file_cache: dict[str, dict[str, Any]] = {}
        self._file_cache_timestamps: dict[str, float] = {}
        self._file_cache_access_times: dict[str, float] = {}

        # JSON conversion cache data
        self._json_conversion_cache: dict[str, Any] = {}
        self._json_cache_access: dict[str, float] = {}

        # Path resolution cache data
        self._path_resolution_cache: dict[str, Path] = {}

        # Statistics
        self._statistics = CacheStatistics()
        self._file_cache_evictions = 0
        self._json_cache_evictions = 0

        self._logger.info(
            "Initializing cache manager: FileIOCache(%s), JSONCache(%s), PathCache(unlimited)",
            file_cache_max_size,
            json_cache_max_size,
        )

    # ===== FileIOCache Interface =====

    def file_cache_get(
        self, file_path: Path, loader_func: Callable[[Path], dict[str, Any]]
    ) -> dict[str, Any]:
        """Retrieve cached file data or load if expired.

        Purpose:
            Unified interface for FileIOCache get operations with LRU + TTL.

        Args:
            file_path: Path to file to load
            loader_func: Function to call if cache miss

        Returns:
            Cached or freshly loaded file data
        """
        import time

        key = str(file_path)
        current_time = time.time()

        # Check if entry exists and is valid
        if key in self._file_cache and key in self._file_cache_timestamps:
            if current_time - self._file_cache_timestamps[key] < self._file_cache_ttl:
                self._file_cache_access_times[key] = current_time
                return self._file_cache[key]

        # Cache miss or expired: load and store
        data = loader_func(file_path)
        self._file_cache[key] = data
        self._file_cache_timestamps[key] = current_time
        self._file_cache_access_times[key] = current_time

        # Evict LRU if at capacity
        if len(self._file_cache) > self._file_cache_max_size:
            self._evict_lru_file_cache()

        return data

    def file_cache_invalidate(self, file_path: Path) -> None:
        """Invalidate cache entry for a specific file.

        Args:
            file_path: Path to file to invalidate
        """
        key = str(file_path)
        self._file_cache.pop(key, None)
        self._file_cache_timestamps.pop(key, None)
        self._file_cache_access_times.pop(key, None)
        self._logger.debug(
            "Invalidated file cache for: %s",
            file_path,
        )

    def file_cache_clear(self) -> None:
        """Clear all FileIOCache entries."""
        count = len(self._file_cache)
        self._file_cache.clear()
        self._file_cache_timestamps.clear()
        self._file_cache_access_times.clear()
        self._logger.debug(
            "Cleared FileIOCache (%s entries)",
            count,
        )

    # ===== JSON Conversion Cache Interface =====

    def json_cache_get(
        self, key: str, compute_func: Callable[[], Any] | None = None
    ) -> Any | None:
        """Retrieve or compute JSON cache entry.

        Args:
            key: Cache key
            compute_func: Function to call if cache miss

        Returns:
            Cached value or result of compute_func
        """
        import time

        current_time = time.time()

        # Check cache validity
        if (
            key in self._json_conversion_cache
            and current_time - self._json_cache_access.get(key, 0) < self._json_cache_ttl
        ):
            self._json_cache_access[key] = current_time
            return self._json_conversion_cache[key]

        # Cache miss: compute if function provided
        if compute_func is None:
            return None

        result = compute_func()
        self.json_cache_set(key, result)
        return result

    def json_cache_set(self, key: str, value: Any) -> None:
        """Store value in JSON conversion cache.

        Args:
            key: Cache key
            value: Value to cache
        """
        import time

        # Evict LRU if at capacity
        if len(self._json_conversion_cache) >= self._json_cache_max_size:
            self._evict_lru_json_cache()

        self._json_conversion_cache[key] = value
        self._json_cache_access[key] = time.time()
        self._logger.debug(
            "Cached JSON result: %s",
            key,
        )

    def json_cache_clear(self) -> None:
        """Clear all JSON conversion cache entries."""
        count = len(self._json_conversion_cache)
        self._json_conversion_cache.clear()
        self._json_cache_access.clear()
        self._logger.debug(
            "Cleared JSON cache (%s entries)",
            count,
        )

    # ===== Path Resolution Cache Interface =====

    def path_cache_get(self, key: str) -> Path | None:
        """Retrieve cached path resolution result.

        Args:
            key: Cache key

        Returns:
            Cached Path or None if not found
        """
        return self._path_resolution_cache.get(key)

    def path_cache_set(self, key: str, path: Path) -> None:
        """Cache a path resolution result.

        Args:
            key: Cache key
            path: Path to cache
        """
        self._path_resolution_cache[key] = path

    def path_cache_clear(self) -> None:
        """Clear all path resolution cache entries."""
        count = len(self._path_resolution_cache)
        self._path_resolution_cache.clear()
        self._logger.debug(
            "Cleared path cache (%s entries)",
            count,
        )

    # ===== Cache Lifecycle Management =====

    def cleanup_expired(self) -> None:
        """Clean up all expired cache entries.

        Purpose:
            Remove entries that exceed their TTL. Called periodically
            to release memory without clearing all caches.

        Side Effects:
            - Removes expired FileIOCache entries
            - Removes expired JSON cache entries
            - Logs cleanup statistics
        """
        import time

        current_time = time.time()

        # Cleanup FileIOCache
        expired_file_keys = [
            key
            for key, timestamp in self._file_cache_timestamps.items()
            if current_time - timestamp >= self._file_cache_ttl
        ]
        for key in expired_file_keys:
            self._file_cache.pop(key, None)
            self._file_cache_timestamps.pop(key, None)
            self._file_cache_access_times.pop(key, None)

        # Cleanup JSON cache
        expired_json_keys = [
            key
            for key, access_time in self._json_cache_access.items()
            if current_time - access_time >= self._json_cache_ttl
        ]
        for key in expired_json_keys:
            self._json_conversion_cache.pop(key, None)
            self._json_cache_access.pop(key, None)

        self._logger.debug(
            "Cleanup expired entries: FileIOCache(%s), JSONCache(%s)",
            len(expired_file_keys),
            len(expired_json_keys),
        )

    def emergency_cleanup(self) -> None:
        """Immediately clear all caches.

        Purpose:
            Used when memory pressure is critical. Clears all caches
            to free maximum memory immediately.

        Side Effects:
            - Clears all caches (FileIOCache, JSON, path)
            - Logs emergency cleanup event
        """
        old_size = len(self._file_cache) + len(self._json_conversion_cache)
        self.file_cache_clear()
        self.json_cache_clear()
        self.path_cache_clear()
        self._logger.warning(
            "Emergency cache cleanup: freed %s entries",
            old_size,
        )

    def get_statistics(self) -> CacheStatistics:
        """Get current cache statistics.

        Returns:
            CacheStatistics with current cache sizes and eviction counts
        """
        self._statistics.file_cache_size = len(self._file_cache)
        self._statistics.json_cache_size = len(self._json_conversion_cache)
        self._statistics.path_cache_size = len(self._path_resolution_cache)
        self._statistics.total_entries = (
            self._statistics.file_cache_size
            + self._statistics.json_cache_size
            + self._statistics.path_cache_size
        )
        self._statistics.file_cache_evictions = self._file_cache_evictions
        self._statistics.json_cache_evictions = self._json_cache_evictions

        return self._statistics

    # ===== Internal Cache Management =====

    def _evict_lru_file_cache(self) -> None:
        """Evict the least recently accessed FileIOCache entry."""
        if not self._file_cache_access_times:
            return

        lru_key = min(self._file_cache_access_times.items(), key=lambda x: x[1])[0]
        self._file_cache.pop(lru_key, None)
        self._file_cache_timestamps.pop(lru_key, None)
        self._file_cache_access_times.pop(lru_key, None)
        self._file_cache_evictions += 1
        self._logger.debug(
            "FileIOCache LRU eviction: %s",
            lru_key,
        )

    def _evict_lru_json_cache(self) -> None:
        """Evict the least recently accessed JSON cache entry."""
        if not self._json_cache_access:
            return

        lru_key = min(self._json_cache_access.items(), key=lambda x: x[1])[0]
        self._json_conversion_cache.pop(lru_key, None)
        self._json_cache_access.pop(lru_key, None)
        self._json_cache_evictions += 1
        self._logger.debug(
            "JSON cache LRU eviction: %s",
            lru_key,
        )
