"""Graph Bridge — optional integration with DataBridge's 8 internal graph systems.

Wraps each graph system with try/except ImportError so the SDK works
with or without the full DataBridge engine installed.  When available,
workflows gain persistent memory: lineage tracking, evidence trails,
vector search, knowledge base graphs, entity linking, and Codex events.

Graph systems mapped to SDK workflows:
    1. LineageTracker          — all workflows (data provenance DAG)
    2. EvidenceTracker         — recon, detection, quality, fix_generation
    3. GraphRAG Vectors        — all workflows (semantic search index)
    4. KBIngestionPipeline     — recon, detection, profiling, catalog, blce
    5. Entity Linker           — blce.full_engine, blce.logic_extraction
    6. Codex GraphRAG          — detection workflows (runtime events)
    7. IKE Graph               — institutional knowledge graph via IKE APIs
    8. Formula Dependency Graph — Excel dependency graph via IKE P6 APIs
"""

from __future__ import annotations

import json
import logging
import uuid
from collections import deque
from pathlib import Path
from typing import Any, Dict, List, Optional, Set, Tuple

from databridge_core.sdk.core.ike_bridge import IKEBridge
from databridge_core.sdk.core.knowledge_router import KnowledgeRouter

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# 1. LineageTracker
# ---------------------------------------------------------------------------
_LINEAGE_AVAILABLE = False
_LineageTracker = None
_NodeType = None
_TransformationType = None

try:
    from src.lineage.lineage_tracker import LineageTracker as _LineageTrackerCls
    from src.lineage.types import NodeType as _NodeTypeCls
    from src.lineage.types import TransformationType as _TransformationTypeCls

    _LINEAGE_AVAILABLE = True
    _LineageTracker = _LineageTrackerCls
    _NodeType = _NodeTypeCls
    _TransformationType = _TransformationTypeCls
except ImportError:
    pass

# ---------------------------------------------------------------------------
# 2. EvidenceTracker
# ---------------------------------------------------------------------------
_EVIDENCE_AVAILABLE = False
_EvidenceTracker = None

try:
    from src.graphrag.evidence import EvidenceTracker as _EvidenceTrackerCls

    _EVIDENCE_AVAILABLE = True
    _EvidenceTracker = _EvidenceTrackerCls
except ImportError:
    pass

# ---------------------------------------------------------------------------
# 3. GraphRAG Vector Store
# ---------------------------------------------------------------------------
_VECTOR_AVAILABLE = False
_get_vector_store = None
_get_embedding_provider = None

try:
    from src.graphrag.vector_store import get_vector_store as _get_vs_fn
    from src.graphrag.embedding_provider import get_embedding_provider as _get_ep_fn

    _VECTOR_AVAILABLE = True
    _get_vector_store = _get_vs_fn
    _get_embedding_provider = _get_ep_fn
except ImportError:
    pass

# ---------------------------------------------------------------------------
# 4. Knowledge Base Ingestion
# ---------------------------------------------------------------------------
_KB_AVAILABLE = False
_KBIngestionPipeline = None
_GraphNode = None
_GraphEdge = None

try:
    from src.knowledge_base.ingestion import KBIngestionPipeline as _KBPipelineCls
    from src.knowledge_base.types import GraphNode as _GraphNodeCls
    from src.knowledge_base.types import GraphEdge as _GraphEdgeCls

    _KB_AVAILABLE = True
    _KBIngestionPipeline = _KBPipelineCls
    _GraphNode = _GraphNodeCls
    _GraphEdge = _GraphEdgeCls
except ImportError:
    pass

# ---------------------------------------------------------------------------
# 5. Entity Linker
# ---------------------------------------------------------------------------
_LINKER_AVAILABLE = False
_link_entities_incremental = None
_publish_clusters_to_graph = None

try:
    from databridge.linker import link_entities_incremental as _link_fn
    from databridge.linker import publish_clusters_to_graph as _publish_fn

    _LINKER_AVAILABLE = True
    _link_entities_incremental = _link_fn
    _publish_clusters_to_graph = _publish_fn
except ImportError:
    pass

