"""
Multi-Level Cache Manager for groknroll.

Provides a three-tier caching strategy:
- L1: In-memory LRU cache (fast, limited size)
- L2: SQLite cache (persistent, moderate speed)
- L3: File system cache (persistent, large storage)

Example:
    cache = CacheManager(max_memory_mb=100, max_sqlite_mb=500)

    # Store
    await cache.set("tool_result_123", {"result": "success"}, ttl=300)

    # Retrieve (checks L1 → L2 → L3)
    result = await cache.get("tool_result_123")

    # Invalidate
    await cache.invalidate("tool_result_123")
"""

import asyncio
import hashlib
import json
import pickle
import sqlite3
import time
from collections import OrderedDict
from pathlib import Path
from typing import Any, Optional


class LRUCache:
    """
    In-memory LRU cache (L1).

    Fast access, limited by memory size and TTL.
    """

    def __init__(self, max_size_mb: int = 100, default_ttl: int = 300):
        """
        Initialize LRU cache.

        Args:
            max_size_mb: Maximum cache size in MB
            default_ttl: Default time-to-live in seconds
        """
        self.max_size_bytes = max_size_mb * 1024 * 1024
        self.default_ttl = default_ttl
        self.cache: OrderedDict[str, tuple[Any, float]] = OrderedDict()
        self.current_size = 0
        self._lock = asyncio.Lock()

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

        Args:
            key: Cache key

        Returns:
            Cached value or None if not found/expired
        """
        async with self._lock:
            if key not in self.cache:
                return None

            value, expiry = self.cache[key]

            # Check if expired
            if time.time() > expiry:
                del self.cache[key]
                return None

            # Move to end (most recently used)
            self.cache.move_to_end(key)
            return value

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

        Args:
            key: Cache key
            value: Value to cache
            ttl: Time-to-live in seconds (None = default_ttl)
        """
        async with self._lock:
            expiry = time.time() + (ttl if ttl is not None else self.default_ttl)

            # Estimate size (rough approximation)
            value_size = len(pickle.dumps(value))

            # Remove old entry if exists
            if key in self.cache:
                old_value, _ = self.cache[key]
                self.current_size -= len(pickle.dumps(old_value))
                del self.cache[key]

            # Evict oldest entries if needed
            while self.current_size + value_size > self.max_size_bytes and self.cache:
                oldest_key = next(iter(self.cache))
                oldest_value, _ = self.cache[oldest_key]
                self.current_size -= len(pickle.dumps(oldest_value))
                del self.cache[oldest_key]

            # Add new entry
            self.cache[key] = (value, expiry)
            self.current_size += value_size

    async def delete(self, key: str):
        """Delete key from cache."""
        async with self._lock:
            if key in self.cache:
                value, _ = self.cache[key]
                self.current_size -= len(pickle.dumps(value))
                del self.cache[key]

    async def clear(self):
        """Clear all cached entries."""
        async with self._lock:
            self.cache.clear()
            self.current_size = 0


