"""
In-memory cache backend using cachetools.

Supports LRU, TTL, and LFU eviction policies.
"""

from __future__ import annotations

import threading
import time
from dataclasses import dataclass, field
from typing import Any, Dict, Optional

from cachestack.base import BaseCache, BackendUnavailableError, CacheError


@dataclass
class MemoryConfig:
    """Configuration for the in-memory backend.

    Attributes:
        policy: Eviction policy — 'lru', 'ttl', or 'lfu'.
        maxsize: Maximum number of items to store.
        ttl: Default time-to-live in seconds. Only used when policy='ttl'.
        silent: If True, suppress errors and return None instead of raising.
    """
    policy: str = "lru"
    maxsize: int = 1024
    ttl: Optional[int] = None
    silent: bool = False


class MemoryBackend(BaseCache):
    """
    In-memory cache backend.

    Uses cachetools under the hood with thread-safe locking.
    Falls back to a plain dict with manual TTL if cachetools is unavailable.

    Args:
        config: MemoryConfig instance.
    """

    def __init__(self, config: Optional[MemoryConfig] = None) -> None:
        self._config = config or MemoryConfig()
        self._lock = threading.Lock()
        self._hits = 0
        self._misses = 0
        self._errors = 0
        self._cache: Dict[str, Any] = {}
        self._expiry: Dict[str, float] = {}
        self._access: Dict[str, int] = {}
        self._freq: Dict[str, int] = {}
        self._access_counter = 0
        self._setup_cache()

    def _setup_cache(self) -> None:
        """Initialize the underlying cache structure."""
        try:
            import cachetools
            policy = self._config.policy.lower()
            if policy == "lru":
                self._ct_cache: Any = cachetools.LRUCache(maxsize=self._config.maxsize)
            elif policy == "ttl":
                ttl = self._config.ttl or 600
                self._ct_cache = cachetools.TTLCache(maxsize=self._config.maxsize, ttl=ttl)
            elif policy == "lfu":
                self._ct_cache = cachetools.LFUCache(maxsize=self._config.maxsize)
            else:
                raise ValueError(f"Unknown policy: {self._config.policy}")
            self._use_cachetools = True
        except ImportError:
            self._ct_cache = {}
            self._use_cachetools = False

    def _is_expired(self, key: str) -> bool:
        """Check if a key has expired."""
        if key in self._expiry:
            return time.monotonic() > self._expiry[key]
        return False

    def _evict_if_needed(self) -> None:
        """Evict entries if cache exceeds maxsize (for fallback dict mode)."""
        if not self._use_cachetools and len(self._ct_cache) >= self._config.maxsize:
            policy = self._config.policy.lower()
            if policy == "lru" and self._access:
                oldest = min(self._access, key=self._access.get)  # type: ignore
                self._ct_cache.pop(oldest, None)
                self._expiry.pop(oldest, None)
                self._access.pop(oldest, None)
            elif policy == "lfu" and self._freq:
                least = min(self._freq, key=self._freq.get)  # type: ignore
                self._ct_cache.pop(least, None)
                self._expiry.pop(least, None)
                self._freq.pop(least, None)

    async def get(self, key: str) -> Optional[Any]:
        """Retrieve a value by key."""
        try:
            with self._lock:
                if self._use_cachetools:
                    value = self._ct_cache.get(key)
                    if value is None and key not in self._ct_cache:
                        self._misses += 1
                        return None
                    # Check manual TTL for non-TTLCache
                    if self._is_expired(key):
                        del self._ct_cache[key]
                        self._expiry.pop(key, None)
                        self._misses += 1
                        return None
                    self._hits += 1
                    return value
                else:
                    if key not in self._ct_cache or self._is_expired(key):
                        self._ct_cache.pop(key, None)
                        self._expiry.pop(key, None)
                        self._misses += 1
                        return None
                    self._access_counter += 1
                    self._access[key] = self._access_counter
                    self._freq[key] = self._freq.get(key, 0) + 1
                    self._hits += 1
                    return self._ct_cache[key]
        except Exception as e:
            self._errors += 1
            if self._config.silent:
                return None
            raise CacheError(f"MemoryBackend get failed: {e}") from e

    async def set(self, key: str, value: Any, ttl: Optional[int] = None) -> None:
        """Store a value by key."""
        try:
            with self._lock:
                effective_ttl = ttl or self._config.ttl
                if self._use_cachetools:
                    self._ct_cache[key] = value
                    if effective_ttl and not hasattr(self._ct_cache, 'timer'):
                        self._expiry[key] = time.monotonic() + effective_ttl
                else:
                    self._evict_if_needed()
                    self._ct_cache[key] = value
                    self._access_counter += 1
                    self._access[key] = self._access_counter
                    self._freq[key] = self._freq.get(key, 0) + 1
                    if effective_ttl:
                        self._expiry[key] = time.monotonic() + effective_ttl
        except Exception as e:
            self._errors += 1
            if self._config.silent:
                return
            raise CacheError(f"MemoryBackend set failed: {e}") from e

    async def delete(self, key: str) -> None:
        """Delete a key."""
        try:
            with self._lock:
                if self._use_cachetools:
                    self._ct_cache.pop(key, None)
                else:
                    self._ct_cache.pop(key, None)
                self._expiry.pop(key, None)
                self._access.pop(key, None)
                self._freq.pop(key, None)
        except Exception as e:
            self._errors += 1
            if self._config.silent:
                return
            raise CacheError(f"MemoryBackend delete failed: {e}") from e

    async def exists(self, key: str) -> bool:
        """Check if a key exists."""
        value = await self.get(key)
        return value is not None

    async def clear(self) -> None:
        """Clear all entries."""
        try:
            with self._lock:
                if self._use_cachetools:
                    self._ct_cache.clear()
                else:
                    self._ct_cache.clear()
                self._expiry.clear()
                self._access.clear()
                self._freq.clear()
        except Exception as e:
            self._errors += 1
            if self._config.silent:
                return
            raise CacheError(f"MemoryBackend clear failed: {e}") from e

    async def stats(self) -> Dict[str, Any]:
        """Return cache statistics."""
        with self._lock:
            size = len(self._ct_cache)
        return {
            "backend": "memory",
            "policy": self._config.policy,
            "hits": self._hits,
            "misses": self._misses,
            "errors": self._errors,
            "size": size,
            "maxsize": self._config.maxsize,
        }