# ---------------------------------------------------------------------------
# 6. Codex GraphRAG
# ---------------------------------------------------------------------------
_CODEX_AVAILABLE = False
_ingest_discrepancy_event = None

try:
    from src.codex_graphrag.core import ingest_discrepancy_event as _codex_fn

    _CODEX_AVAILABLE = True
    _ingest_discrepancy_event = _codex_fn
except ImportError:
    pass

# ---------------------------------------------------------------------------
# 7-8. IKE Graph + Formula Dependency Graph (via IKE API client)
# ---------------------------------------------------------------------------
_IKE_AVAILABLE = False
_IKEClient = None

try:
    from ike.client.sync_client import IKEClient as _IKEClientCls

    _IKE_AVAILABLE = True
    _IKEClient = _IKEClientCls
except ImportError:
    try:
        # Source-tree fallback (repo layout: ike/ike/client/sync_client.py)
        from ike.ike.client.sync_client import IKEClient as _IKEClientCls

        _IKE_AVAILABLE = True
        _IKEClient = _IKEClientCls
    except ImportError:
        pass


def graphs_available() -> Dict[str, bool]:
    """Return availability status for each graph system."""
    return {
        "lineage_tracker": _LINEAGE_AVAILABLE,
        "evidence_tracker": _EVIDENCE_AVAILABLE,
        "graphrag_vectors": _VECTOR_AVAILABLE,
        "kb_ingestion_pipeline": _KB_AVAILABLE,
        "entity_linker": _LINKER_AVAILABLE,
        "codex_graphrag": _CODEX_AVAILABLE,
        "ike_graph": _IKE_AVAILABLE,
        "formula_dependency_graph": _IKE_AVAILABLE,
    }