class SQLiteCache:
    """
    SQLite-based cache (L2).

    Persistent storage with SQL query capabilities.
    """

    def __init__(self, db_path: Path, default_ttl: int = 3600):
        """
        Initialize SQLite cache.

        Args:
            db_path: Path to SQLite database
            default_ttl: Default time-to-live in seconds
        """
        self.db_path = db_path
        self.default_ttl = default_ttl
        self._lock = asyncio.Lock()
        self._init_db()

    def _init_db(self):
        """Initialize database schema."""
        db_path = self.db_path
        db_path.parent.mkdir(parents=True, exist_ok=True)

        conn = sqlite3.connect(db_path)
        cursor = conn.cursor()

        cursor.execute(
            """
            CREATE TABLE IF NOT EXISTS cache (
                key TEXT PRIMARY KEY,
                value BLOB NOT NULL,
                expiry REAL NOT NULL,
                created_at REAL NOT NULL
            )
            """
        )

        # Index for expiry-based queries
        cursor.execute("CREATE INDEX IF NOT EXISTS idx_expiry ON cache(expiry)")

        conn.commit()
        conn.close()

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

        Args:
            key: Cache key

        Returns:
            Cached value or None if not found/expired
        """
        async with self._lock:
            conn = sqlite3.connect(self.db_path)
            cursor = conn.cursor()

            cursor.execute(
                "SELECT value, expiry FROM cache WHERE key = ?",
                (key,),
            )
            row = cursor.fetchone()
            conn.close()

            if row is None:
                return None

            value_blob, expiry = row

            # Check if expired
            if time.time() > expiry:
                await self.delete(key)
                return None

            # Deserialize
            return pickle.loads(value_blob)

    async def set(self, key: str, value: Any, ttl: Optional[int] = None):
        """
        Set value in SQLite cache.

        Args:
            key: Cache key
            value: Value to cache
            ttl: Time-to-live in seconds
        """
        async with self._lock:
            expiry = time.time() + (ttl if ttl is not None else self.default_ttl)
            value_blob = pickle.dumps(value)
            created_at = time.time()

            conn = sqlite3.connect(self.db_path)
            cursor = conn.cursor()

            cursor.execute(
                """
                INSERT OR REPLACE INTO cache (key, value, expiry, created_at)
                VALUES (?, ?, ?, ?)
                """,
                (key, value_blob, expiry, created_at),
            )

            conn.commit()
            conn.close()

    async def delete(self, key: str):
        """Delete key from SQLite cache."""
        async with self._lock:
            conn = sqlite3.connect(self.db_path)
            cursor = conn.cursor()

            cursor.execute("DELETE FROM cache WHERE key = ?", (key,))

            conn.commit()
            conn.close()

    async def clear_expired(self):
        """Remove all expired entries."""
        async with self._lock:
            conn = sqlite3.connect(self.db_path)
            cursor = conn.cursor()

            cursor.execute("DELETE FROM cache WHERE expiry < ?", (time.time(),))

            conn.commit()
            conn.close()


class FileCache:
    """
    File system-based cache (L3).

    Large persistent storage for infrequently accessed data.
    """

    def __init__(self, cache_dir: Path, default_ttl: int = 86400):
        """
        Initialize file system cache.

        Args:
            cache_dir: Directory for cache files
            default_ttl: Default time-to-live in seconds
        """
        self.cache_dir = cache_dir
        self.default_ttl = default_ttl
        self._lock = asyncio.Lock()
        cache_dir.mkdir(parents=True, exist_ok=True)

    def _get_cache_path(self, key: str) -> Path:
        """Get file path for cache key."""
        key_hash = hashlib.sha256(key.encode()).hexdigest()
        return self.cache_dir / f"{key_hash}.cache"

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

        Args:
            key: Cache key

        Returns:
            Cached value or None if not found/expired
        """
        async with self._lock:
            cache_path = self._get_cache_path(key)

            if not cache_path.exists():
                return None

            try:
                with open(cache_path, "rb") as f:
                    data = pickle.load(f)

                value = data["value"]
                expiry = data["expiry"]

                # Check if expired
                if time.time() > expiry:
                    cache_path.unlink()
                    return None

                return value

            except Exception:
                # Corrupted cache file
                if cache_path.exists():
                    cache_path.unlink()
                return None

    async def set(self, key: str, value: Any, ttl: Optional[int] = None):
        """
        Set value in file cache.

        Args:
            key: Cache key
            value: Value to cache
            ttl: Time-to-live in seconds
        """
        async with self._lock:
            cache_path = self._get_cache_path(key)
            expiry = time.time() + (ttl if ttl is not None else self.default_ttl)

            data = {
                "key": key,
                "value": value,
                "expiry": expiry,
                "created_at": time.time(),
            }

            with open(cache_path, "wb") as f:
                pickle.dump(data, f)

    async def delete(self, key: str):
        """Delete key from file cache."""
        async with self._lock:
            cache_path = self._get_cache_path(key)
            if cache_path.exists():
                cache_path.unlink()


