"""Kùzu embedded graph database service - No Docker required.

Includes automatic retry with exponential backoff for database lock errors.
KùzuDB uses file-level locking — when another process holds the database open,
operations will fail with an IO/lock error.  The retry mechanism handles this
transparently by waiting and retrying up to a configurable number of times.
"""

from __future__ import annotations

import json
import os
import time
import types
from collections.abc import Generator, Sequence
from contextlib import contextmanager
from pathlib import Path
from typing import TYPE_CHECKING, Any

from loguru import logger

from ..types import (
    GraphData,
    PropertyDict,
    PropertyValue,
    ResultRow,
)

if TYPE_CHECKING:
    import kuzu

# ---------------------------------------------------------------------------
# Lock / retry configuration
# ---------------------------------------------------------------------------

#: Maximum number of retry attempts when the database is locked.
DB_LOCK_MAX_RETRIES: int = int(os.environ.get("CGB_DB_LOCK_MAX_RETRIES", "5"))

#: Initial wait time in seconds before the first retry (doubles each attempt).
DB_LOCK_INITIAL_WAIT: float = float(os.environ.get("CGB_DB_LOCK_INITIAL_WAIT", "1.0"))

#: Maximum wait time in seconds between retries.
DB_LOCK_MAX_WAIT: float = float(os.environ.get("CGB_DB_LOCK_MAX_WAIT", "30.0"))

# Keywords that indicate a lock-related error in Kuzu exception messages.
_LOCK_ERROR_KEYWORDS = ("lock", "locked", "busy", "io error", "access", "locking")


def _is_lock_error(exc: Exception) -> bool:
    """Return True if *exc* looks like a database lock / busy error."""
    msg = str(exc).lower()
    return any(kw in msg for kw in _LOCK_ERROR_KEYWORDS)


def _remove_stale_lock(db_path: Path) -> bool:
    """Try to remove a stale .lock file inside a Kuzu database directory.

    Kuzu stores its lock as ``<db_path>/.lock`` or ``<db_path>/lock``.
    If the lock file exists but no process appears to hold it, it is removed.

    Returns True if a stale lock was cleaned up.
    """
    lock_candidates = [db_path / ".lock", db_path / "lock"]
    cleaned = False
    for lock_file in lock_candidates:
        if lock_file.exists():
            try:
                lock_file.unlink()
                logger.warning(f"Removed stale lock file: {lock_file}")
                cleaned = True
            except OSError as e:
                logger.debug(f"Could not remove lock file {lock_file}: {e}")
    return cleaned