class GraphBridge:
    """Bridge SDK workflows to DataBridge's 8 internal graph systems.

    Follows the same pattern as ``IKEBridge``:
    - try/except ImportError at module level for each system
    - Constructor accepts ``SDKConfig`` and lazily initializes each backend
    - Every method guards with ``if not self._xxx: return None``
    - All methods return ``Optional[...]`` — safe to call even when unavailable

    Usage::

        bridge = GraphBridge(config)
        if bridge.available:
            bridge.record_lineage_node("sdk", "my_table", "TABLE")
            bridge.index_content("doc_1", "summary text", "recon_result", {})
    """

    def __init__(self, config: Any = None):
        self._lineage = None
        self._evidence = None
        self._vector_store = None
        self._embedder = None
        self._kb_pipeline = None
        self._ike_bridge = None
        self._knowledge_router = None
        self._ike_client = None
        self._config = config

        # Per-system enable flags (default True when not configured)
        _en_lineage = getattr(config, "enable_lineage", True) if config else True
        _en_evidence = getattr(config, "enable_evidence", True) if config else True
        _en_vectors = getattr(config, "enable_vectors", True) if config else True
        _en_kb = getattr(config, "enable_kb_graph", True) if config else True
        self._en_linker = getattr(config, "enable_entity_linker", True) if config else True
        self._en_codex = getattr(config, "enable_codex", True) if config else True
        self._en_ike_graph = getattr(config, "enable_ike_graph", False) if config else False
        self._en_formula_graph = getattr(config, "enable_formula_graph", False) if config else False
        self._knowledge_backend = getattr(config, "knowledge_backend", "legacy") if config else "legacy"
        self._legacy_graph_vector_enabled = (
            getattr(config, "legacy_graph_vector_enabled", True) if config else True
        )
        self._legacy_knowledge_enabled = (
            self._legacy_graph_vector_enabled and self._knowledge_backend in {"legacy", "hybrid"}
        )

        ike_enabled = getattr(config, "enable_ike", False) if config else False
        ike_endpoint = getattr(config, "ike_endpoint", None) if config else None
        ike_tenant_id = getattr(config, "ike_tenant_id", "default") if config else "default"

        if ike_enabled and ike_endpoint:
            try:
                self._ike_bridge = IKEBridge(
                    endpoint=str(ike_endpoint),
                    tenant_id=str(ike_tenant_id),
                )
                logger.info("Graph bridge: IKE knowledge bridge initialized")
            except Exception as exc:
                logger.warning("Graph bridge: IKE knowledge bridge init failed: %s", exc)
                self._ike_bridge = None

        # --- Lineage ---
        if _LINEAGE_AVAILABLE and _en_lineage:
            try:
                output_dir = None
                if config and hasattr(config, "lineage_output_dir") and config.lineage_output_dir:
                    output_dir = str(config.lineage_output_dir)
                self._lineage = _LineageTracker(output_dir=output_dir)
                logger.info("Graph bridge: LineageTracker initialized")
            except Exception as exc:
                logger.warning("Graph bridge: LineageTracker init failed: %s", exc)

        # --- Vector Store + Embedder ---
        if _VECTOR_AVAILABLE and _en_vectors and self._legacy_knowledge_enabled:
            try:
                from src.graphrag.types import EmbeddingProvider, VectorStoreType
                self._embedder = _get_embedding_provider(EmbeddingProvider.HUGGINGFACE)
                self._vector_store = _get_vector_store(VectorStoreType.SQLITE)
                logger.info("Graph bridge: Vector store initialized")
            except Exception as exc:
                logger.warning("Graph bridge: Vector store init failed: %s", exc)
                self._embedder = None
                self._vector_store = None

        # --- Evidence (depends on lineage + vector store) ---
        if _EVIDENCE_AVAILABLE and _en_evidence:
            try:
                self._evidence = _EvidenceTracker(
                    lineage_tracker=self._lineage,
                    vector_store=self._vector_store,
                    embedder=self._embedder,
                )
                logger.info("Graph bridge: EvidenceTracker initialized")
            except Exception as exc:
                logger.warning("Graph bridge: EvidenceTracker init failed: %s", exc)

        # --- Knowledge Base ---
        if _KB_AVAILABLE and _en_kb and self._legacy_knowledge_enabled:
            try:
                base_dir = None
                if config and hasattr(config, "kb_base_dir") and config.kb_base_dir:
                    base_dir = str(config.kb_base_dir)
                self._kb_pipeline = _KBIngestionPipeline(base_dir=base_dir) if base_dir else _KBIngestionPipeline()
                logger.info("Graph bridge: KBIngestionPipeline initialized")
            except Exception as exc:
                logger.warning("Graph bridge: KBIngestionPipeline init failed: %s", exc)

        # --- IKE client for systems 7-8 ---
        if _IKE_AVAILABLE and (self._en_ike_graph or self._en_formula_graph):
            try:
                if ike_enabled and ike_endpoint:
                    self._ike_client = _IKEClient(
                        base_url=str(ike_endpoint),
                        tenant_id=str(ike_tenant_id),
                    )
                    logger.info("Graph bridge: IKE client initialized for ike_graph/formula_graph")
            except Exception as exc:
                logger.warning("Graph bridge: IKE client init failed: %s", exc)
                self._ike_client = None

        self._knowledge_router = KnowledgeRouter(
            config=config,
            ike_bridge=self._ike_bridge,
            vector_store=self._vector_store,
            embedder=self._embedder,
        )

    # --- Properties ---

    @property
    def available(self) -> bool:
        """True if at least one graph system is available and enabled."""
        return any([
            self._lineage is not None,
            self._evidence is not None,
            self.knowledge_available,
            self._vector_store is not None,
            self._kb_pipeline is not None,
            _LINKER_AVAILABLE and self._en_linker,
            _CODEX_AVAILABLE and self._en_codex,
            self.ike_graph_available,
            self.formula_graph_available,
        ])

    @property
    def lineage_available(self) -> bool:
        return self._lineage is not None

    @property
    def evidence_available(self) -> bool:
        return self._evidence is not None

    @property
    def vector_available(self) -> bool:
        return self._vector_store is not None and self._embedder is not None

    @property
    def knowledge_available(self) -> bool:
        return self._knowledge_router is not None and self._knowledge_router.available

    @property
    def kb_available(self) -> bool:
        return self._kb_pipeline is not None

    @property
    def linker_available(self) -> bool:
        return _LINKER_AVAILABLE and self._en_linker

    @property
    def codex_available(self) -> bool:
        return _CODEX_AVAILABLE and self._en_codex

    @property
    def ike_graph_available(self) -> bool:
        return _IKE_AVAILABLE and self._en_ike_graph and self._ike_client is not None

    @property
    def formula_graph_available(self) -> bool:
        return _IKE_AVAILABLE and self._en_formula_graph and self._ike_client is not None

    def close(self) -> None:
        """Release resources held by graph subsystems."""
        self._lineage = None
        self._evidence = None
        self._vector_store = None
        self._embedder = None
        self._kb_pipeline = None
        if self._ike_bridge is not None:
            try:
                self._ike_bridge.close()
            except Exception:
                pass
        self._ike_bridge = None
        self._knowledge_router = None
        if self._ike_client is not None:
            try:
                self._ike_client.close()
            except Exception:
                pass
        self._ike_client = None

    # ===================================================================
    # 1. Lineage Tracker
    # ===================================================================

    def record_lineage_node(
        self,
        graph_name: str,
        name: str,
        node_type: str,
        database: Optional[str] = None,
        schema_name: Optional[str] = None,
        columns: Optional[List[Dict[str, Any]]] = None,
        description: Optional[str] = None,
        tags: Optional[List[str]] = None,
        trace_id: Optional[str] = None,
    ) -> Optional[Dict[str, Any]]:
        """Add a node to the lineage graph.

        Args:
            graph_name: Lineage graph name (e.g. ``"sdk"``, ``"default"``).
            name: Object name.
            node_type: One of LineageTracker NodeType values
                (``TABLE``, ``VIEW``, ``HIERARCHY``, ``COLUMN``, etc.).
            database: Database context.
            schema_name: Schema context.
            columns: Column definitions ``[{"name": ..., "data_type": ...}]``.
            description: Node description.
            tags: Tags for classification.

        Returns:
            Node dict or None if lineage is unavailable.
        """
        if not self._lineage:
            logger.debug("Lineage unavailable — skipping node recording")
            return None
        try:
            nt = _NodeType(node_type) if _NodeType else node_type
            merged_tags = list(tags or [])
            if trace_id:
                merged_tags.append(f"trace:{trace_id}")
            node = self._lineage.add_node(
                graph_name=graph_name,
                name=name,
                node_type=nt,
                database=database,
                schema_name=schema_name,
                columns=columns,
                description=description,
                tags=merged_tags or None,
            )
            return node.to_dict()
        except Exception as exc:
            logger.warning("Lineage node recording failed: %s", exc)
            return None

    def record_lineage_edge(
        self,
        graph_name: str,
        source: str,
        target: str,
        transform_type: str = "DIRECT",
        description: Optional[str] = None,
    ) -> Optional[Dict[str, Any]]:
        """Add an edge between lineage nodes.

        Args:
            graph_name: Lineage graph name.
            source: Source node ID or name.
            target: Target node ID or name.
            transform_type: ``DIRECT``, ``AGGREGATION``, ``CALCULATION``,
                ``FILTER``, ``JOIN``, ``DERIVED``, etc.
            description: Edge description.

        Returns:
            Edge dict or None if lineage is unavailable.
        """
        if not self._lineage:
            logger.debug("Lineage unavailable — skipping edge recording")
            return None
        try:
            tt = _TransformationType(transform_type) if _TransformationType else transform_type
            edge = self._lineage.add_edge(
                graph_name=graph_name,
                source_node=source,
                target_node=target,
                transformation_type=tt,
                description=description,
            )
            return edge.to_dict()
        except Exception as exc:
            logger.warning("Lineage edge recording failed: %s", exc)
            return None

    # ===================================================================
    # 2. Evidence Trail
    # ===================================================================

    def record_evidence(
        self,
        account_id: str,
        hierarchy_node_id: str,
        method: str,
        confidence: float,
        evidence_sources: List[Dict[str, Any]],
        project_id: str = "",
        trace_id: Optional[str] = None,
    ) -> Optional[Dict[str, Any]]:
        """Record a mapping decision with evidence trail.

        Args:
            account_id: Account being mapped.
            hierarchy_node_id: Target hierarchy node.
            method: Mapping method (``fuzzy_match``, ``code_prefix``,
                ``ai_classification``, ``manual``).
            confidence: Decision confidence 0.0–1.0.
            evidence_sources: List of evidence dicts with keys:
                ``content``, ``source_type``, ``source_id``, ``confidence``.
            project_id: Project scope.

        Returns:
            Decision node dict or None.
        """
        if not self._evidence:
            logger.debug("Evidence tracker unavailable — skipping evidence recording")
            return None
        try:
            decision = self._evidence.record_mapping_decision(
                account_id=account_id,
                hierarchy_node_id=hierarchy_node_id,
                method=method,
                confidence=confidence,
                evidence_sources=evidence_sources,
                project_id=project_id,
            )
            return {
                "decision_id": decision.id,
                "account_id": decision.account_id,
                "method": decision.method,
                "confidence": decision.confidence,
                "evidence_count": len(decision.evidence_ids),
            }
        except Exception as exc:
            logger.warning("Evidence recording failed: %s", exc)
            return None

    def record_recon_evidence(
        self,
        recon_id: str,
        match_rate: float,
        orphans: int,
        conflicts: int,
        source_file: str,
        target_file: str,
    ) -> Optional[Dict[str, Any]]:
        """Record reconciliation outcome as evidence.

        Args:
            recon_id: Reconciliation run identifier.
            match_rate: Percentage of matched records (0.0–1.0).
            orphans: Number of orphan records.
            conflicts: Number of conflict records.
            source_file: Source file path.
            target_file: Target file path.

        Returns:
            Recon evidence dict or None.
        """
        if not self._evidence:
            logger.debug("Evidence tracker unavailable — skipping recon evidence")
            return None
        try:
            result = self._evidence.record_recon_outcome(
                recon_id=recon_id,
                match_rate=match_rate,
                orphans=orphans,
                conflicts=conflicts,
                source_file=source_file,
                target_file=target_file,
            )
            return result
        except Exception as exc:
            logger.warning("Recon evidence recording failed: %s", exc)
            return None

    # ===================================================================
    # 3. GraphRAG Vector Store
    # ===================================================================

    def index_content(
        self,
        doc_id: str,
        content: str,
        source_type: str,
        metadata: Optional[Dict[str, Any]] = None,
        trace_id: Optional[str] = None,
    ) -> Optional[bool]:
        """Index content into the GraphRAG vector store.

        Args:
            doc_id: Unique document identifier.
            content: Text content to embed and index.
            source_type: Source type tag (``recon_result``, ``detection_finding``,
                ``profile_stats``, ``catalog_asset``, etc.).
            metadata: Additional metadata for filtering.

        Returns:
            True on success, None if unavailable.
        """
        if not self._vector_store or not self._embedder:
            logger.debug("Vector store unavailable — skipping content indexing")
            return None
        try:
            embedding = self._embedder.embed(content)
            meta = {"source_type": source_type, **(metadata or {})}
            if trace_id:
                meta["trace_id"] = trace_id
            return self._vector_store.upsert(
                id=doc_id,
                embedding=embedding,
                content=content,
                metadata=meta,
            )
        except Exception as exc:
            logger.warning("Vector indexing failed: %s", exc)
            return None

    # ===================================================================
    # 4. Knowledge Base Graph
    # ===================================================================

    def ingest_kb_nodes(
        self,
        nodes: List[Dict[str, Any]],
        edges: Optional[List[Dict[str, Any]]] = None,
    ) -> Optional[int]:
        """Ingest nodes and edges into the Knowledge Base graph.

        Args:
            nodes: List of node dicts with keys: ``id``, ``type``, ``properties``.
            edges: List of edge dicts with keys: ``source``, ``target``,
                ``type``, ``properties``.

        Returns:
            Count of ingested edges, or None if unavailable.
        """
        if not self._kb_pipeline:
            logger.debug("KB pipeline unavailable — skipping KB ingestion")
            return None
        try:
            graph_nodes = [
                _GraphNode(
                    id=n.get("id", str(uuid.uuid4())[:8]),
                    type=n.get("type", "Unknown"),
                    properties=n.get("properties", {}),
                )
                for n in nodes
            ]
            graph_edges = []
            if edges:
                graph_edges = [
                    _GraphEdge(
                        source=e["source"],
                        target=e["target"],
                        type=e.get("type", "RELATED_TO"),
                        properties=e.get("properties", {}),
                    )
                    for e in edges
                ]
            return self._kb_pipeline.ingest_graph(graph_nodes, graph_edges)
        except Exception as exc:
            logger.warning("KB graph ingestion failed: %s", exc)
            return None

    # ===================================================================
    # 5. Entity Linker
    # ===================================================================

    def link_entities_incremental(
        self,
        logic_dna_path: str,
        output_dir: str = "data/linker",
        threshold: float = 0.60,
    ) -> Optional[Dict[str, Any]]:
        """Incrementally link entities from a Logic DNA file.

        Args:
            logic_dna_path: Path to ``*_logic_dna.json`` file.
            output_dir: Entity linker output directory.
            threshold: Link threshold (0.0–1.0).

        Returns:
            Incremental link results or None.
        """
        if not _LINKER_AVAILABLE or not self._en_linker:
            logger.debug("Entity linker unavailable or disabled — skipping entity linking")
            return None
        try:
            return _link_entities_incremental(
                logic_dna_path=logic_dna_path,
                output_dir=output_dir,
                threshold=threshold,
            )
        except Exception as exc:
            logger.warning("Entity linking failed: %s", exc)
            return None

    def publish_entity_clusters(
        self,
        output_dir: str = "data/linker",
    ) -> Optional[Dict[str, Any]]:
        """Publish entity clusters to GraphRAG and vector store.

        Args:
            output_dir: Entity linker output directory.

        Returns:
            Sync statistics or None.
        """
        if not _LINKER_AVAILABLE or not self._en_linker:
            logger.debug("Entity linker unavailable or disabled — skipping cluster publish")
            return None
        try:
            return _publish_clusters_to_graph(output_dir=output_dir)
        except Exception as exc:
            logger.warning("Entity cluster publish failed: %s", exc)
            return None

    # ===================================================================
    # 6. Codex GraphRAG
    # ===================================================================

    def notify_runtime_event(
        self,
        event: Dict[str, Any],
    ) -> Optional[Dict[str, Any]]:
        """Ingest a runtime discrepancy event into Codex GraphRAG.

        Args:
            event: Event dict with keys: ``event_key``, ``source_system``,
                ``finding_id``, ``account``, ``fix``, ``risk_tier``, etc.

        Returns:
            Ingestion status dict or None.
        """
        if not _CODEX_AVAILABLE or not self._en_codex:
            logger.debug("Codex GraphRAG unavailable or disabled — skipping event ingestion")
            return None
        try:
            return _ingest_discrepancy_event(event)
        except Exception as exc:
            logger.warning("Codex event ingestion failed: %s", exc)
            return None

    # ===================================================================
    # 7. IKE Graph
    # ===================================================================

    def query_ike_graph(
        self,
        query: str,
        top_k: int = 10,
        trace_id: Optional[str] = None,
        **kwargs: Any,
    ) -> Optional[Dict[str, Any]]:
        """Query IKE's institutional graph-backed knowledge store."""
        if not self.ike_graph_available:
            logger.debug("IKE graph unavailable or disabled — skipping IKE graph query")
            return None
        try:
            return self._ike_client.query(query, top_k=top_k, **kwargs)
        except Exception as exc:
            logger.warning("IKE graph query failed: %s", exc)
            return None

    # ===================================================================
    # 8. Formula Dependency Graph
    # ===================================================================

    def get_formula_dependency_graph(
        self,
        workbook_id: str,
        max_depth: int = 10,
    ) -> Optional[Dict[str, Any]]:
        """Fetch formula dependency graph for a workbook via IKE Excel APIs."""
        if not self.formula_graph_available:
            logger.debug("Formula dependency graph unavailable or disabled — skipping graph fetch")
            return None
        try:
            return self._ike_client.excel_get_workbook_graph(workbook_id, max_depth=max_depth)
        except Exception as exc:
            logger.warning("Formula dependency graph fetch failed: %s", exc)
            return None

    # ===================================================================
    # 8b. Formula Graph Helpers (E8)
    # ===================================================================

    def find_upstream(
        self,
        workbook_id: str,
        cell_ref: str,
        max_depth: int = 50,
    ) -> Optional[List[str]]:
        """Find all upstream precedents for a cell via BFS on DEPENDS_ON edges.

        Returns list of cell ref strings, or None if formula graph unavailable.
        """
        graph = self.get_formula_dependency_graph(workbook_id, max_depth=max_depth)
        if graph is None:
            return None
        try:
            adj = _build_adjacency(graph)
            return _bfs_formula_graph(adj, cell_ref)
        except Exception as exc:
            logger.warning("find_upstream failed: %s", exc)
            return None

    def find_downstream(
        self,
        workbook_id: str,
        cell_ref: str,
        max_depth: int = 50,
    ) -> Optional[List[str]]:
        """Find all downstream dependents for a cell via BFS on reversed DEPENDS_ON edges.

        Returns list of cell ref strings, or None if formula graph unavailable.
        """
        graph = self.get_formula_dependency_graph(workbook_id, max_depth=max_depth)
        if graph is None:
            return None
        try:
            adj = _build_reverse_adjacency(graph)
            return _bfs_formula_graph(adj, cell_ref)
        except Exception as exc:
            logger.warning("find_downstream failed: %s", exc)
            return None

    def critical_path(
        self,
        workbook_id: str,
        metric_cell: str,
        max_depth: int = 50,
    ) -> Optional[List[str]]:
        """Find the longest dependency chain leading to ``metric_cell``.

        Uses topological sort + longest-path on the formula DAG.
        Returns ordered list from root to metric_cell, or None.
        """
        graph = self.get_formula_dependency_graph(workbook_id, max_depth=max_depth)
        if graph is None:
            return None
        try:
            adj = _build_adjacency(graph)
            return _longest_path(adj, metric_cell)
        except Exception as exc:
            logger.warning("critical_path failed: %s", exc)
            return None

    def detect_cycles(
        self,
        workbook_id: str,
        max_depth: int = 50,
    ) -> Optional[List[List[str]]]:
        """Detect circular formula references in a workbook.

        Returns list of cycles (each a list of cell refs), or None.
        """
        graph = self.get_formula_dependency_graph(workbook_id, max_depth=max_depth)
        if graph is None:
            return None
        try:
            adj = _build_adjacency(graph)
            return _find_cycles(adj)
        except Exception as exc:
            logger.warning("detect_cycles failed: %s", exc)
            return None

    def query_formulas(
        self,
        query: str,
        category: Optional[str] = None,
        top_k: int = 10,
    ) -> Optional[Dict[str, Any]]:
        """Semantic formula search via IKE Excel APIs.

        Wraps ``IKEClient.excel_query_formulas()`` for pattern retrieval.
        """
        if not self.formula_graph_available:
            logger.debug("Formula graph unavailable — skipping formula query")
            return None
        try:
            return self._ike_client.excel_query_formulas(
                query=query, category=category, top_k=top_k
            )
        except Exception as exc:
            logger.warning("query_formulas failed: %s", exc)
            return None

    # --- Convenience ---

    def __repr__(self) -> str:
        systems = []
        if self._lineage:
            systems.append("lineage")
        if self._evidence:
            systems.append("evidence")
        if self._vector_store:
            systems.append("vector")
        if self._kb_pipeline:
            systems.append("kb")
        if _LINKER_AVAILABLE:
            systems.append("linker")
        if _CODEX_AVAILABLE:
            systems.append("codex")
        if self.ike_graph_available:
            systems.append("ike_graph")
        if self.formula_graph_available:
            systems.append("formula_graph")
        status = ", ".join(systems) if systems else "none"
        return f"GraphBridge(systems=[{status}])"


