from __future__ import annotations

import logging
import os
from pathlib import Path
from threading import RLock
from typing import Dict, Optional, Tuple

from cachetools import LRUCache

from backends.editor.utils.presets import get_webapp_neo4j_preset
from backends.editor.utils.store import create_graph_metadata_store
from backends.editor.utils.webapp_config import webapp_config
from graph.connections.db_handler import DbHandler
from graph.connections.kuzu.db_handler import KuzuLocalDbHandler
from graph.connections.neo4j.db_handler import Neo4DbHandler
from solutions.graph.models import DbType, GraphId, PresetNotSetException, create_neo4j_db_name

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[Tuple[GraphId, DbType], DbHandler]:
        """Override to cleanup handler before removing from cache."""
        key, handler = super().popitem()
        graph_id, db_type = key
        try:
            handler.cleanup_handler()
            logger.info(f"Cleaned up evicted {db_type} handler for graph {graph_id}")
        except Exception as e:
            logger.exception(f"Error cleaning up {db_type} handler for graph {graph_id}: {e}")
        return key, handler


class EditorDbHandlerRegistry:
    """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, cache_size: int = 128):
        self.cache_size = cache_size
        self._cache = CleanupLRUCache(maxsize=cache_size)
        self._cache_lock = RLock()

        self._store = create_graph_metadata_store()

    def get_handler(self, graph_id: GraphId, db_type: Optional[DbType] = None) -> DbHandler:
        """Get or create a database handler for a given graph."""
        if not graph_id:
            raise ValueError("graph_id cannot be None or empty")

        graph_metadata = self._store.get(graph_id)
        db_type = db_type if db_type else graph_metadata["connection"]["type"]

        cache_key = (graph_id, db_type)

        # Check cache without lock and update LRU order
        try:
            return self._cache[cache_key]  # type: ignore
        except KeyError:
            pass

        handler = self._create_handler(graph_id, db_type)

        with self._cache_lock:
            # another thread might have created it while we were creating ours
            if cache_key in self._cache:
                # If so, clean up our redundant handler
                try:
                    handler.cleanup_handler()
                except Exception as e:
                    logger.exception(f"Error cleaning up redundant handler for graph {graph_id}: {e}")
                return self._cache[cache_key]  # type: ignore

            self._cache[cache_key] = handler
            return handler

    def _create_handler(self, graph_id: GraphId, db_type: DbType) -> DbHandler:
        """Create a new database handler."""
        logger.debug(f"Creating {db_type} handler for graph {graph_id}")
        if db_type == "kuzu":
            return self._create_kuzu_handler(graph_id)
        elif db_type == "neo4j":
            return self._create_neo4j_handler(graph_id)
        else:
            raise ValueError(f"Unknown database type: {db_type}")

    def _create_kuzu_handler(self, graph_id: GraphId) -> DbHandler:
        """Create a Kuzu database handler for a graph."""
        db_path = Path(os.path.join(webapp_config.db_folder_path, graph_id, "db.kz"))
        logger.info(f"Creating Kuzu handler for graph {graph_id} at path {db_path}")
        return KuzuLocalDbHandler(db_path=db_path, should_db_persist=True)

    def _create_neo4j_handler(self, graph_id: GraphId) -> DbHandler:
        """Create a Neo4j database handler for a graph."""
        logger.info(f"Creating Neo4j handler for graph {graph_id}")

        preset = get_webapp_neo4j_preset()
        if not preset:
            raise PresetNotSetException()

        db_name = create_neo4j_db_name("tmp", webapp_config.default_project_key, graph_id)
        logger.info(f"Using Neo4j database name: {db_name}")

        return Neo4DbHandler(
            uri=preset.neo4j_uri,
            username=preset.neo4j_username,
            password=preset.get_neo4j_password(),
            database=db_name,
        )

    def clear_cache(self) -> None:
        """Clear all cached handlers and cleanup their resources."""
        with self._cache_lock:
            for (graph_id, db_type), handler in list(self._cache.items()):
                try:
                    handler.cleanup_handler()
                    logger.debug(f"Cleaned up {db_type} handler for graph {graph_id}")
                except Exception as e:
                    logger.exception(f"Error cleaning up {db_type} handler for graph {graph_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,
            }


_registry_instance: Optional[EditorDbHandlerRegistry] = None


def get_db_handler_registry() -> EditorDbHandlerRegistry:
    global _registry_instance
    if _registry_instance is None:
        _registry_instance = EditorDbHandlerRegistry()
    return _registry_instance


def get_handler_for_graph(graph_id: GraphId, db_type: Optional[DbType] = None) -> DbHandler:
    logger.debug("Cache info: %s", get_db_handler_registry().get_cache_info())
    return get_db_handler_registry().get_handler(graph_id, db_type)
