import hashlib
import json
import time
from collections import OrderedDict
from typing import Any, Optional


class CacheEntry:
    def __init__(self, value: Any, ttl: Optional[float] = None):
        self.value = value
        self.created_at = time.time()
        self.ttl = ttl
        self.access_count = 0
        self.last_accessed = self.created_at

    def is_expired(self) -> bool:
        if self.ttl is None:
            return False
        return (time.time() - self.created_at) > self.ttl

    def access(self) -> Any:
        self.access_count += 1
        self.last_accessed = time.time()
        return self.value


class LRUCache:
    def __init__(self, max_size: int = 100, default_ttl: Optional[float] = None):
        self.max_size = max_size
        self.default_ttl = default_ttl
        self._cache: OrderedDict[str, CacheEntry] = OrderedDict()
        self._hits = 0
        self._misses = 0

    def _generate_key(self, key_data: Any) -> str:
        if isinstance(key_data, str):
            return hashlib.sha256(key_data.encode()).hexdigest()
        try:
            serialized = json.dumps(key_data, sort_keys=True)
            return hashlib.sha256(serialized.encode()).hexdigest()
        except (TypeError, ValueError):
            return hashlib.sha256(str(key_data).encode()).hexdigest()

    def get(self, key: Any) -> Optional[Any]:
        cache_key = self._generate_key(key)

        if cache_key not in self._cache:
            self._misses += 1
            return None

        entry = self._cache[cache_key]

        if entry.is_expired():
            del self._cache[cache_key]
            self._misses += 1
            return None

        self._cache.move_to_end(cache_key)
        self._hits += 1
        return entry.access()

    def set(self, key: Any, value: Any, ttl: Optional[float] = None) -> None:
        cache_key = self._generate_key(key)

        if len(self._cache) >= self.max_size and cache_key not in self._cache:
            self._cache.popitem(last=False)

        effective_ttl = ttl if ttl is not None else self.default_ttl
        self._cache[cache_key] = CacheEntry(value, effective_ttl)
        self._cache.move_to_end(cache_key)

    def invalidate(self, key: Any) -> bool:
        cache_key = self._generate_key(key)
        if cache_key in self._cache:
            del self._cache[cache_key]
            return True
        return False

    def clear(self) -> None:
        self._cache.clear()
        self._hits = 0
        self._misses = 0

    def size(self) -> int:
        self._cleanup_expired()
        return len(self._cache)

    def _cleanup_expired(self) -> None:
        expired_keys = [
            key for key, entry in self._cache.items() if entry.is_expired()
        ]
        for key in expired_keys:
            del self._cache[key]

    def get_stats(self) -> dict[str, Any]:
        self._cleanup_expired()
        total_requests = self._hits + self._misses
        hit_rate = (self._hits / total_requests * 100) if total_requests > 0 else 0

        return {
            "size": len(self._cache),
            "max_size": self.max_size,
            "hits": self._hits,
            "misses": self._misses,
            "hit_rate": hit_rate,
            "total_requests": total_requests,
        }


class PatternCache:
    _instance: Optional["PatternCache"] = None
    _cache: Optional[LRUCache] = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._cache = None
        return cls._instance

    def initialize(
        self, max_size: int = 100, default_ttl: Optional[float] = 3600.0
    ) -> None:
        if self._cache is None:
            self._cache = LRUCache(max_size=max_size, default_ttl=default_ttl)

    def get_cache(self) -> LRUCache:
        if self._cache is None:
            self.initialize()
        return self._cache

    def clear(self) -> None:
        if self._cache is not None:
            self._cache.clear()

    @classmethod
    def reset(cls) -> None:
        cls._instance = None
        cls._cache = None