# -----------------------------------------------------------------------
# Formula graph helpers (E8) — stdlib only
# -----------------------------------------------------------------------

def _cell_ref_from_id(node_id: str) -> str:
    """Extract a human-readable cell ref from a graph node ID."""
    # IKE formula graphs use node IDs like "Sheet1!A1" or raw cell refs
    return str(node_id)


def _build_adjacency(graph: Dict[str, Any]) -> Dict[str, List[str]]:
    """Build adjacency dict from IKE formula graph (source → [targets]).

    Expects graph with ``nodes`` list and ``edges`` list
    where each edge has ``source`` and ``target`` keys.
    """
    adj: Dict[str, List[str]] = {}
    for node in graph.get("nodes", []):
        nid = _cell_ref_from_id(node.get("id", ""))
        if nid not in adj:
            adj[nid] = []
    for edge in graph.get("edges", []):
        src = _cell_ref_from_id(edge.get("source", ""))
        tgt = _cell_ref_from_id(edge.get("target", ""))
        adj.setdefault(src, []).append(tgt)
        if tgt not in adj:
            adj[tgt] = []
    return adj


def _build_reverse_adjacency(graph: Dict[str, Any]) -> Dict[str, List[str]]:
    """Build reversed adjacency dict (target → [sources])."""
    adj: Dict[str, List[str]] = {}
    for node in graph.get("nodes", []):
        nid = _cell_ref_from_id(node.get("id", ""))
        if nid not in adj:
            adj[nid] = []
    for edge in graph.get("edges", []):
        src = _cell_ref_from_id(edge.get("source", ""))
        tgt = _cell_ref_from_id(edge.get("target", ""))
        adj.setdefault(tgt, []).append(src)
        if src not in adj:
            adj[src] = []
    return adj