class CacheManager:
    """
    Multi-level cache manager.

    Provides transparent access to L1 (memory) → L2 (SQLite) → L3 (file system) caches.
    """

    def __init__(
        self,
        max_memory_mb: int = 100,
        max_sqlite_mb: int = 500,
        cache_dir: Optional[Path] = None,
        memory_ttl: int = 300,
        sqlite_ttl: int = 3600,
        file_ttl: int = 86400,
    ):
        """
        Initialize cache manager.

        Args:
            max_memory_mb: Max L1 cache size in MB
            max_sqlite_mb: Max L2 cache size in MB (not enforced yet)
            cache_dir: Directory for L2 and L3 caches (default: ~/.groknroll/cache)
            memory_ttl: L1 TTL in seconds
            sqlite_ttl: L2 TTL in seconds
            file_ttl: L3 TTL in seconds
        """
        if cache_dir is None:
            cache_dir = Path.home() / ".groknroll" / "cache"

        cache_dir.mkdir(parents=True, exist_ok=True)

        self.l1 = LRUCache(max_size_mb=max_memory_mb, default_ttl=memory_ttl)
        self.l2 = SQLiteCache(db_path=cache_dir / "cache.db", default_ttl=sqlite_ttl)
        self.l3 = FileCache(cache_dir=cache_dir / "files", default_ttl=file_ttl)

        # Statistics
        self.stats = {"l1_hits": 0, "l2_hits": 0, "l3_hits": 0, "misses": 0}

    async def get(self, key: str) -> Optional[Any]:
        """
        Get value from cache (L1 → L2 → L3).

        Args:
            key: Cache key

        Returns:
            Cached value or None if not found
        """
        # Try L1 (memory)
        value = await self.l1.get(key)
        if value is not None:
            self.stats["l1_hits"] += 1
            return value

        # Try L2 (SQLite)
        value = await self.l2.get(key)
        if value is not None:
            self.stats["l2_hits"] += 1
            # Promote to L1
            await self.l1.set(key, value)
            return value

        # Try L3 (file system)
        value = await self.l3.get(key)
        if value is not None:
            self.stats["l3_hits"] += 1
            # Promote to L1 and L2
            await self.l1.set(key, value)
            await self.l2.set(key, value)
            return value

        self.stats["misses"] += 1
        return None

    async def set(self, key: str, value: Any, ttl: Optional[int] = None):
        """
        Set value in all cache levels.

        Args:
            key: Cache key
            value: Value to cache
            ttl: Time-to-live in seconds
        """
        await self.l1.set(key, value, ttl=ttl)
        await self.l2.set(key, value, ttl=ttl)
        await self.l3.set(key, value, ttl=ttl)

    async def invalidate(self, key: str):
        """
        Invalidate key across all cache levels.

        Args:
            key: Cache key to invalidate
        """
        await self.l1.delete(key)
        await self.l2.delete(key)
        await self.l3.delete(key)

    async def clear(self):
        """Clear all cache levels."""
        await self.l1.clear()
        # L2 and L3 clear not implemented to avoid accidental data loss

    def get_stats(self) -> dict[str, int]:
        """Get cache statistics."""
        total_requests = sum(self.stats.values())
        hit_rate = (
            (self.stats["l1_hits"] + self.stats["l2_hits"] + self.stats["l3_hits"])
            / total_requests
            if total_requests > 0
            else 0
        )

        return {
            **self.stats,
            "total_requests": total_requests,
            "hit_rate": hit_rate,
        }


# Global cache manager instance
_cache_manager: Optional[CacheManager] = None


def get_cache_manager() -> CacheManager:
    """Get or create global cache manager."""
    global _cache_manager
    if _cache_manager is None:
        _cache_manager = CacheManager()
    return _cache_manager
