"""Abstract base class for GraphStore plugins."""
from __future__ import annotations

from abc import ABC, abstractmethod
from collections import deque


class GraphStore(ABC):
    """Interface for entity graph storage backends.

    Implementations: SQLiteGraphStore, Neo4jGraphStore
    """

    # --- Node CRUD ---

    @abstractmethod
    def put_node(self, node: dict) -> None:
        """Insert or update an entity node.

        Required keys: entity_id, name, entity_type.
        """
        ...

    @abstractmethod
    def get_node(self, entity_id: str) -> dict | None:
        """Get a node by its entity_id. Returns None if not found."""
        ...

    @abstractmethod
    def get_node_by_name(self, name: str, user_id: str | None = None) -> dict | None:
        """Get an active node by name + user_id (case-insensitive).

        Returns None if not found or is_active is False.
        """
        ...

    @abstractmethod
    def delete_node(self, entity_id: str) -> bool:
        """Soft-delete a node (set is_active=False). Returns True if found."""
        ...

    # --- Edge CRUD ---

    @abstractmethod
    def put_edge(self, edge: dict) -> None:
        """Insert or update an entity edge (relationship).

        Required keys: edge_id, subject_id, subject_name, predicate,
                       object_id, object_name.
        """
        ...

    @abstractmethod
    def get_edge(self, edge_id: str) -> dict | None:
        """Get an edge by its edge_id. Returns None if not found."""
        ...

    @abstractmethod
    def delete_edge(self, edge_id: str) -> bool:
        """Soft-delete an edge (set is_active=False). Returns True if found."""
        ...

    # --- Search & Traversal ---

    @abstractmethod
    def search_edges(
        self,
        user_id: str | None = None,
        entity_name: str = "",
        predicate: str | None = None,
        direction: str = "both",
        limit: int = 20,
        partial_match: bool = False,
    ) -> list[dict]:
        """Search edges connected to an entity by name.

        Args:
            direction: "both" | "outgoing" | "incoming"
            partial_match: If True, use LIKE partial match on entity name.

        Returns list of edge dicts ordered by mention_count DESC.
        """
        ...

    @abstractmethod
    def traverse(
        self,
        user_id: str | None = None,
        start_name: str = "",
        max_hops: int = 2,
        limit_edges_per_hop: int = 100,
    ) -> tuple[list[dict], list[dict]]:
        """BFS traversal from a starting entity.

        Returns (nodes, edges) reachable within max_hops.
        Graph-native backends (Neo4j) can override with Cypher
        for O(1)-per-hop performance.
        """
        ...

    @abstractmethod
    def list_edges(
        self,
        user_id: str | None = None,
        limit: int = 100,
    ) -> list[dict]:
        """List all active edges for a user scope.

        Returns edge dicts ordered by created_at DESC.
        """
        ...

    # --- Stats (optional override) ---

    def get_stats(self, user_id: str | None = None) -> dict:
        """Return entity/edge counts and distributions. Optional override."""
        raise NotImplementedError

    # --- Shared BFS helper ---

    def _default_bfs_traverse(
        self,
        user_id: str | None = None,
        start_name: str = "",
        max_hops: int = 2,
        limit_edges_per_hop: int = 100,
    ) -> tuple[list[dict], list[dict]]:
        """Default Python-side BFS using search_edges / get_node_by_name.

        Concrete subclasses (SQLite) delegate traverse() here.
        Graph-native backends override traverse() entirely.
        """
        max_hops = max(1, min(max_hops, 3))
        visited: set[str] = set()
        queue: deque[tuple[str, int]] = deque([(start_name.strip().lower(), 0)])
        nodes: dict[str, dict] = {}
        edges: list[dict] = []
        seen_edge_ids: set[str] = set()

        while queue:
            current_name, depth = queue.popleft()
            if current_name in visited:
                continue
            visited.add(current_name)

            node = self.get_node_by_name(current_name, user_id)
            if node:
                nodes[node["entity_id"]] = node

            if depth >= max_hops:
                continue

            connected = self.search_edges(
                user_id=user_id,
                entity_name=current_name,
                direction="both",
                limit=limit_edges_per_hop,
            )
            for edge in connected:
                eid = edge.get("edge_id", "")
                if eid not in seen_edge_ids:
                    seen_edge_ids.add(eid)
                    edges.append(edge)
                subj = edge.get("subject_name", "").strip().lower()
                obj = edge.get("object_name", "").strip().lower()
                for neighbour in (subj, obj):
                    if neighbour != current_name and neighbour not in visited:
                        queue.append((neighbour, depth + 1))

        return list(nodes.values()), edges