def _bfs_formula_graph(adj: Dict[str, List[str]], start: str) -> List[str]:
    """BFS traversal from ``start`` returning all reachable nodes (excluding start)."""
    visited: Set[str] = set()
    queue: deque = deque([start])
    visited.add(start)
    result: List[str] = []
    while queue:
        node = queue.popleft()
        for neighbor in adj.get(node, []):
            if neighbor not in visited:
                visited.add(neighbor)
                result.append(neighbor)
                queue.append(neighbor)
    return result


def _longest_path(adj: Dict[str, List[str]], target: str) -> List[str]:
    """Find the longest path ending at ``target`` using topological sort.

    Returns the path as a list from root to target.  If ``target`` is
    not in the graph, returns an empty list.
    """
    if target not in adj:
        return []

    # Kahn's topological sort
    in_degree: Dict[str, int] = {n: 0 for n in adj}
    for n in adj:
        for nb in adj[n]:
            in_degree[nb] = in_degree.get(nb, 0) + 1

    queue: deque = deque(n for n, d in in_degree.items() if d == 0)
    dist: Dict[str, int] = {n: 0 for n in adj}
    pred: Dict[str, Optional[str]] = {n: None for n in adj}

    while queue:
        node = queue.popleft()
        for nb in adj.get(node, []):
            if dist[node] + 1 > dist.get(nb, 0):
                dist[nb] = dist[node] + 1
                pred[nb] = node
            in_degree[nb] -= 1
            if in_degree[nb] == 0:
                queue.append(nb)

    # Reconstruct path
    path: List[str] = []
    current: Optional[str] = target
    while current is not None:
        path.append(current)
        current = pred.get(current)
    path.reverse()
    return path


def _find_cycles(adj: Dict[str, List[str]]) -> List[List[str]]:
    """Find all cycles in the graph using DFS coloring.

    Returns a list of cycles, where each cycle is a list of cell refs.
    """
    WHITE, GRAY, BLACK = 0, 1, 2
    color: Dict[str, int] = {n: WHITE for n in adj}
    parent: Dict[str, Optional[str]] = {n: None for n in adj}
    cycles: List[List[str]] = []

    def dfs(u: str) -> None:
        color[u] = GRAY
        for v in adj.get(u, []):
            if v not in color:
                color[v] = WHITE
            if color[v] == WHITE:
                parent[v] = u
                dfs(v)
            elif color[v] == GRAY:
                # Back edge found — reconstruct cycle
                cycle = [v]
                cur = u
                while cur != v:
                    cycle.append(cur)
                    cur = parent.get(cur)  # type: ignore
                    if cur is None:
                        break
                cycle.append(v)
                cycle.reverse()
                cycles.append(cycle)
        color[u] = BLACK

    for node in adj:
        if color.get(node, WHITE) == WHITE:
            dfs(node)
    return cycles
