from __future__ import annotations

import logging
from threading import RLock
from typing import Tuple

from cachetools import LRUCache

from backends.explorer.discovery.info_registry import ConnectionInfoRegistry
from backends.explorer.factories.db_handler_factory import DbHandlerFactory
from backends.explorer.utils.presets import get_webapp_neo4j_preset
from graph.connections.connection_info import AbstractConnectionInfo
from graph.connections.db_handler import DbHandler
from solutions.graph.models import PresetNotSetException

logger = logging.getLogger(__name__)


class CleanupLRUCache(LRUCache):
    """LRU Cache that calls cleanup_handler() on evicted database handlers.

    This ensures proper resource cleanup (closing connections, releasing file handles)
    when handlers are evicted from the cache due to size limits.
    """

    def popitem(self) -> Tuple[str, DbHandler]:
        """Override to cleanup handler before removing from cache."""
        snapshot_id, handler = super().popitem()
        try:
            handler.cleanup_handler()
            logger.info(f"Cleaned up evicted handler for graph {snapshot_id}")
        except Exception as e:
            logger.exception(f"Error cleaning up handler for graph {snapshot_id}")

        return snapshot_id, handler

class ExplorerDbHandlerRegistry:
    """Centralized registry for managing database handlers with LRU caching.

    Uses a custom LRU cache that properly cleans up database handlers (closes connections,
    releases resources) when they are evicted from the cache.
    """
    def __init__(self, conn_info_registry: ConnectionInfoRegistry, cache_size: int = 128):
        self.webapp_neo4j_preset = get_webapp_neo4j_preset()
        self.conn_info_registry = conn_info_registry

        self.cache_size = cache_size
        self._cache = CleanupLRUCache(maxsize=cache_size)
        self._cache_lock = RLock()

    def get_handler(self, snapshot_id: str) -> DbHandler:
        """Get or create a database handler for a given graph."""
        if not snapshot_id:
            raise ValueError("snapshot_id cannot be empty")

        with self._cache_lock:
            # Check if handler exists in cache
            if snapshot_id in self._cache:
                return self._cache[snapshot_id]  # type: ignore
            
            # Create handler only if not in cache
            handler = self._create_handler(snapshot_id)
            self._cache[snapshot_id] = handler
            return handler
    
    def _create_handler(self, snapshot_id: str) -> DbHandler:
        """Create a new database handler."""
        logger.debug(f"Creating handler for graph {snapshot_id}")
        db_info = self.conn_info_registry.get_connection_info(snapshot_id)
        if not db_info:
            raise ValueError(f"No connection info found for snapshot {snapshot_id}")

        if db_info.type == "kuzu":
            return self._create_kuzu_handler(db_info)
        elif db_info.type == "neo4j":
            return self._create_neo4j_handler(db_info)
        else:
            raise ValueError(f"Unknown database type for snapshot {snapshot_id}")

    def _create_kuzu_handler(self, db_info: AbstractConnectionInfo) -> DbHandler:
        """Create a Kuzu database handler for a snapshot."""
        return DbHandlerFactory.get_db_handler(db_info)

    def _create_neo4j_handler(self, db_info: AbstractConnectionInfo) -> DbHandler:
        """Create a Neo4j database handler for a snapshot."""
        if not self.webapp_neo4j_preset:
            raise PresetNotSetException()

        return DbHandlerFactory.get_db_handler(db_info, self.webapp_neo4j_preset)

    def clear_cache(self) -> None:
        """Clear all cached handlers and cleanup their resources."""
        with self._cache_lock:
            for snapshot_id, handler in list(self._cache.items()):
                try:
                    handler.cleanup_handler()
                    logger.debug(f"Cleaned up handler for graph {snapshot_id}")
                except Exception as e:
                    logger.exception(f"Error cleaning up handler for graph {snapshot_id}: {e}")
            self._cache.clear()
            logger.info("DB handler cache cleared")

    def get_cache_info(self) -> dict:
        """Get information about the current cache state."""
        with self._cache_lock:
            return {
                "maxsize": self._cache.maxsize,
                "currsize": self._cache.currsize,
            }

_explorer_registry_instance: ExplorerDbHandlerRegistry | None = None
_registry_init_lock = RLock()

def get_db_handler_registry(conn_info_registry: ConnectionInfoRegistry, cache_size: int = 128) -> ExplorerDbHandlerRegistry:
    global _explorer_registry_instance
    if _explorer_registry_instance:
        return _explorer_registry_instance

    with _registry_init_lock:
        if _explorer_registry_instance:
            return _explorer_registry_instance

        _explorer_registry_instance = ExplorerDbHandlerRegistry(conn_info_registry=conn_info_registry, cache_size=cache_size)
        return _explorer_registry_instance


def get_handler_for_snapshot(snapshot_id: str, conn_info_registry: ConnectionInfoRegistry, cache_size: int = 128) -> DbHandler:
    db_handler = get_db_handler_registry(conn_info_registry=conn_info_registry, cache_size=cache_size)
    logger.debug("Cache info: %s", db_handler.get_cache_info())
    return db_handler.get_handler(snapshot_id)