class KuzuIngestor:
    """Ingestor for writing code graph data to Kùzu embedded database.

    Kùzu is an embedded graph database that requires no server or Docker.
    Perfect for local development and testing.

    Example:
        >>> ingestor = KuzuIngestor("./my_graph.db")
        >>> with ingestor:
        ...     ingestor.ensure_node_batch("Function", {"name": "foo", "id": "1"})
        ...     ingestor.flush_all()
        >>> # Query later
        >>> results = ingestor.query("MATCH (f:Function) RETURN f.name")
    """

    def __init__(self, db_path: str | Path, batch_size: int = 1000, read_only: bool = False):
        """Initialize Kùzu ingestor.

        Args:
            db_path: Path to store the database files
            batch_size: Batch size for writes
            read_only: If True, open the database in read-only mode.
                       Multiple processes can open the same DB concurrently
                       when all use read_only=True.
        """
        self.db_path = Path(db_path)
        self.batch_size = batch_size
        self.read_only = read_only
        self._db: kuzu.Database | None = None
        self._conn: kuzu.Connection | None = None
        self._ref_count: int = 0  # Reentrant context manager reference count
        self.node_buffer: list[tuple[str, dict[str, PropertyValue]]] = []
        self.relationship_buffer: list[
            tuple[
                tuple[str, str, PropertyValue],
                str,
                tuple[str, str, PropertyValue],
                dict[str, PropertyValue] | None,
            ]
        ] = []
        self._initialized = False

    def __enter__(self) -> KuzuIngestor:
        """Enter context manager and initialize database.

        Reentrant: if already connected, increments the reference count and
        returns immediately — no new database handle is opened.  This allows
        ``build_graph()`` to hold a long-lived session while ``query()`` calls
        within the same pipeline reuse it via nested ``with ingestor:`` blocks.

        If the database is locked by another process, retries with exponential
        backoff up to ``DB_LOCK_MAX_RETRIES`` times.  On the first lock error
        the method also attempts to clean up stale lock files.
        """
        # Reentrant: already connected — just bump the ref count
        if self._conn is not None:
            self._ref_count += 1
            return self

        import kuzu

        mode_label = "read-only" if self.read_only else "read-write"
        logger.info(f"Opening Kùzu database at {self.db_path} ({mode_label})")
        self.db_path.parent.mkdir(parents=True, exist_ok=True)

        last_exc: Exception | None = None
        for attempt in range(DB_LOCK_MAX_RETRIES + 1):
            try:
                self._db = kuzu.Database(str(self.db_path), read_only=self.read_only)
                self._conn = kuzu.Connection(self._db)
                self._ref_count = 1
                if attempt > 0:
                    logger.info(
                        f"Kùzu database opened successfully after {attempt} retries"
                    )
                else:
                    logger.info("Kùzu database opened successfully")
                return self
            except Exception as exc:
                last_exc = exc
                if not _is_lock_error(exc):
                    raise  # Non-lock error — fail immediately

                # On first lock failure, try removing stale lock files
                if attempt == 0 and self.db_path.is_dir():
                    _remove_stale_lock(self.db_path)

                if attempt < DB_LOCK_MAX_RETRIES:
                    wait = min(
                        DB_LOCK_INITIAL_WAIT * (2 ** attempt),
                        DB_LOCK_MAX_WAIT,
                    )
                    logger.warning(
                        f"Database locked (attempt {attempt + 1}/{DB_LOCK_MAX_RETRIES + 1}), "
                        f"retrying in {wait:.1f}s... ({exc})"
                    )
                    time.sleep(wait)
                    # Explicitly close before next attempt to release file handles
                    if self._conn:
                        try:
                            self._conn.close()
                        except Exception:
                            pass
                    if self._db:
                        try:
                            self._db.close()
                        except Exception:
                            pass
                    self._db = None
                    self._conn = None

        raise ConnectionError(
            f"Failed to open Kùzu database after {DB_LOCK_MAX_RETRIES + 1} attempts. "
            f"The database at {self.db_path} may be locked by another process. "
            f"Last error: {last_exc}"
        )

    def __exit__(
        self,
        exc_type: type | None,
        exc_val: Exception | None,
        exc_tb: types.TracebackType | None,
    ) -> None:
        """Exit context manager and cleanup.

        Reentrant: decrements the reference count and only performs the real
        close (flush buffers, release connection) when it reaches zero.
        """
        self._ref_count -= 1
        if self._ref_count > 0:
            # Still held by an outer ``with`` block — do nothing
            return

        if exc_type:
            logger.exception(f"Exception during ingest: {exc_val}")
            try:
                self.flush_all()
            except Exception as flush_err:
                logger.error(f"Flush error during exception handling: {flush_err}")
        else:
            self.flush_all()

        if self._conn:
            try:
                self._conn.close()
            except Exception as e:
                logger.debug(f"Error closing connection: {e}")
            self._conn = None
        if self._db:
            try:
                self._db.close()
            except Exception as e:
                logger.debug(f"Error closing database: {e}")
            self._db = None
        logger.info("Kùzu database closed")

    def _ensure_schema(self, label: str) -> None:
        """Ensure node table exists for the given label."""
        if not self._conn:
            raise ConnectionError("Not connected to database")

        # Kùzu requires predefined schema, create generic node table
        try:
            # Check if table exists by trying to query
            self._conn.execute(f"MATCH (n:{label}) RETURN n LIMIT 1")
        except Exception:
            # Table doesn't exist, create it
            # Kùzu uses CREATE NODE TABLE with specific properties
            logger.info(f"Creating node table for label: {label}")
            try:
                self._conn.execute(f"""
                    CREATE NODE TABLE {label} (
                        qualified_name STRING,
                        name STRING,
                        path STRING,
                        start_line INT64,
                        end_line INT64,
                        docstring STRING,
                        return_type STRING,
                        signature STRING,
                        visibility STRING,
                        parameters STRING[],
                        kind STRING,
                        PRIMARY KEY (qualified_name)
                    )
                """)
            except Exception as e:
                logger.debug(f"Table creation may have failed (could already exist): {e}")

    def _ensure_rel_schema(self, rel_type: str, from_label: str, to_label: str) -> None:
        """Ensure relationship table exists for the given label combination.

        Kùzu REL TABLEs are bound to specific (FROM, TO) node label pairs.
        A single rel_type (e.g., DEFINES) may need multiple tables if it
        connects different label pairs (Module→Function, Module→Class, etc.).
        Kùzu supports REL TABLE GROUP for this purpose.
        """
        if not self._conn:
            raise ConnectionError("Not connected to database")

        cache_key = (rel_type, from_label, to_label)
        if not hasattr(self, "_rel_schema_cache"):
            self._rel_schema_cache: set[tuple[str, str, str]] = set()
        if cache_key in self._rel_schema_cache:
            return
        self._rel_schema_cache.add(cache_key)

        # Try to create the relationship. If the table already exists with the
        # same (FROM, TO) pair, the CREATE will fail harmlessly.
        # If the table exists but with a DIFFERENT pair, we need ALTER or GROUP.
        try:
            self._conn.execute(f"""
                CREATE REL TABLE {rel_type} (
                    FROM {from_label} TO {to_label},
                    MANY_MANY
                )
            """)
            logger.info(f"Created relationship table: {rel_type} ({from_label}→{to_label})")
        except Exception:
            # Table already exists — check if it actually supports this label pair
            # by inspecting show_connection() rather than a probe MATCH (which
            # returns empty instead of raising when the pair is unsupported).
            pair_supported = False
            try:
                result = self._conn.execute(
                    f'CALL show_connection("{rel_type}") RETURN *'
                )
                while result.has_next():
                    row = result.get_next()
                    # row is a list: [source_table_name, dest_table_name, src_pk, dst_pk]
                    if len(row) >= 2 and row[0] == from_label and row[1] == to_label:
                        pair_supported = True
                        break
            except Exception as e:
                logger.debug(f"show_connection check failed for {rel_type}: {e}")

            if not pair_supported:
                # Label pair not supported — create a separate REL TABLE
                group_name = f"{rel_type}_{from_label}_{to_label}"
                try:
                    self._conn.execute(f"""
                        CREATE REL TABLE {group_name} (
                            FROM {from_label} TO {to_label},
                            MANY_MANY
                        )
                    """)
                    logger.info(f"Created additional rel table: {group_name} ({from_label}→{to_label})")
                    if not hasattr(self, "_rel_table_overrides"):
                        self._rel_table_overrides: dict[tuple[str, str, str], str] = {}
                    self._rel_table_overrides[(rel_type, from_label, to_label)] = group_name
                except Exception as e:
                    logger.debug(f"Additional rel table creation failed: {e}")

    def ensure_node_batch(self, label: str, properties: PropertyDict) -> None:
        """Add a node to the batch buffer.

        Args:
            label: Node label (e.g., "Function", "Class")
            properties: Node properties dictionary
        """
        self.node_buffer.append((label, properties.copy()))
        if len(self.node_buffer) >= self.batch_size:
            self.flush_nodes()

    def ensure_relationship_batch(
        self,
        source: tuple[str, str, PropertyValue],
        rel_type: str,
        target: tuple[str, str, PropertyValue],
        properties: PropertyDict | None = None,
    ) -> None:
        """Add a relationship to the batch buffer.

        Args:
            source: (label, key, value) tuple for source node
            rel_type: Relationship type (e.g., "CALLS", "DEFINES")
            target: (label, key, value) tuple for target node
            properties: Optional relationship properties
        """
        self.relationship_buffer.append((source, rel_type, target, properties))
        if len(self.relationship_buffer) >= self.batch_size:
            # Flush nodes first — rel table creation requires node tables to exist
            if self.node_buffer:
                self.flush_nodes()
            self.flush_relationships()

    def _value_to_cypher(self, value: PropertyValue) -> str:
        """Convert Python value to Cypher literal.

        Handles backslashes (Windows paths), newlines, single quotes,
        and other special characters that would break Cypher syntax.
        """
        if value is None:
            return "NULL"
        if isinstance(value, bool):
            return str(value).lower()
        if isinstance(value, (int, float)):
            return str(value)
        if isinstance(value, str):
            # Escape backslashes FIRST (before escaping quotes)
            escaped = value.replace("\\", "\\\\")
            # Escape single quotes
            escaped = escaped.replace("'", "\\'")
            # Replace newlines and tabs with escaped versions
            escaped = escaped.replace("\n", "\\n")
            escaped = escaped.replace("\r", "\\r")
            escaped = escaped.replace("\t", "\\t")
            return f"'{escaped}'"
        if isinstance(value, list):
            items = [self._value_to_cypher(v) for v in value]
            return f"[{', '.join(items)}]"
        return self._value_to_cypher(str(value))

    def flush_nodes(self) -> None:
        """Flush node buffer to database."""
        if not self.node_buffer or not self._conn:
            return

        # Group nodes by label
        by_label: dict[str, list[PropertyDict]] = {}
        for label, props in self.node_buffer:
            if label not in by_label:
                by_label[label] = []
            by_label[label].append(props)

        for label, nodes in by_label.items():
            self._ensure_schema(label)

            for props in nodes:
                # Build CREATE statement with safe type coercion
                qualified_name = props.get("qualified_name", props.get("name", ""))
                name = props.get("name", "")
                path = props.get("path", "")
                start_line = int(props.get("start_line") or 0)
                end_line = int(props.get("end_line") or 0)
                docstring = props.get("docstring", "") or ""
                return_type = props.get("return_type", "") or ""
                signature = props.get("signature", "") or ""
                visibility = props.get("visibility", "") or ""
                parameters = props.get("parameters")
                kind = props.get("kind", "") or ""

                try:
                    cypher = f"""
                        MERGE (n:{label} {{
                            qualified_name: {self._value_to_cypher(qualified_name)}
                        }})
                        ON CREATE SET
                            n.name = {self._value_to_cypher(name)},
                            n.path = {self._value_to_cypher(path)},
                            n.start_line = {start_line},
                            n.end_line = {end_line},
                            n.docstring = {self._value_to_cypher(docstring)},
                            n.return_type = {self._value_to_cypher(return_type)},
                            n.signature = {self._value_to_cypher(signature)},
                            n.visibility = {self._value_to_cypher(visibility)},
                            n.parameters = {self._value_to_cypher(parameters if parameters else [])},
                            n.kind = {self._value_to_cypher(kind)}
                        ON MATCH SET
                            n.name = {self._value_to_cypher(name)},
                            n.path = {self._value_to_cypher(path)},
                            n.start_line = {start_line},
                            n.end_line = {end_line},
                            n.docstring = {self._value_to_cypher(docstring)},
                            n.return_type = {self._value_to_cypher(return_type)},
                            n.signature = {self._value_to_cypher(signature)},
                            n.visibility = {self._value_to_cypher(visibility)},
                            n.parameters = {self._value_to_cypher(parameters if parameters else [])},
                            n.kind = {self._value_to_cypher(kind)}
                    """
                    self._execute_with_retry(cypher)
                except Exception as e:
                    logger.warning(
                        f"Failed to create {label} node "
                        f"(qn={qualified_name!r}, name={name!r}): {e}"
                    )

        logger.debug(f"Flushed {len(self.node_buffer)} nodes")
        self.node_buffer = []

    def flush_relationships(self) -> None:
        """Flush relationship buffer to database."""
        if not self.relationship_buffer or not self._conn:
            return

        seen: set[tuple] = set()
        for source, rel_type, target, _props in self.relationship_buffer:
            from_label, from_key, from_val = source
            to_label, to_key, to_val = target

            dedup_key = (from_val, rel_type, to_val)
            if dedup_key in seen:
                continue
            seen.add(dedup_key)

            self._ensure_rel_schema(rel_type, from_label, to_label)

            # Use override table name if one was created for this label pair
            actual_rel = rel_type
            overrides = getattr(self, "_rel_table_overrides", {})
            override_key = (rel_type, from_label, to_label)
            if override_key in overrides:
                actual_rel = overrides[override_key]

            try:
                cypher = f"""
                    MATCH (a:{from_label} {{{from_key}: {self._value_to_cypher(from_val)}}}),
                          (b:{to_label} {{{to_key}: {self._value_to_cypher(to_val)}}})
                    CREATE (a)-[:{actual_rel}]->(b)
                """
                self._execute_with_retry(cypher)
            except Exception as e:
                logger.warning(
                    f"Failed to create relationship {actual_rel}: "
                    f"{from_label}({from_val!r}) -> {to_label}({to_val!r}): {e}"
                )

        logger.debug(f"Flushed {len(self.relationship_buffer)} relationships")
        self.relationship_buffer = []

    def flush_all(self) -> None:
        """Flush all pending data."""
        self.flush_nodes()
        self.flush_relationships()

    def _execute_with_retry(self, cypher_query: str, params: PropertyDict | None = None) -> Any:
        """Execute a Cypher query with automatic retry on lock errors.

        Returns the raw Kuzu QueryResult on success.
        Raises the original exception after exhausting retries.
        """
        if not self._conn:
            raise ConnectionError("Not connected to database")

        last_exc: Exception | None = None
        for attempt in range(DB_LOCK_MAX_RETRIES + 1):
            try:
                return self._conn.execute(cypher_query, parameters=params or {})
            except Exception as exc:
                last_exc = exc
                if not _is_lock_error(exc):
                    raise  # Non-lock error — fail immediately

                if attempt < DB_LOCK_MAX_RETRIES:
                    wait = min(
                        DB_LOCK_INITIAL_WAIT * (2 ** attempt),
                        DB_LOCK_MAX_WAIT,
                    )
                    logger.warning(
                        f"Query blocked by lock (attempt {attempt + 1}/{DB_LOCK_MAX_RETRIES + 1}), "
                        f"retrying in {wait:.1f}s..."
                    )
                    time.sleep(wait)

                    # Try to reconnect if connection went stale
                    self._try_reconnect()

        raise last_exc  # type: ignore[misc]

    def _try_reconnect(self) -> None:
        """Attempt to re-establish the database connection."""
        if self._db is None:
            return
        # Close stale connection first to release file handles (critical on Windows)
        if self._conn:
            try:
                self._conn.close()
            except Exception:
                pass
        try:
            import kuzu
            self._conn = kuzu.Connection(self._db)
            logger.info("Reconnected to Kùzu database")
        except Exception as e:
            logger.debug(f"Reconnect attempt failed: {e}")

    def query(self, cypher_query: str, params: PropertyDict | None = None) -> list[ResultRow]:
        """Execute a Cypher query.

        Automatically retries with exponential backoff if the database is locked.

        Args:
            cypher_query: Cypher query string
            params: Optional query parameters

        Returns:
            List of result rows as dictionaries
        """
        try:
            result = self._execute_with_retry(cypher_query, params)
            col_names = result.get_column_names()
            rows = []
            while result.has_next():
                row = result.get_next()
                if col_names:
                    rows.append(dict(zip(col_names, row)))
                else:
                    rows.append({"result": row})
            return rows
        except Exception as e:
            logger.error(f"Query error: {e}")
            return []

    def fetch_all(self, cypher_query: str, params: PropertyDict | None = None) -> list[ResultRow]:
        """Alias for query(); satisfies GraphServiceProtocol."""
        return self.query(cypher_query, params)

    def fetch_module_apis(
        self,
        module_qn: str | None = None,
        visibility: str | None = "public",
    ) -> list[ResultRow]:
        """Fetch API interfaces (functions) for a module or the entire project.

        Args:
            module_qn: Qualified name of a module. If None, returns APIs across all modules.
            visibility: Filter by visibility ("public", "static", or None for all).

        Returns:
            List of result rows with function name, signature, return_type, etc.
        """
        if not self._conn:
            raise ConnectionError("Not connected to database")

        conditions: list[str] = []
        if module_qn:
            safe_qn = module_qn.replace("'", "\\'")
            conditions.append(f"m.qualified_name = '{safe_qn}'")
        # Note: Function nodes in C graphs may not have a visibility property;
        # skip that filter to avoid schema errors.

        where_clause = f"WHERE {' AND '.join(conditions)}" if conditions else ""

        # Use only universally-available properties to avoid schema mismatches.
        # (Kuzu silently returns empty results when a non-existent property is
        # referenced, rather than raising an error.)
        # The caller uses `signature or name` for display, so returning '' is fine.
        cypher = f"""
            MATCH (m:Module)-[:DEFINES]->(f:Function)
            {where_clause}
            RETURN m.qualified_name AS module,
                   f.name AS name,
                   '' AS signature,
                   '' AS return_type,
                   '' AS visibility,
                   '' AS parameters,
                   f.start_line AS start_line,
                   f.end_line AS end_line
            ORDER BY m.qualified_name, f.start_line
        """

        try:
            return self.query(cypher)
        except Exception as e:
            logger.error(f"fetch_module_apis error: {e}")
            return []

    def fetch_module_type_apis(
        self,
        module_qn: str | None = None,
    ) -> list[ResultRow]:
        """Fetch type API interfaces (structs, unions, enums, typedefs) for a module.

        Args:
            module_qn: Qualified name of a module. If None, returns across all modules.

        Returns:
            List of result rows with type name, kind, signature, members, etc.
        """
        if not self._conn:
            raise ConnectionError("Not connected to database")

        # Query Class nodes (struct/union/enum)
        class_conditions: list[str] = []
        if module_qn:
            safe_qn = module_qn.replace("'", "\\'")
            class_conditions.append(f"m.qualified_name = '{safe_qn}'")

        class_where = f"WHERE {' AND '.join(class_conditions)}" if class_conditions else ""

        # Class nodes only store name/path/start_line/end_line/docstring in C graphs.
        class_cypher = f"""
            MATCH (m:Module)-[:DEFINES]->(c:Class)
            {class_where}
            RETURN m.qualified_name AS module,
                   c.name AS name,
                   'struct' AS kind,
                   '' AS signature,
                   '' AS members,
                   c.start_line AS start_line,
                   c.end_line AS end_line
            ORDER BY m.qualified_name, c.start_line
        """

        # Query Type nodes (typedefs)
        type_conditions: list[str] = []
        if module_qn:
            safe_qn = module_qn.replace("'", "\\'")
            type_conditions.append(f"m.qualified_name = '{safe_qn}'")

        type_where = f"WHERE {' AND '.join(type_conditions)}" if type_conditions else ""

        type_cypher = f"""
            MATCH (m:Module)-[:DEFINES]->(t:Type)
            {type_where}
            RETURN m.qualified_name AS module,
                   t.name AS name,
                   t.kind AS kind,
                   t.signature AS signature,
                   t.start_line AS start_line,
                   t.end_line AS end_line
            ORDER BY m.qualified_name, t.start_line
        """

        try:
            class_rows = self.query(class_cypher)
        except Exception as e:
            logger.error(f"fetch_module_type_apis (class) error: {e}")
            class_rows = []
        try:
            type_rows = self.query(type_cypher)
        except Exception as e:
            logger.error(f"fetch_module_type_apis (type) error: {e}")
            type_rows = []
        return class_rows + type_rows

    def fetch_all_calls(self) -> list[ResultRow]:
        """Fetch all CALLS relationships in the graph.

        Returns a list of rows, each containing 7 fields:
        [caller_qn, callee_qn, callee_path, callee_start_line,
         caller_path, caller_start_line, caller_end_line].

        This matches the field order expected by
        ``api_doc_generator._build_call_graph()``.
        """
        if not self._conn:
            raise ConnectionError("Not connected to database")

        cypher = """
            MATCH (caller:Function)-[:CALLS]->(callee:Function)
            RETURN DISTINCT caller.qualified_name AS caller_qn,
                   callee.qualified_name AS callee_qn,
                   callee.path AS callee_path,
                   callee.start_line AS callee_start_line,
                   caller.path AS caller_path,
                   caller.start_line AS caller_start_line,
                   caller.end_line AS caller_end_line
        """
        try:
            return self.query(cypher)
        except Exception as e:
            logger.error(f"fetch_all_calls error: {e}")
            return []

    def fetch_all_functions_for_docs(self) -> list[ResultRow]:
        """Fetch all functions with module info, docstring, and path for doc generation.

        Returns rows with:
        [module_qn, module_path, func_qn, func_name, signature, return_type,
         visibility, parameters, docstring, start_line, end_line, path].
        """
        if not self._conn:
            raise ConnectionError("Not connected to database")

        cypher = """
            MATCH (m:Module)-[:DEFINES]->(f:Function)
            RETURN DISTINCT m.qualified_name AS module_qn,
                   m.path AS module_path,
                   f.qualified_name AS func_qn,
                   f.name AS func_name,
                   f.signature AS signature,
                   f.return_type AS return_type,
                   f.visibility AS visibility,
                   f.parameters AS parameters,
                   f.docstring AS docstring,
                   f.start_line AS start_line,
                   f.end_line AS end_line,
                   f.path AS path
            ORDER BY m.qualified_name, f.start_line
        """
        try:
            return self.query(cypher)
        except Exception as e:
            logger.error(f"fetch_all_functions_for_docs error: {e}")
            return []

    def fetch_all_types_for_docs(self) -> list[ResultRow]:
        """Fetch all type definitions (structs, unions, enums, typedefs) for doc generation."""
        if not self._conn:
            raise ConnectionError("Not connected to database")

        class_cypher = """
            MATCH (m:Module)-[:DEFINES]->(c:Class)
            RETURN m.qualified_name AS module_qn,
                   c.name AS name,
                   c.kind AS kind,
                   c.signature AS signature,
                   c.parameters AS members,
                   c.start_line AS start_line,
                   c.end_line AS end_line
            ORDER BY m.qualified_name, c.start_line
        """
        type_cypher = """
            MATCH (m:Module)-[:DEFINES]->(t:Type)
            RETURN m.qualified_name AS module_qn,
                   t.name AS name,
                   t.kind AS kind,
                   t.signature AS signature,
                   t.start_line AS start_line,
                   t.end_line AS end_line
            ORDER BY m.qualified_name, t.start_line
        """
        try:
            return self.query(class_cypher) + self.query(type_cypher)
        except Exception as e:
            logger.error(f"fetch_all_types_for_docs error: {e}")
            return []

    def clean_database(self) -> None:
        """Clean all data from the database."""
        if not self._conn:
            raise ConnectionError("Not connected to database")

        try:
            # Drop all tables
            result = self._execute_with_retry("CALL show_tables() RETURN *")
            tables = []
            while result.has_next():
                row = result.get_next()
                tables.append(row[0] if row else None)

            for table in tables:
                if table:
                    try:
                        # Quote table name to handle special cases (e.g., numeric names)
                        self._execute_with_retry(f'DROP TABLE "{table}"')
                    except Exception as e:
                        logger.debug(f"Error dropping table {table}: {e}")

            logger.info("Database cleaned")
        except Exception as e:
            logger.error(f"Error cleaning database: {e}")

    def export_graph(self) -> GraphData:
        """Export the entire graph as GraphData."""
        if not self._conn:
            raise ConnectionError("Not connected to database")

        nodes = []
        relationships = []

        try:
            # Get all nodes
            result = self._execute_with_retry("MATCH (n) RETURN n")
            while result.has_next():
                row = result.get_next()
                if row and len(row) > 0:
                    node = row[0]
                    nodes.append({
                        "label": node.get("_label", "Unknown"),
                        "properties": dict(node),
                    })

            # Get all relationships
            result = self._execute_with_retry("MATCH (a)-[r]->(b) RETURN a, r, b")
            while result.has_next():
                row = result.get_next()
                if row and len(row) >= 3:
                    relationships.append({
                        "source": {"qualified_name": row[0].get("qualified_name", "")},
                        "type": row[1].get("_label", "UNKNOWN"),
                        "target": {"qualified_name": row[2].get("qualified_name", "")},
                    })

        except Exception as e:
            logger.error(f"Export error: {e}")

        return {"nodes": nodes, "relationships": relationships}

    def get_statistics(self) -> dict[str, Any]:
        """Get database statistics."""
        if not self._conn:
            raise ConnectionError("Not connected to database")

        stats: dict[str, Any] = {
            "node_count": 0,
            "relationship_count": 0,
            "node_labels": {},
            "relationship_types": {},
        }

        try:
            # Count nodes
            result = self._execute_with_retry("MATCH (n) RETURN count(n) as count")
            if result.has_next():
                stats["node_count"] = result.get_next()[0]

            # Count relationships
            result = self._execute_with_retry("MATCH ()-[r]->() RETURN count(r) as count")
            if result.has_next():
                stats["relationship_count"] = result.get_next()[0]

            # Get labels with counts
            result = self._execute_with_retry("CALL show_tables() RETURN *")
            while result.has_next():
                row = result.get_next()
                if row:
                    label = row[0]
                    # Count nodes for this label
                    try:
                        count_result = self._execute_with_retry(
                            f"MATCH (n:{label}) RETURN count(n) as count"
                        )
                        if count_result.has_next():
                            count = count_result.get_next()[0]
                            stats["node_labels"][label] = count
                    except Exception:
                        stats["node_labels"][label] = 0

        except Exception as e:
            logger.error(f"Statistics error: {e}")

        return stats
