"""In-memory caching layer for frequently accessed data."""

from __future__ import annotations

import threading
import time
from collections import OrderedDict
from typing import Any, Callable, Dict, Optional, Tuple

logger_options = __import__("logging")
logger = logger_options.getLogger(__name__)


class CacheEntry:
    """A single cached value with metadata."""
    __slots__ = ("value", "created_at", "expires_at", "access_count")

    def __init__(self, value: Any, ttl_seconds: float):
        self.value = value
        self.created_at = time.monotonic()
        self.expires_at = self.created_at + ttl_seconds
        self.access_count = 0

    @property
    def is_expired(self) -> bool:
        return time.monotonic() > self.expires_at


class CacheManager:
    """Thread-safe in-memory cache with TTL and size limits."""

    def __init__(self, max_size: int = 10000, default_ttl: int = 300):
        self._cache: "OrderedDict[str, CacheEntry]" = OrderedDict()
        self._max_size = max_size
        self._default_ttl = default_ttl
        self._lock = threading.RLock()
        self._hits = 0
        self._misses = 0

    def get(self, key: str) -> Optional[Any]:
        """
        Get a value from the cache.

        Args:
            key: Cache key

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

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

            entry.access_count += 1
            self._hits += 1

            # Move to end (LRU)
            self._cache.move_to_end(key)
            return entry.value

    def set(self, key: str, value: Any, ttl: Optional[int] = None) -> None:
        """
        Set a value in the cache.

        Args:
            key: Cache key
            value: Value to cache
            ttl: TTL in seconds (uses default if None)
        """
        with self._lock:
            ttl_seconds = ttl if ttl is not None else self._default_ttl
            self._cache[key] = CacheEntry(value, ttl_seconds)

            # Evict oldest if over capacity
            while len(self._cache) > self._max_size:
                oldest_key = next(iter(self._cache))
                del self._cache[oldest_key]

    def get_or_compute(self, key: str,
                        compute_fn: Callable[[], Any],
                        ttl: Optional[int] = None) -> Any:
        """
        Get from cache or compute and store if missing.

        Args:
            key: Cache key
            compute_fn: Function to compute value if not cached
            ttl: TTL override

        Returns:
            Cached or computed value
        """
        cached = self.get(key)
        if cached is not None:
            return cached

        value = compute_fn()
        self.set(key, value, ttl)
        return value

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

        Args:
            key: Key to remove

        Returns:
            True if key was found and removed
        """
        with self._lock:
            if key in self._cache:
                del self._cache[key]
                return True
            return False

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

        Args:
            pattern: String pattern to match (uses string startswith)

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

    def clear(self) -> None:
        """Clear the entire cache."""
        with self._lock:
            self._cache.clear()
            self._hits = 0
            self._misses = 0

    @property
    def stats(self) -> Dict[str, Any]:
        """Return 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,
                "hit_rate": round(hit_rate, 4),
            }

    @property
    def is_healthy(self) -> bool:
        """Check if cache is operational."""
        return True  # In-memory cache is always available


class CacheKeyBuilder:
    """Helper for building consistent cache keys."""

    @staticmethod
    def candle_key(instrument: str,
                   timeframe: str,
                   start_ts: Optional[int] = None,
                   end_ts: Optional[int] = None) -> str:
        """Build cache key for candle data."""
        parts = [f"candles:{instrument}:{timeframe}"]
        if start_ts is not None:
            parts.append(f"s:{start_ts}")
        if end_ts is not None:
            parts.append(f"e:{end_ts}")
        return ":".join(parts)

    @staticmethod
    def trend_key(instrument: str,
                  timeframe: str,
                  period: int) -> str:
        """Build cache key for trend analysis."""
        return f"trend:{instrument}:{timeframe}:p{period}"

    @staticmethod
    def pattern_key(instrument: str,
                    timeframe: str) -> str:
        """Build cache key for pattern detection."""
        return f"pattern:{instrument}:{timeframe}"

    @staticmethod
    def risk_key(instrument: str,
                 timeframe: str,
                 signal_type: str) -> str:
        """Build cache key for risk analysis."""
        return f"risk:{instrument}:{timeframe}:{signal_type}"