"""
Context Graph Implementation

In-memory GraphStore implementation for building and querying context graphs
from conversations and entities with advanced analytics integration.

Core Features:
    - In-memory GraphStore implementation
    - Entity and relationship extraction from conversations
    - BFS-based neighbor discovery
    - Type-based indexing
    - Export to dictionary format
    - Decision tracking integration

Comprehensive Decision Management:
    - Decision Recording: Store decisions with full context and metadata
    - Precedent Search: Find similar decisions using hybrid search algorithms
    - Influence Analysis: Analyze decision impact and relationships
    - Causal Analysis: Trace decision causality chains
    - Policy Enforcement: Built-in policy compliance checking
    - Advanced Analytics: Comprehensive decision insights

KG Algorithm Integration:
    - Centrality Analysis: Degree, betweenness, closeness, eigenvector centrality
    - Community Detection: Modularity-based community identification
    - Node Embeddings: Node2Vec embeddings for similarity analysis
    - Path Finding: Shortest path and advanced path algorithms
    - Link Prediction: Relationship prediction between entities
    - Similarity Calculation: Multi-type similarity measures

Vector Store Integration:
    - Hybrid Search: Semantic + structural similarity
    - Custom Similarity Weights: Configurable scoring
    - Advanced Precedent Search: KG-enhanced similarity
    - Multi-Embedding Support: Multiple embedding types

Advanced Graph Analytics:
    - Node Centrality Analysis: Multiple centrality measures
    - Community Detection: Identify clusters and communities
    - Node Similarity: Content and structural similarity
    - Graph Structure Analysis: Comprehensive metrics
    - Path Analysis: Find paths and connectivity
    - Embedding Generation: Node embeddings for ML

Decision Tracking Integration:
    - Decision Storage: Store decisions with full context
    - Precedent Search: Find similar decisions using graph traversal
    - Causal Analysis: Trace decision influence
    - Decision Analytics: Analyze decision patterns
    - Influence Analysis: Decision influence scoring and analysis
    - Policy Engine: Policy enforcement and compliance checking
    - Relationship Mapping: Map decision dependencies

Enhanced Methods:
    - analyze_graph_with_kg(): Comprehensive graph analysis
    - get_node_centrality(): Get centrality measures for nodes
    - find_similar_nodes(): Find similar nodes with advanced similarity
    - record_decision(): Add decisions with context integration
    - find_precedents(): Find decision precedents
    - analyze_decision_influence(): Analyze decision influence
    - get_decision_insights(): Get comprehensive decision analytics
    - trace_decision_causality(): Trace decision causality
    - enforce_decision_policy(): Enforce decision policies
    - get_graph_metrics(): Get comprehensive statistics
    - export_graph(): Export graph in various formats

Example Usage:
    >>> from semantica.context import ContextGraph
    >>> graph = ContextGraph(advanced_analytics=True,
    ...                    centrality_analysis=True,
    ...                    community_detection=True,
    ...                    node_embeddings=True)
    >>> 
    >>> # Basic graph operations
    >>> graph.add_node("Python", type="language", properties={"popularity": "high"})
    >>> graph.add_node("Programming", type="concept")
    >>> graph.add_edge("Python", "Programming", type="related_to")
    >>> centrality = graph.get_node_centrality("Python")
    >>> similar = graph.find_similar_nodes("Python", similarity_type="content")
    >>> analysis = graph.analyze_graph_with_kg()
    >>> 
    >>> # Decision management
    >>> decision_id = graph.record_decision(
    ...     category="loan_approval",
    ...     scenario="First-time homebuyer",
    ...     reasoning="Good credit score",
    ...     outcome="approved",
    ...     confidence=0.95,
    ...     entities=["customer_123", "property_456"]
    ... )
    >>> precedents = graph.find_precedents("loan_approval", limit=5)
    >>> influence = graph.analyze_decision_influence(decision_id)
    >>> insights = graph.get_decision_insights()
    >>> causality = graph.trace_decision_causality(decision_id)

Production Use Cases:
    - Knowledge Management: Build and analyze knowledge graphs
    - Decision Support: Context graphs for decision making
    - Recommendation Systems: Graph-based recommendations
    - Social Networks: Analyze connections and influence
    - Research Networks: Map collaborations and citations
    - Financial Services: Loan approvals, fraud detection, risk assessment
    - Healthcare: Treatment decisions, policy compliance, clinical pathways
    - Legal: Case precedent analysis, decision consistency
    - Business: Workflow decisions, policy compliance, audit trails
"""

from collections import defaultdict, deque
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional, Set, Tuple, Union
import uuid

from ..utils.logging import get_logger
from ..utils.progress_tracker import get_progress_tracker
from .entity_linker import EntityLinker

# Optional imports for advanced features
try:
    from ..kg import (
        GraphBuilder, GraphAnalyzer, CentralityCalculator, CommunityDetector,
        PathFinder, NodeEmbedder, SimilarityCalculator, LinkPredictor,
        ConnectivityAnalyzer
    )
    KG_AVAILABLE = True
except ImportError:
    KG_AVAILABLE = False


def _parse_iso_dt(value: str) -> Optional[datetime]:
    """Parse an ISO datetime string into a tz-naive UTC datetime.

    Always returns a naive datetime in UTC so callers can compare uniformly
    without worrying about mixed aware/naive arithmetic.
    """
    try:
        dt = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
        if dt.tzinfo is not None:
            dt = dt.astimezone(timezone.utc).replace(tzinfo=None)
        return dt
    except (ValueError, AttributeError):
        return None


@dataclass
class ContextNode:
    """Context graph node (Internal implementation)."""

    node_id: str
    node_type: str
    content: str
    metadata: Dict[str, Any] = field(default_factory=dict)
    properties: Dict[str, Any] = field(default_factory=dict)
    valid_from: Optional[str] = None   # ISO datetime string, e.g. "2026-01-01T00:00:00"
    valid_until: Optional[str] = None  # ISO datetime string; None = no expiry

    def is_active(self, at_time: Optional[datetime] = None) -> bool:
        """Return True if this node is active at the given time (defaults to now).

        Both ``at_time`` and stored bounds are normalized to tz-naive UTC so that
        callers may pass either aware or naive datetimes without raising TypeError.
        """
        if self.valid_from is None and self.valid_until is None:
            return True
        now = at_time if at_time is not None else datetime.utcnow()
        if now.tzinfo is not None:
            now = now.astimezone(timezone.utc).replace(tzinfo=None)
        start = _parse_iso_dt(self.valid_from) if self.valid_from is not None else None
        end = _parse_iso_dt(self.valid_until) if self.valid_until is not None else None
        if start is not None and now < start:
            return False
        if end is not None and now > end:
            return False
        return True

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary format."""
        props = self.properties.copy()
        props.update(self.metadata)
        props["content"] = self.content
        if self.valid_from is not None:
            props["valid_from"] = self.valid_from
        if self.valid_until is not None:
            props["valid_until"] = self.valid_until
        return {"id": self.node_id, "type": self.node_type, "properties": props}


@dataclass
class ContextEdge:
    """Context graph edge (Internal implementation)."""

    source_id: str
    target_id: str
    edge_type: str
    weight: float = 1.0
    metadata: Dict[str, Any] = field(default_factory=dict)
    valid_from: Optional[str] = None   # ISO datetime string
    valid_until: Optional[str] = None  # ISO datetime string; None = no expiry

    def is_active(self, at_time: Optional[datetime] = None) -> bool:
        """Return True if this edge is active at the given time (defaults to now).

        Both ``at_time`` and stored bounds are normalized to tz-naive UTC so that
        callers may pass either aware or naive datetimes without raising TypeError.
        """
        if self.valid_from is None and self.valid_until is None:
            return True
        now = at_time if at_time is not None else datetime.utcnow()
        if now.tzinfo is not None:
            now = now.astimezone(timezone.utc).replace(tzinfo=None)
        start = _parse_iso_dt(self.valid_from) if self.valid_from is not None else None
        end = _parse_iso_dt(self.valid_until) if self.valid_until is not None else None
        if start is not None and now < start:
            return False
        if end is not None and now > end:
            return False
        return True

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary format."""
        d = {
            "source_id": self.source_id,
            "target_id": self.target_id,
            "type": self.edge_type,
            "weight": self.weight,
            "properties": self.metadata,
        }
        if self.valid_from is not None:
            d["valid_from"] = self.valid_from
        if self.valid_until is not None:
            d["valid_until"] = self.valid_until
        return d


class ContextGraph:
    """
    Easy-to-Use Context Graph with All Advanced Features.
    
    This class provides simple methods for:
    - Building knowledge graphs
    - Recording and analyzing decisions
    - Finding precedents and patterns
    - Causal analysis and policy enforcement
    - Advanced graph analytics
    
    Perfect for building intelligent AI agents that can learn from decisions!
    """

    def __init__(self, config: Optional[Dict[str, Any]] = None, **kwargs):
        """
        Initialize context graph with optional advanced features.

        Args:
            config: Configuration dictionary
            **kwargs: Additional configuration options:
                - extract_entities: Extract entities from content (default: True)
                - extract_relationships: Extract relationships (default: True)
                - entity_linker: Entity linker instance
                - advanced_analytics: Enable KG algorithms (default: True)
                - centrality_analysis: Enable centrality measures (default: True)
                - community_detection: Enable community detection (default: True)
                - node_embeddings: Enable Node2Vec embeddings (default: True)
        """
        self.logger = get_logger("context_graph")
        self.config = config or {}
        self.config.update(kwargs)

        self.extract_entities = self.config.get("extract_entities", True)
        self.extract_relationships = self.config.get("extract_relationships", True)

        self.entity_linker = self.config.get("entity_linker") or EntityLinker()

        # Stable identifier so this graph can be referenced after save/load
        self.graph_id: str = str(uuid.uuid4())

        # Graph structure
        self.nodes: Dict[str, ContextNode] = {}
        self.edges: List[ContextEdge] = []

        # Adjacency list for efficient traversal: source_id -> list of edges
        self._adjacency: Dict[str, List[ContextEdge]] = defaultdict(list)

        # Indexes
        self.node_type_index: Dict[str, Set[str]] = defaultdict(set)
        self.edge_type_index: Dict[str, List[ContextEdge]] = defaultdict(list)

        # Cross-graph navigation: link_id -> (other_graph, source_node_id, target_node_id)
        self._linked_graphs: Dict[str, Tuple["ContextGraph", str, str]] = {}
        # Unresolved link metadata (populated after load_from_file, before resolve_links)
        self._unresolved_links: Dict[str, Dict[str, str]] = {}

        # Progress tracker
        self.progress_tracker = get_progress_tracker()
        # Ensure progress tracker is enabled
        if not self.progress_tracker.enabled:
            self.progress_tracker.enabled = True
        
        # Initialize advanced KG components if available
        self.kg_components = {}
        self._analytics_cache = {}
        
        enable_advanced = self.config.get("advanced_analytics", True)
        
        if KG_AVAILABLE and enable_advanced:
            try:
                if self.config.get("centrality_analysis", True):
                    self.kg_components["centrality_calculator"] = CentralityCalculator()
                if self.config.get("community_detection", True):
                    self.kg_components["community_detector"] = CommunityDetector()
                if self.config.get("node_embeddings", True):
                    self.kg_components["node_embedder"] = NodeEmbedder()
                self.kg_components["path_finder"] = PathFinder()
                self.kg_components["similarity_calculator"] = SimilarityCalculator()
                self.kg_components["connectivity_analyzer"] = ConnectivityAnalyzer()
                
                self.logger.info("Advanced KG components initialized successfully")
            except Exception as e:
                self.logger.warning(f"Failed to initialize KG components: {e}")
                self.kg_components = {}

    # --- GraphStore Protocol Implementation ---

    def add_nodes(self, nodes: List[Dict[str, Any]]) -> int:
        """
        Add nodes to graph.

        Args:
            nodes: List of nodes to add (dicts with id, type, properties)

        Returns:
            Number of nodes added
        """
        count = 0
        for node in nodes:
            # Extract content from properties if not explicit
            node_props = node.get("properties", {})
            content = node_props.get("content", node.get("id"))
            # Restore validity windows from properties (written there by ContextNode.to_dict)
            # or from top-level keys on the node dict
            valid_from = (
                node.get("valid_from")
                or node_props.get("valid_from")
            )
            valid_until = (
                node.get("valid_until")
                or node_props.get("valid_until")
            )
            metadata = {
                k: v for k, v in node_props.items()
                if k not in ("content", "valid_from", "valid_until")
            }

            internal_node = ContextNode(
                node_id=node.get("id"),
                node_type=node.get("type", "entity"),
                content=content,
                metadata=metadata,
                properties=node_props,
                valid_from=valid_from,
                valid_until=valid_until,
            )

            if self._add_internal_node(internal_node):
                count += 1
        return count

    def add_edges(self, edges: List[Dict[str, Any]]) -> int:
        """
        Add edges to graph.

        Args:
            edges: List of edges to add (dicts with source_id, target_id, type,
                weight, properties)

        Returns:
            Number of edges added
        """
        count = 0
        for edge in edges:
            edge_props = edge.get("properties", {})
            # Restore validity windows — ContextEdge.to_dict() writes them at top level
            valid_from = edge.get("valid_from") or edge_props.get("valid_from")
            valid_until = edge.get("valid_until") or edge_props.get("valid_until")
            internal_edge = ContextEdge(
                source_id=edge.get("source_id"),
                target_id=edge.get("target_id"),
                edge_type=edge.get("type", "related_to"),
                weight=edge.get("weight", 1.0),
                metadata=edge_props,
                valid_from=valid_from,
                valid_until=valid_until,
            )

            if self._add_internal_edge(internal_edge):
                count += 1
        return count

    def __contains__(self, node_id: object) -> bool:
        if not isinstance(node_id, str):
            return False
        return node_id in self.nodes

    def has_node(self, node_id: str) -> bool:
        return node_id in self.nodes

    def neighbors(self, node_id: str) -> List[str]:
        return self.get_neighbor_ids(node_id)

    def get_neighbor_ids(
        self,
        node_id: str,
        relationship_types: Optional[List[str]] = None,
    ) -> List[str]:
        if node_id not in self.nodes:
            return []

        rel_filter = set(relationship_types) if relationship_types else None
        neighbor_ids: List[str] = []
        for edge in self._adjacency.get(node_id, []):
            if rel_filter is None or edge.edge_type in rel_filter:
                neighbor_ids.append(edge.target_id)
        return neighbor_ids

    def get_nodes_by_label(self, label: str) -> List[str]:
        return list(self.node_type_index.get(label, set()))

    def get_node_property(self, node_id: str, property_name: str) -> Any:
        node = self.nodes.get(node_id)
        if not node:
            return None
        return node.properties.get(property_name)

    def get_node_attributes(self, node_id: str) -> Dict[str, Any]:
        node = self.nodes.get(node_id)
        if not node:
            return {}
        return node.properties.copy()

    def add_node_attribute(self, node_id: str, attributes: Dict[str, Any]) -> None:
        node = self.nodes.get(node_id)
        if not node:
            return
        node.properties.update(attributes)
        node.metadata.update(attributes)

    def get_edge_data(self, source_id: str, target_id: str) -> Dict[str, Any]:
        for edge in self._adjacency.get(source_id, []):
            if edge.target_id == target_id:
                data = edge.metadata.copy()
                data["type"] = edge.edge_type
                data["weight"] = edge.weight
                return data
        return {}

    def get_neighbors(
        self,
        node_id: str,
        hops: int = 1,
        relationship_types: Optional[List[str]] = None,
        min_weight: float = 0.0,
    ) -> List[Dict[str, Any]]:
        """
        Get neighbors of a node.

        Args:
            node_id: Starting node ID.
            hops: Maximum number of hops to traverse (BFS depth).
            relationship_types: Optional whitelist of edge types to follow.
            min_weight: Minimum edge weight required to traverse an edge (default 0.0
                means all edges pass). Use e.g. ``min_weight=0.5`` to follow only
                strong/high-confidence relationships.

        Returns:
            List of dicts with neighbor info (id, type, content, relationship, weight, hop).
        """
        if node_id not in self.nodes:
            return []

        neighbors: List[Dict[str, Any]] = []
        visited = {node_id}
        queue = deque([(node_id, 0)])
        rel_filter = set(relationship_types) if relationship_types else None

        while queue:
            current_id, current_hop = queue.popleft()
            if current_hop >= hops:
                continue

            outgoing_edges = self._adjacency.get(current_id, [])
            for edge in outgoing_edges:
                if rel_filter is not None and edge.edge_type not in rel_filter:
                    continue
                if edge.weight < min_weight:
                    continue
                neighbor_id = edge.target_id
                if neighbor_id in visited:
                    continue
                visited.add(neighbor_id)
                queue.append((neighbor_id, current_hop + 1))

                node = self.nodes.get(neighbor_id)
                if not node:
                    continue
                neighbors.append(
                    {
                        "id": node.node_id,
                        "type": node.node_type,
                        "content": node.content,
                        "relationship": edge.edge_type,
                        "weight": edge.weight,
                        "hop": current_hop + 1,
                    }
                )

        return neighbors

    def query(self, query: str) -> List[Dict[str, Any]]:
        """
        Execute a simple keyword search query on the graph nodes.

        Args:
            query: Keyword query string

        Returns:
            List of matching node dicts
        """
        results = []
        query_lower = query.lower().split()

        for node in self.nodes.values():
            content_lower = node.content.lower()
            if any(word in content_lower for word in query_lower):
                # Calculate simple score
                overlap = sum(1 for word in query_lower if word in content_lower)
                score = overlap / len(query_lower) if query_lower else 0.0

                results.append(
                    {
                        "node": node.to_dict(),
                        "score": score,
                        "content": node.content,
                    }
                )

        return sorted(results, key=lambda x: x["score"], reverse=True)

    def add_node(
        self,
        node_id: str,
        node_type: str,
        content: Optional[str] = None,
        **properties,
    ) -> bool:
        """
        Add a single node to the graph.

        Args:
            node_id: Unique identifier
            node_type: Node type (e.g., 'entity', 'concept')
            content: Node content/label
            **properties: Additional properties. Use `valid_from` and `valid_until`
                (ISO datetime strings) to define a temporal validity window.
        """
        content = content or node_id
        valid_from = properties.pop("valid_from", None)
        valid_until = properties.pop("valid_until", None)
        return self._add_internal_node(
            ContextNode(
                node_id=node_id,
                node_type=node_type,
                content=content,
                metadata=properties,
                properties=properties,
                valid_from=valid_from,
                valid_until=valid_until,
            )
        )

    def add_edge(
        self,
        source_id: str,
        target_id: str,
        edge_type: str = "related_to",
        weight: float = 1.0,
        **properties,
    ) -> bool:
        """
        Add a single edge to the graph.

        Args:
            source_id: Source node ID
            target_id: Target node ID
            edge_type: Relationship type
            weight: Edge weight
            **properties: Additional properties. Use `valid_from` and `valid_until`
                (ISO datetime strings) to define a temporal validity window.
        """
        valid_from = properties.pop("valid_from", None)
        valid_until = properties.pop("valid_until", None)
        return self._add_internal_edge(
            ContextEdge(
                source_id=source_id,
                target_id=target_id,
                edge_type=edge_type,
                weight=weight,
                metadata=properties,
                valid_from=valid_from,
                valid_until=valid_until,
            )
        )

    def save_to_file(self, path: str) -> None:
        """
        Save context graph to file (JSON format).

        Args:
            path: File path to save to
        """
        import json

        # Serialise cross-graph link metadata (object references are not serialisable,
        # so we store other_graph_id; callers can reconnect with resolve_links()).
        links_data = []
        for link_id, (other_graph, source_node_id, target_node_id) in self._linked_graphs.items():
            links_data.append(
                {
                    "link_id": link_id,
                    "source_node_id": source_node_id,
                    "target_node_id": target_node_id,
                    "other_graph_id": other_graph.graph_id,
                }
            )

        data = {
            "graph_id": self.graph_id,
            "nodes": [node.to_dict() for node in self.nodes.values()],
            "edges": [edge.to_dict() for edge in self.edges],
            "links": links_data,
        }

        with open(path, "w", encoding="utf-8") as f:
            json.dump(data, f, indent=2, ensure_ascii=False)

        self.logger.info(f"Saved context graph to {path}")

    def load_from_file(self, path: str) -> None:
        """
        Load context graph from file (JSON format).

        Args:
            path: File path to load from
        """
        import json
        import os

        if not os.path.exists(path):
            self.logger.warning(f"File not found: {path}")
            return

        with open(path, "r", encoding="utf-8") as f:
            data = json.load(f)

        # Clear existing
        self.nodes.clear()
        self.edges.clear()
        self._adjacency.clear()
        self.node_type_index.clear()
        self.edge_type_index.clear()
        self._linked_graphs.clear()
        self._unresolved_links.clear()

        # Restore stable graph identity
        if "graph_id" in data:
            self.graph_id = data["graph_id"]

        # Load nodes
        nodes = data.get("nodes", [])
        self.add_nodes(nodes)

        # Load edges
        edges = data.get("edges", [])
        self.add_edges(edges)

        # Restore link metadata — object references require resolve_links() to reconnect
        for link_meta in data.get("links", []):
            link_id = link_meta.get("link_id")
            if link_id:
                self._unresolved_links[link_id] = link_meta

        self.logger.info(f"Loaded context graph from {path}")

    def find_node(self, node_id: str) -> Optional[Dict[str, Any]]:
        """Find a node by ID."""
        node = self.nodes.get(node_id)
        if node:
            merged_metadata = {}
            merged_metadata.update(getattr(node, "metadata", {}) or {})
            merged_metadata.update(getattr(node, "properties", {}) or {})
            return {
                "id": node.node_id,
                "type": node.node_type,
                "content": node.content,
                "metadata": merged_metadata,
            }
        return None

    def find_nodes(self, node_type: Optional[str] = None) -> List[Dict[str, Any]]:
        """Find nodes, optionally filtered by type."""
        if node_type:
            node_ids = self.node_type_index.get(node_type, set())
            nodes = [self.nodes[nid] for nid in node_ids]
        else:
            nodes = self.nodes.values()

        return [
            {
                "id": n.node_id,
                "type": n.node_type,
                "content": n.content,
                "metadata": {**(getattr(n, "metadata", {}) or {}), **(getattr(n, "properties", {}) or {})},
            }
            for n in nodes
        ]

    def find_active_nodes(
        self,
        node_type: Optional[str] = None,
        at_time: Optional[datetime] = None,
    ) -> List[Dict[str, Any]]:
        """
        Find nodes that are currently active within their validity window.

        Nodes without ``valid_from``/``valid_until`` are always considered active.

        Args:
            node_type: Optional node type filter.
            at_time: Point in time to evaluate validity (defaults to ``datetime.utcnow()``).

        Returns:
            List of active node dicts (same format as :meth:`find_nodes`).
        """
        now = at_time or datetime.utcnow()
        if node_type:
            node_ids = self.node_type_index.get(node_type, set())
            nodes_iter = [self.nodes[nid] for nid in node_ids if nid in self.nodes]
        else:
            nodes_iter = list(self.nodes.values())

        result = []
        for node in nodes_iter:
            if node.is_active(now):
                result.append(
                    {
                        "id": node.node_id,
                        "type": node.node_type,
                        "content": node.content,
                        "metadata": {
                            **(getattr(node, "metadata", {}) or {}),
                            **(getattr(node, "properties", {}) or {}),
                        },
                    }
                )
        return result

    def link_graph(
        self,
        other_graph: "ContextGraph",
        source_node_id: str,
        target_node_id: str,
        link_type: str = "CROSS_GRAPH",
    ) -> str:
        """
        Create a navigable link from a node in this graph to a node in another graph.

        This enables cross-graph navigation: separate ContextGraph instances can be
        linked hierarchically, allowing agents to traverse from one problem space into
        a related one without merging the graphs (like "a dream within a dream").

        Args:
            other_graph: The target ContextGraph instance.
            source_node_id: Node ID in *this* graph that serves as the exit point.
            target_node_id: Node ID in *other_graph* that serves as the entry point.
            link_type: Edge type label for the cross-graph bridge (default "CROSS_GRAPH").

        Returns:
            A unique link ID that can be passed to :meth:`navigate_to`.

        Raises:
            KeyError: If source_node_id is not in this graph or target_node_id is not
                in other_graph.
        """
        if source_node_id not in self.nodes:
            raise KeyError(f"Source node '{source_node_id}' not found in this graph")
        if target_node_id not in other_graph.nodes:
            raise KeyError(f"Target node '{target_node_id}' not found in other_graph")

        link_id = str(uuid.uuid4())
        self._linked_graphs[link_id] = (other_graph, source_node_id, target_node_id)

        # Create a dedicated marker node so it is clearly typed and does not pollute
        # the entity namespace. _add_internal_edge auto-creates missing targets as
        # "entity" nodes — by pre-inserting a "cross_graph_link" node we prevent that.
        marker_node_id = f"__cross_graph_{link_id}"
        self._add_internal_node(
            ContextNode(
                node_id=marker_node_id,
                node_type="cross_graph_link",
                content=f"Cross-graph link → {target_node_id}",
                metadata={"cross_graph": True, "link_id": link_id, "target_node_id": target_node_id},
                properties={},
            )
        )
        # Record a marker edge so the link shows up in graph traversal
        self._add_internal_edge(
            ContextEdge(
                source_id=source_node_id,
                target_id=marker_node_id,
                edge_type=link_type,
                weight=1.0,
                metadata={"cross_graph": True, "link_id": link_id},
            )
        )
        return link_id

    def navigate_to(self, link_id: str) -> Tuple["ContextGraph", str]:
        """
        Navigate to the target graph and entry node for a cross-graph link.

        Args:
            link_id: Link ID returned by :meth:`link_graph`.

        Returns:
            Tuple of ``(other_graph, target_node_id)``.

        Raises:
            KeyError: If link_id is not registered on this graph.
        """
        if link_id not in self._linked_graphs:
            if link_id in self._unresolved_links:
                meta = self._unresolved_links[link_id]
                raise KeyError(
                    f"Cross-graph link '{link_id}' exists but its target graph "
                    f"(graph_id={meta.get('other_graph_id')!r}) has not been reconnected. "
                    "Call resolve_links({graph_id: graph_instance, ...}) to restore navigation."
                )
            raise KeyError(
                f"No cross-graph link '{link_id}' found. "
                "Call link_graph() first to create the link."
            )
        other_graph, _, target_node_id = self._linked_graphs[link_id]
        return other_graph, target_node_id

    def resolve_links(self, graphs: Dict[str, "ContextGraph"]) -> int:
        """
        Reconnect cross-graph links after a :meth:`load_from_file` call.

        Since ``other_graph`` object references cannot be serialised, links are stored
        as metadata only (``other_graph_id``).  Call this method with a mapping of
        ``{graph_id: graph_instance}`` to restore live navigation.

        Args:
            graphs: Mapping of graph_id strings to ContextGraph instances.

        Returns:
            Number of links successfully resolved.

        Example::

            g1.save_to_file("g1.json")
            g2.save_to_file("g2.json")

            g1b, g2b = ContextGraph(), ContextGraph()
            g1b.load_from_file("g1.json")
            g2b.load_from_file("g2.json")
            resolved = g1b.resolve_links({g2b.graph_id: g2b})
        """
        resolved = 0
        for link_id, meta in list(self._unresolved_links.items()):
            other_graph_id = meta.get("other_graph_id")
            if other_graph_id in graphs:
                other_graph = graphs[other_graph_id]
                source_node_id = meta["source_node_id"]
                target_node_id = meta["target_node_id"]
                # Validate target node still exists in the restored graph
                if target_node_id in other_graph.nodes:
                    self._linked_graphs[link_id] = (other_graph, source_node_id, target_node_id)
                    del self._unresolved_links[link_id]
                    resolved += 1
                else:
                    self.logger.warning(
                        f"resolve_links: target node '{target_node_id}' not found in "
                        f"graph '{other_graph_id}' for link '{link_id}'"
                    )
        return resolved

    def find_edges(self, edge_type: Optional[str] = None) -> List[Dict[str, Any]]:
        """Find edges, optionally filtered by type."""
        if edge_type:
            edges = self.edge_type_index.get(edge_type, [])
        else:
            edges = self.edges

        return [
            {
                "source": e.source_id,
                "target": e.target_id,
                "type": e.edge_type,
                "weight": e.weight,
                "metadata": e.metadata,
            }
            for e in edges
        ]

    def stats(self) -> Dict[str, Any]:
        """Get graph statistics."""
        return {
            "node_count": len(self.nodes),
            "edge_count": len(self.edges),
            "node_types": {k: len(v) for k, v in self.node_type_index.items()},
            "edge_types": {k: len(v) for k, v in self.edge_type_index.items()},
            "density": self.density(),
        }

    def density(self) -> float:
        """Calculate graph density."""
        n = len(self.nodes)
        if n < 2:
            return 0.0
        max_edges = n * (n - 1)  # Directed graph
        return len(self.edges) / max_edges

    # --- Internal Helpers ---

    def _normalize_timestamp(self, timestamp_value) -> datetime:
        """
        Normalize timestamp value to datetime object.
        
        Handles various timestamp formats:
        - datetime: returns as-is
        - int/float: converts from epoch seconds
        - str: parses ISO format (with optional Z)
        - None/invalid: returns current datetime
        
        Args:
            timestamp_value: Timestamp value in various formats
            
        Returns:
            datetime: Normalized datetime object
        """
        from datetime import datetime
        
        if isinstance(timestamp_value, datetime):
            return timestamp_value
        elif isinstance(timestamp_value, (int, float)):
            return datetime.fromtimestamp(timestamp_value)
        elif isinstance(timestamp_value, str):
            # Handle ISO format with optional Z suffix
            timestamp_str = timestamp_value.rstrip('Z')  # Remove Z if present
            try:
                return datetime.fromisoformat(timestamp_str)
            except ValueError:
                # Fallback to current datetime if parsing fails
                return datetime.now()
        else:
            # Fallback for None or other types
            return datetime.now()

    def _add_internal_node(self, node: ContextNode) -> bool:
        """Internal method to add a node."""
        self.nodes[node.node_id] = node
        # Handle edge case where node_type might be None or not a string
        if hasattr(node, 'node_type') and isinstance(node.node_type, str):
            self.node_type_index[node.node_type].add(node.node_id)
        else:
            # Use 'unknown' as fallback for invalid node_type
            self.node_type_index['unknown'].add(node.node_id)
        return True

    def _add_internal_edge(self, edge: ContextEdge) -> bool:
        """Internal method to add an edge."""
        # Ensure nodes exist
        if edge.source_id not in self.nodes:
            self._add_internal_node(
                ContextNode(edge.source_id, "entity", edge.source_id)
            )
        if edge.target_id not in self.nodes:
            self._add_internal_node(
                ContextNode(edge.target_id, "entity", edge.target_id)
            )

        self.edges.append(edge)
        self.edge_type_index[edge.edge_type].append(edge)
        self._adjacency[edge.source_id].append(edge)
        return True

    # --- Builder Methods (Legacy/Utility) ---

    def build_from_conversations(
        self,
        conversations: List[Union[str, Dict[str, Any]]],
        link_entities: bool = True,
        extract_intents: bool = False,
        extract_sentiments: bool = False,
        **options,
    ) -> Dict[str, Any]:
        """
        Build context graph from conversations and return dict representation.

        Args:
            conversations: List of conversation files or dictionaries
            ...

        Returns:
            Graph dictionary (nodes, edges)
        """
        tracking_id = self.progress_tracker.start_tracking(
            file=None,
            module="context",
            submodule="ContextGraph",
            message=f"Building graph from {len(conversations)} conversations",
        )

        try:
            for conv in conversations:
                conv_data = (
                    conv if isinstance(conv, dict) else self._load_conversation(conv)
                )
                self._process_conversation(
                    conv_data,
                    extract_intents=extract_intents,
                    extract_sentiments=extract_sentiments,
                )

            if link_entities:
                self._link_entities()

            self.progress_tracker.stop_tracking(tracking_id, status="completed")
            return self.to_dict()

        except Exception as e:
            self.progress_tracker.stop_tracking(
                tracking_id, status="failed", message=str(e)
            )
            raise

    def build_from_entities_and_relationships(
        self,
        entities: List[Dict[str, Any]],
        relationships: List[Dict[str, Any]],
        **kwargs,
    ) -> Dict[str, Any]:
        """
        Build graph from entities and relationships.

        Args:
            entities: List of entity dictionaries
            relationships: List of relationship dictionaries
            **kwargs: Additional options

        Returns:
            Graph dictionary (nodes, edges)
        """
        tracking_id = self.progress_tracker.start_tracking(
            file=None,
            module="context",
            submodule="ContextGraph",
            message=(
                f"Building graph from {len(entities)} entities and "
                f"{len(relationships)} relationships"
            ),
        )

        try:
            # Add entities
            for entity in entities:
                entity_id = entity.get("id") or entity.get("entity_id")
                if entity_id:
                    self._add_internal_node(
                        ContextNode(
                            node_id=entity_id,
                            node_type=entity.get("type", "entity"),
                            content=entity.get("text")
                            or entity.get("label")
                            or entity_id,
                            metadata=entity,
                            properties=entity,
                        )
                    )

            # Add relationships
            for rel in relationships:
                source = rel.get("source_id")
                target = rel.get("target_id")
                if source and target:
                    self._add_internal_edge(
                        ContextEdge(
                            source_id=source,
                            target_id=target,
                            edge_type=rel.get("type", "related_to"),
                            weight=rel.get("confidence", 1.0),
                            metadata=rel,
                        )
                    )

            self.progress_tracker.stop_tracking(tracking_id, status="completed")
            return self.to_dict()

        except Exception as e:
            self.progress_tracker.stop_tracking(
                tracking_id, status="failed", message=str(e)
            )
            raise

    def _process_conversation(self, conv_data: Dict[str, Any], **kwargs) -> None:
        """Process a single conversation."""
        conv_id = conv_data.get("id") or f"conv_{hash(str(conv_data)) % 10000}"

        # Add conversation node
        self._add_internal_node(
            ContextNode(
                node_id=conv_id,
                node_type="conversation",
                content=conv_data.get("content", "") or conv_data.get("summary", ""),
                metadata={"timestamp": conv_data.get("timestamp")},
            )
        )

        # Track name to ID mapping for relationship resolution
        name_to_id = {}

        # Extract entities
        if self.extract_entities:
            for entity in conv_data.get("entities", []):
                entity_id = entity.get("id") or entity.get("entity_id")
                entity_text = (
                    entity.get("text")
                    or entity.get("label")
                    or entity.get("name")
                    or entity_id
                )
                entity_type = entity.get("type", "entity")

                # Generate ID if missing
                if not entity_id and entity_text and self.entity_linker:
                    # Use EntityLinker to generate ID
                    if hasattr(self.entity_linker, "_generate_entity_id"):
                        entity_id = self.entity_linker._generate_entity_id(
                            entity_text, entity_type
                        )
                    else:
                        # Fallback ID generation
                        import hashlib

                        entity_hash = hashlib.md5(
                            f"{entity_text}_{entity_type}".encode()
                        ).hexdigest()[:12]
                        entity_id = f"{entity_type.lower()}_{entity_hash}"

                if entity_id:
                    if entity_text:
                        name_to_id[entity_text] = entity_id

                    self._add_internal_node(
                        ContextNode(
                            node_id=entity_id,
                            node_type="entity",
                            content=entity_text,
                            metadata={"type": entity_type, **entity},
                        )
                    )
                    self._add_internal_edge(
                        ContextEdge(
                            source_id=conv_id,
                            target_id=entity_id,
                            edge_type="mentions",
                        )
                    )

        # Extract relationships
        if self.extract_relationships:
            for rel in conv_data.get("relationships", []):
                source = rel.get("source_id")
                target = rel.get("target_id")

                # Resolve IDs from names if missing
                if not source and rel.get("source") and rel.get("source") in name_to_id:
                    source = name_to_id[rel.get("source")]

                if not target and rel.get("target") and rel.get("target") in name_to_id:
                    target = name_to_id[rel.get("target")]

                if source and target:
                    self._add_internal_edge(
                        ContextEdge(
                            source_id=source,
                            target_id=target,
                            edge_type=rel.get("type", "related_to"),
                            weight=rel.get("confidence", 1.0),
                        )
                    )

    def _link_entities(self) -> None:
        """Link similar entities using EntityLinker."""
        if not self.entity_linker:
            return

        entity_nodes = [n for n in self.nodes.values() if n.node_type == "entity"]
        for i, node1 in enumerate(entity_nodes):
            for node2 in entity_nodes[i + 1 :]:
                similarity = self.entity_linker._calculate_text_similarity(
                    node1.content.lower(), node2.content.lower()
                )
                if similarity >= self.entity_linker.similarity_threshold:
                    self._add_internal_edge(
                        ContextEdge(
                            source_id=node1.node_id,
                            target_id=node2.node_id,
                            edge_type="similar_to",
                            weight=similarity,
                        )
                    )

    def _load_conversation(self, file_path: str) -> Dict[str, Any]:
        """Load conversation from file."""
        from ..utils.helpers import read_json_file
        from pathlib import Path

        return read_json_file(Path(file_path))

    def to_dict(self) -> Dict[str, Any]:
        """Export graph to dictionary format."""
        nodes_out = []
        for n in self.nodes.values():
            entry: Dict[str, Any] = {
                "id": n.node_id,
                "type": n.node_type,
                "content": n.content,
                "properties": n.properties,
                "metadata": n.metadata,
            }
            if n.valid_from is not None:
                entry["valid_from"] = n.valid_from
            if n.valid_until is not None:
                entry["valid_until"] = n.valid_until
            nodes_out.append(entry)

        edges_out = []
        for e in self.edges:
            entry = {
                "source": e.source_id,
                "target": e.target_id,
                "type": e.edge_type,
                "weight": e.weight,
            }
            if e.valid_from is not None:
                entry["valid_from"] = e.valid_from
            if e.valid_until is not None:
                entry["valid_until"] = e.valid_until
            edges_out.append(entry)

        return {
            "nodes": nodes_out,
            "edges": edges_out,
            "statistics": {
                "node_count": len(self.nodes),
                "edge_count": len(self.edges),
            },
        }

    def from_dict(self, graph_dict: Dict[str, Any]) -> None:
        """Load graph from dictionary format."""
        # Clear existing graph
        self.nodes.clear()
        self.edges.clear()

        # Add nodes — restore validity windows if present
        for node_data in graph_dict.get("nodes", []):
            node_props = node_data.get("properties", {})
            node = ContextNode(
                node_id=node_data["id"],
                node_type=node_data["type"],
                content=node_data.get("content", ""),
                properties=node_props,
                metadata=node_data.get("metadata", {}),
                valid_from=node_data.get("valid_from") or node_props.get("valid_from"),
                valid_until=node_data.get("valid_until") or node_props.get("valid_until"),
            )
            self._add_internal_node(node)

        # Add edges — restore validity windows if present
        for edge_data in graph_dict.get("edges", []):
            edge = ContextEdge(
                source_id=edge_data["source"],
                target_id=edge_data["target"],
                edge_type=edge_data["type"],
                weight=edge_data.get("weight", 1.0),
                metadata=edge_data.get("metadata", {}),
                valid_from=edge_data.get("valid_from"),
                valid_until=edge_data.get("valid_until"),
            )
            self._add_internal_edge(edge)

    # Decision Support Methods
    def add_decision(self, decision: "Decision") -> None:
        """
        Add decision node to graph.
        
        Args:
            decision: Decision object to add
        """
        from .decision_models import Decision
        
        # Handle empty decision ID by generating UUID for both None and empty string
        # This ensures consistent behavior with Decision model's __post_init__ method
        node_id = decision.decision_id if decision.decision_id else str(uuid.uuid4())
        
        # Handle None metadata
        metadata = decision.metadata or {}
        
        # Normalize timestamp to ensure consistent storage format
        normalized_timestamp = self._normalize_timestamp(decision.timestamp)
        
        node = ContextNode(
            node_id=node_id,
            node_type="Decision",
            content=decision.scenario,
            properties={
                "category": decision.category,
                "reasoning": decision.reasoning,
                "outcome": decision.outcome,
                "confidence": decision.confidence,
                "timestamp": normalized_timestamp.isoformat(),
                "decision_maker": decision.decision_maker,
                "reasoning_embedding": decision.reasoning_embedding,
                "node2vec_embedding": decision.node2vec_embedding,
                **metadata
            }
        )
        self._add_internal_node(node)

    def add_causal_relationship(
        self,
        source_decision_id: str,
        target_decision_id: str,
        relationship_type: str
    ) -> None:
        """
        Add causal relationship between decisions.
        
        Args:
            source_decision_id: Source decision ID
            target_decision_id: Target decision ID
            relationship_type: Type of relationship (CAUSED, INFLUENCED, PRECEDENT_FOR)
        """
        valid_types = ["CAUSED", "INFLUENCED", "PRECEDENT_FOR"]
        if relationship_type not in valid_types:
            raise ValueError(f"Relationship type must be one of: {valid_types}")
        
        # Check if decisions exist - if not, skip adding relationship
        if source_decision_id not in self.nodes or target_decision_id not in self.nodes:
            return
        
        # Check if nodes are decision nodes - if not, skip adding relationship
        source_node = self.nodes[source_decision_id]
        target_node = self.nodes[target_decision_id]
        if (not hasattr(source_node, 'node_type') or not isinstance(source_node.node_type, str) or
            not hasattr(target_node, 'node_type') or not isinstance(target_node.node_type, str) or
            source_node.node_type.lower() != "decision" or 
            target_node.node_type.lower() != "decision"):
            return
        
        edge = ContextEdge(
            source_id=source_decision_id,
            target_id=target_decision_id,
            edge_type=relationship_type,
            weight=1.0
        )
        self._add_internal_edge(edge)

    def get_causal_chain(
        self,
        decision_id: str,
        direction: str = "upstream",
        max_depth: int = 10
    ) -> List["Decision"]:
        """
        Get causal chain from graph.
        
        Args:
            decision_id: Starting decision ID
            direction: "upstream" or "downstream"
            max_depth: Maximum traversal depth
            
        Returns:
            List of decisions in causal chain
        """
        from .decision_models import Decision
        
        if direction not in ["upstream", "downstream"]:
            raise ValueError("Direction must be 'upstream' or 'downstream'")
        
        # BFS traversal
        visited = set()
        queue = deque([(decision_id, 0)])
        decisions = []
        
        while queue:
            current_id, depth = queue.popleft()
            
            if current_id in visited or depth > max_depth:
                continue
            
            visited.add(current_id)
            
            # Skip the starting decision - only add connected decisions
            if current_id != decision_id:
                # Get decision node
                if current_id in self.nodes:
                    node = self.nodes[current_id]
                    if (hasattr(node, 'node_type') and isinstance(node.node_type, str) and
                        node.node_type.lower() == "decision"):
                        decision_data = node.properties
                        timestamp = self._normalize_timestamp(decision_data.get("timestamp"))
                        decision = Decision(
                            decision_id=current_id,
                            category=decision_data.get("category", ""),
                            scenario=node.content,
                            reasoning=decision_data.get("reasoning", ""),
                            outcome=decision_data.get("outcome", ""),
                            confidence=decision_data.get("confidence", 0.0),
                            timestamp=timestamp,
                            decision_maker=decision_data.get("decision_maker", ""),
                            reasoning_embedding=decision_data.get("reasoning_embedding"),
                            node2vec_embedding=decision_data.get("node2vec_embedding"),
                            metadata={k: v for k, v in decision_data.items() if k not in [
                                "category", "reasoning", "outcome", "confidence", 
                                "timestamp", "decision_maker", "reasoning_embedding", "node2vec_embedding"
                            ]}
                        )
                        decision.metadata["causal_distance"] = depth
                        decisions.append(decision)
            
            # Find connected decisions
            for edge in self.edges:
                if direction == "upstream":
                    if edge.target_id == current_id and edge.edge_type in ["CAUSED", "INFLUENCED", "PRECEDENT_FOR"]:
                        if edge.source_id not in visited and depth < max_depth:
                            queue.append((edge.source_id, depth + 1))
                else:  # downstream
                    if edge.source_id == current_id and edge.edge_type in ["CAUSED", "INFLUENCED", "PRECEDENT_FOR"]:
                        if edge.target_id not in visited and depth < max_depth:
                            queue.append((edge.target_id, depth + 1))
        
        # Sort by depth for upstream (most distant first) and downstream (closest first)
        if direction == "upstream":
            decisions.sort(key=lambda d: d.metadata.get("causal_distance", 0), reverse=True)
        else:
            decisions.sort(key=lambda d: d.metadata.get("causal_distance", 0))
        
        return decisions

    def find_precedents(self, decision_id: str, limit: int = 10) -> List["Decision"]:
        """
        Find precedent decisions.
        
        Args:
            decision_id: Decision ID to find precedents for
            limit: Maximum number of results
            
        Returns:
            List of precedent decisions
        """
        # Find decisions connected via PRECEDENT_FOR relationships
        precedent_ids = []
        for edge in self.edges:
            if edge.target_id == decision_id and edge.edge_type == "PRECEDENT_FOR":
                precedent_ids.append(edge.source_id)
        
        # Convert to Decision objects
        decisions = []
        for pid in precedent_ids[:limit]:
            if pid in self.nodes:
                node = self.nodes[pid]
                if (hasattr(node, 'node_type') and isinstance(node.node_type, str) and
                    node.node_type.lower() == "decision"):
                    decision_data = node.properties
                    from .decision_models import Decision
                    timestamp = self._normalize_timestamp(decision_data.get("timestamp"))
                    decision = Decision(
                        decision_id=pid,
                        category=decision_data.get("category", ""),
                        scenario=node.content,
                        reasoning=decision_data.get("reasoning", ""),
                        outcome=decision_data.get("outcome", ""),
                        confidence=decision_data.get("confidence", 0.0),
                        timestamp=timestamp,
                        decision_maker=decision_data.get("decision_maker", ""),
                        reasoning_embedding=decision_data.get("reasoning_embedding"),
                        node2vec_embedding=decision_data.get("node2vec_embedding"),
                        metadata={k: v for k, v in decision_data.items() if k not in [
                            "category", "reasoning", "outcome", "confidence", 
                            "timestamp", "decision_maker", "reasoning_embedding", "node2vec_embedding"
                        ]}
                    )
                    decisions.append(decision)
        
        return decisions
    
    # Enhanced methods for comprehensive context graphs
    def analyze_graph_with_kg(self) -> Dict[str, Any]:
        """
        Analyze the context graph using advanced KG algorithms.
        
        Returns:
            Comprehensive graph analysis results
        """
        if not self.kg_components:
            self.logger.warning("KG components not available")
            return {"error": "Advanced features not available"}
        
        try:
            analysis = {
                "graph_metrics": {},
                "centrality_analysis": {},
                "community_analysis": {},
                "connectivity_analysis": {},
                "node_embeddings": {},
                "timestamp": datetime.now().isoformat()
            }
            
            # Convert to KG-compatible format
            kg_graph = self._to_kg_format()
            
            # Basic graph metrics
            analysis["graph_metrics"] = {
                "node_count": len(self.nodes),
                "edge_count": len(self.edges),
                "node_types": self._get_node_type_distribution(),
                "edge_types": self._get_edge_type_distribution()
            }
            
            # Centrality analysis
            if "centrality_calculator" in self.kg_components:
                centrality = self.kg_components["centrality_calculator"].calculate_all_centrality(kg_graph)
                analysis["centrality_analysis"] = centrality
            
            # Community detection
            if "community_detector" in self.kg_components:
                communities = self.kg_components["community_detector"].detect_communities(kg_graph)
                analysis["community_analysis"] = {
                    "communities": communities,
                    "num_communities": len(communities),
                    "modularity": self._calculate_modularity(communities)
                }
            
            # Connectivity analysis
            if "connectivity_analyzer" in self.kg_components:
                connectivity = self.kg_components["connectivity_analyzer"].analyze_connectivity(kg_graph)
                analysis["connectivity_analysis"] = connectivity
            
            # Node embeddings
            if "node_embedder" in self.kg_components:
                embeddings = self.kg_components["node_embedder"].generate_embeddings(kg_graph)
                analysis["node_embeddings"] = embeddings
            
            self.logger.info("Completed comprehensive graph analysis")
            return analysis
            
        except Exception as e:
            self.logger.error(f"Failed to analyze graph with KG: {e}")
            return {"error": "Graph analysis failed due to an internal error"}
    
    def get_node_centrality(self, node_id: str) -> Dict[str, float]:
        """
        Get centrality measures for a specific node.
        
        Args:
            node_id: Node ID to analyze
            
        Returns:
            Dictionary of centrality measures
        """
        if "centrality_calculator" not in self.kg_components:
            return {"error": "Centrality calculator not available"}
        
        if node_id not in self.nodes:
            return {"error": "Node not found"}
        
        # Check cache first
        cache_key = f"centrality_{node_id}"
        if cache_key in self._analytics_cache:
            return self._analytics_cache[cache_key]
        
        try:
            # Get subgraph around the node
            subgraph = self._get_node_subgraph(node_id, max_depth=2)
            
            # Calculate centrality
            centrality = self.kg_components["centrality_calculator"].calculate_all_centrality(subgraph)
            
            # Cache result
            self._analytics_cache[cache_key] = centrality.get(node_id, {})
            
            return centrality.get(node_id, {})
            
        except Exception as e:
            self.logger.error(f"Failed to get node centrality: {e}")
            return {"error": "Node centrality calculation failed due to an internal error"}
    
    def find_similar_nodes(
        self, node_id: str, similarity_type: str = "content", top_k: int = 10
    ) -> List[Tuple[str, float]]:
        """
        Find similar nodes using various similarity measures.
        
        Args:
            node_id: Reference node ID
            similarity_type: Type of similarity ("embedding", "structural", "content")
            top_k: Number of similar nodes to return
            
        Returns:
            List of (node_id, similarity_score) tuples
        """
        if node_id not in self.nodes:
            return []
        
        similar_nodes = []
        reference_node = self.nodes[node_id]
        
        try:
            for other_id, other_node in self.nodes.items():
                if other_id != node_id:
                    if similarity_type == "content":
                        similarity = self._calculate_content_similarity(reference_node, other_node)
                    elif similarity_type == "structural":
                        similarity = self._calculate_structural_similarity(reference_node, other_node)
                    else:
                        similarity = self._calculate_content_similarity(reference_node, other_node)
                    
                    similar_nodes.append((other_id, similarity))
            
            # Sort by similarity and return top_k
            similar_nodes.sort(key=lambda x: x[1], reverse=True)
            return similar_nodes[:top_k]
            
        except Exception as e:
            self.logger.error(f"Failed to find similar nodes: {e}")
            return []
    
    # Helper methods for KG integration
    def _to_kg_format(self) -> Dict[str, Any]:
        """Convert context graph to KG-compatible format."""
        nodes = []
        edges = []
        relationships = []
        
        # Convert nodes
        for node_id, node in self.nodes.items():
            nodes.append({
                "id": node_id,
                "type": node.node_type,
                "properties": node.properties,
                "content": node.content
            })
        
        # Convert edges
        for edge in self.edges:
            edge_data = {
                "source": edge.source_id,
                "target": edge.target_id,
                "type": edge.edge_type,
                "weight": edge.weight,
                "properties": edge.metadata
            }
            edges.append(edge_data)
            relationships.append(edge_data)
        
        return {
            "nodes": nodes, 
            "edges": edges,
            "relationships": relationships  # KG algorithms expect this key
        }
    
    def _get_node_type_distribution(self) -> Dict[str, int]:
        """Get distribution of node types."""
        from collections import defaultdict
        distribution = defaultdict(int)
        for node in self.nodes.values():
            distribution[node.node_type] += 1
        return dict(distribution)
    
    def _get_edge_type_distribution(self) -> Dict[str, int]:
        """Get distribution of edge types."""
        from collections import defaultdict
        distribution = defaultdict(int)
        for edge in self.edges:
            distribution[edge.edge_type] += 1
        return dict(distribution)
    
    def _calculate_modularity(self, communities: Dict) -> float:
        """Calculate modularity for communities (simplified)."""
        # Placeholder for modularity calculation
        return 0.5
    
    def _get_node_subgraph(self, node_id: str, max_depth: int = 2) -> Dict[str, Any]:
        """Get subgraph around a node."""
        neighbors = self.get_neighbors(node_id, hops=max_depth)
        
        subgraph_nodes = {node_id}
        subgraph_edges = []
        
        for neighbor in neighbors:
            neighbor_id = neighbor["id"]
            subgraph_nodes.add(neighbor_id)
        
        # Add edges between nodes in subgraph
        for edge in self.edges:
            if edge.source_id in subgraph_nodes and edge.target_id in subgraph_nodes:
                subgraph_edges.append({
                    "source": edge.source_id,
                    "target": edge.target_id,
                    "type": edge.edge_type,
                    "weight": edge.weight
                })
        
        return {
            "nodes": [{"id": nid} for nid in subgraph_nodes],
            "edges": subgraph_edges
        }
    
    def _calculate_structural_similarity(self, node1: ContextNode, node2: ContextNode) -> float:
        """Calculate structural similarity between two nodes."""
        # Simple structural similarity based on node types and connections
        if node1.node_type != node2.node_type:
            return 0.0
        
        # Count connections
        connections1 = len(self._adjacency.get(node1.node_id, []))
        connections2 = len(self._adjacency.get(node2.node_id, []))
        
        # Similarity based on connection count similarity
        max_connections = max(connections1, connections2, 1)
        return 1.0 - abs(connections1 - connections2) / max_connections
    
    def _calculate_content_similarity(self, node1: ContextNode, node2: ContextNode) -> float:
        """Calculate content similarity between two nodes."""
        words1 = set(node1.content.lower().split())
        words2 = set(node2.content.lower().split())
        
        intersection = words1.intersection(words2)
        union = words1.union(words2)
        
        return len(intersection) / len(union) if union else 0.0
    
    # --- Comprehensive Decision Management Features ---
    
    def record_decision(
        self,
        category: str,
        scenario: str,
        reasoning: str,
        outcome: str,
        confidence: float,
        entities: Optional[List[str]] = None,
        decision_maker: Optional[str] = None,
        metadata: Optional[Dict[str, Any]] = None,
        **kwargs
    ) -> str:
        """
        Record a decision with full context and analytics.
        
        Args:
            category: Decision category (e.g., "loan_approval")
            scenario: Decision scenario description
            reasoning: Decision reasoning explanation
            outcome: Decision outcome
            confidence: Confidence score (0.0 to 1.0)
            entities: Related entities
            decision_maker: Who made the decision
            metadata: Additional metadata
            **kwargs: Additional decision data
            
        Returns:
            Decision ID for reference
        """
        import uuid
        from datetime import datetime
        
        # Input validation
        if not isinstance(category, str) or not category.strip():
            raise ValueError("Category must be a non-empty string")
        if len(category.strip()) > 100:
            raise ValueError("Category must be 100 characters or less")
        
        if not isinstance(scenario, str) or not scenario.strip():
            raise ValueError("Scenario must be a non-empty string")
        if len(scenario.strip()) > 5000:
            raise ValueError("Scenario must be 5000 characters or less")
        
        if not isinstance(reasoning, str) or not reasoning.strip():
            raise ValueError("Reasoning must be a non-empty string")
        if len(reasoning.strip()) > 10000:
            raise ValueError("Reasoning must be 10000 characters or less")
        
        if not isinstance(outcome, str) or not outcome.strip():
            raise ValueError("Outcome must be a non-empty string")
        if len(outcome.strip()) > 1000:
            raise ValueError("Outcome must be 1000 characters or less")
        
        if not isinstance(confidence, (int, float)):
            raise ValueError("Confidence must be a number")
        if not (0.0 <= confidence <= 1.0):
            raise ValueError("Confidence must be between 0.0 and 1.0")
        
        if entities is not None:
            if not isinstance(entities, list):
                raise ValueError("Entities must be a list of strings")
            for entity in entities:
                if not isinstance(entity, str) or not entity.strip():
                    raise ValueError("Each entity must be a non-empty string")
                if len(entity.strip()) > 200:
                    raise ValueError("Each entity must be 200 characters or less")
        
        if decision_maker is not None:
            if not isinstance(decision_maker, str) or not decision_maker.strip():
                raise ValueError("Decision maker must be a non-empty string")
            if len(decision_maker.strip()) > 200:
                raise ValueError("Decision maker must be 200 characters or less")
        
        if metadata is not None:
            if not isinstance(metadata, dict):
                raise ValueError("Metadata must be a dictionary")
            for key, value in metadata.items():
                if not isinstance(key, str) or not key.strip():
                    raise ValueError("Metadata keys must be non-empty strings")
                if len(key.strip()) > 100:
                    raise ValueError("Metadata keys must be 100 characters or less")
                if len(str(value)) > 1000:
                    raise ValueError("Metadata values must be 1000 characters or less")
        
        # Validate kwargs
        for key, value in kwargs.items():
            if not isinstance(key, str) or not key.strip():
                raise ValueError("Additional field names must be non-empty strings")
            if len(key.strip()) > 100:
                raise ValueError("Additional field names must be 100 characters or less")
            if len(str(value)) > 1000:
                raise ValueError("Additional field values must be 1000 characters or less")
        
        decision_id = str(uuid.uuid4())
        timestamp = datetime.now().timestamp()
        
        # Sanitize inputs
        category = category.strip()
        scenario = scenario.strip()
        reasoning = reasoning.strip()
        outcome = outcome.strip()
        confidence = float(confidence)
        entities = [entity.strip() for entity in (entities or []) if entity.strip()]
        decision_maker = decision_maker.strip() if decision_maker else None
        
        # Create decision record
        decision = {
            "id": decision_id,
            "category": category,
            "scenario": scenario,
            "reasoning": reasoning,
            "outcome": outcome,
            "confidence": confidence,
            "entities": entities,
            "decision_maker": decision_maker,
            "timestamp": timestamp,
            "metadata": metadata or {},
            **kwargs
        }
        
        # Store decision in graph
        self._add_decision_to_graph(decision)
        
        # Store in internal decision storage
        if not hasattr(self, '_decisions'):
            self._decisions = {}
            self._decision_index = defaultdict(set)
            self._entity_index = defaultdict(set)
            self._temporal_index = []
        
        self._decisions[decision_id] = decision
        self._decision_index[category].add(decision_id)
        
        for entity in entities or []:
            self._entity_index[entity].add(decision_id)
        
        self._temporal_index.append((decision_id, timestamp))
        self._temporal_index.sort(key=lambda x: x[1], reverse=True)
        
        self.logger.info(f"Recorded decision {decision_id} in category {category}")
        return decision_id
    
    def find_precedents_by_scenario(
        self,
        scenario: str,
        category: Optional[str] = None,
        limit: int = 10,
        similarity_threshold: float = 0.5,
        use_semantic_search: bool = True,
        **filters
    ) -> List[Dict[str, Any]]:
        """
        Find similar decisions (precedents) using hybrid search.
        
        Args:
            scenario: Scenario to find precedents for
            category: Filter by decision category
            limit: Maximum number of precedents
            similarity_threshold: Minimum similarity score
            use_semantic_search: Use vector embeddings for search
            **filters: Additional filters
            
        Returns:
            List of similar decisions with similarity scores
        """
        if not hasattr(self, '_decisions') or not self._decisions:
            return []
        
        candidates = set()
        
        # Get candidates by category
        if category:
            candidates.update(self._decision_index.get(category, set()))
        else:
            candidates.update(self._decisions.keys())
        
        # Filter by entities if provided
        if "entities" in filters:
            entity_candidates = set()
            for entity in filters["entities"]:
                entity_candidates.update(self._entity_index.get(entity, set()))
            candidates = candidates.intersection(entity_candidates)
        
        # Calculate similarities
        precedents = []
        for decision_id in candidates:
            decision = self._decisions[decision_id]
            
            # Content similarity
            content_sim = self._calculate_decision_content_similarity(scenario, decision)
            
            # Structural similarity (graph-based)
            structural_sim = 0.0
            if self.config.get("advanced_analytics"):
                structural_sim = self._calculate_structural_similarity_for_decision(decision_id, scenario)
            
            # Combined similarity
            combined_sim = 0.7 * content_sim + 0.3 * structural_sim
            
            if combined_sim >= similarity_threshold:
                precedents.append({
                    "decision": decision,
                    "similarity": combined_sim,
                    "content_similarity": content_sim,
                    "structural_similarity": structural_sim
                })
        
        # Sort by similarity and limit
        precedents.sort(key=lambda x: x["similarity"], reverse=True)
        return precedents[:limit]
    
    def analyze_decision_influence(
        self,
        decision_id: str,
        max_depth: int = 3,
        include_indirect: bool = True
    ) -> Dict[str, Any]:
        """
        Analyze decision influence and impact.
        
        Args:
            decision_id: Decision to analyze
            max_depth: Maximum depth for influence analysis
            include_indirect: Include indirect influences
            
        Returns:
            Influence analysis results
        """
        if not hasattr(self, '_decisions') or decision_id not in self._decisions:
            raise ValueError(f"Decision {decision_id} not found")
        
        decision = self._decisions[decision_id]
        
        # Direct influence (same entities, category)
        direct_influence = set()
        for entity in decision["entities"]:
            direct_influence.update(self._entity_index.get(entity, set()))
        direct_influence.discard(decision_id)
        direct_influence.update(self._decision_index.get(decision["category"], set()))
        direct_influence.discard(decision_id)
        
        # Indirect influence (through graph relationships)
        indirect_influence = set()
        if include_indirect and self.config.get("advanced_analytics"):
            indirect_influence = self._find_indirect_decision_influence(decision_id, max_depth)
        
        # Calculate influence scores
        influence_scores = {}
        for influenced_id in direct_influence | indirect_influence:
            score = self._calculate_decision_influence_score(decision_id, influenced_id)
            influence_scores[influenced_id] = score
        
        # Sort by influence score
        sorted_influence = sorted(
            influence_scores.items(),
            key=lambda x: x[1],
            reverse=True
        )
        
        return {
            "decision_id": decision_id,
            "direct_influence": list(direct_influence),
            "indirect_influence": list(indirect_influence),
            "influence_scores": sorted_influence,
            "total_influenced": len(influence_scores),
            "max_influence_score": max(influence_scores.values()) if influence_scores else 0.0
        }
    
    def get_decision_insights(self) -> Dict[str, Any]:
        """
        Get comprehensive insights about all decisions.
        
        Returns:
            Comprehensive analytics and insights
        """
        if not hasattr(self, '_decisions') or not self._decisions:
            return {"message": "No decisions recorded yet"}
        
        # Basic statistics
        total_decisions = len(self._decisions)
        categories = {}
        outcomes = {}
        confidence_scores = []
        
        for decision in self._decisions.values():
            # Category distribution
            categories[decision["category"]] = categories.get(decision["category"], 0) + 1
            
            # Outcome distribution
            outcomes[decision["outcome"]] = outcomes.get(decision["outcome"], 0) + 1
            
            # Confidence scores
            confidence_scores.append(decision["confidence"])
        
        # Advanced analytics (if available)
        advanced_insights = {}
        if self.config.get("advanced_analytics"):
            advanced_insights = self.analyze_graph_with_kg()
        
        # Temporal analysis
        temporal_insights = self._get_decision_temporal_analysis()
        
        # Entity analysis
        entity_insights = self._get_decision_entity_analysis()
        
        return {
            "total_decisions": total_decisions,
            "categories": categories,
            "outcomes": outcomes,
            "confidence_stats": {
                "mean": sum(confidence_scores) / len(confidence_scores),
                "min": min(confidence_scores),
                "max": max(confidence_scores),
                "median": sorted(confidence_scores)[len(confidence_scores) // 2]
            },
            "advanced_analytics": advanced_insights,
            "temporal_analysis": temporal_insights,
            "entity_analysis": entity_insights,
            "graph_metrics": self.get_graph_metrics() if hasattr(self, 'get_graph_metrics') else {}
        }
    
    def trace_decision_causality(
        self,
        decision_id: str,
        max_depth: int = 5
    ) -> List[Dict[str, Any]]:
        """
        Trace causal chain for a decision.
        
        Args:
            decision_id: Decision to trace
            max_depth: Maximum depth for causal analysis
            
        Returns:
            Causal chain as list of decision relationships
        """
        if not hasattr(self, '_decisions') or decision_id not in self._decisions:
            raise ValueError(f"Decision {decision_id} not found")
        
        try:
            # Use graph traversal to find causal relationships
            causal_chain = []
            visited = set()
            
            def trace_recursive(current_id, depth, path):
                if depth >= max_depth or current_id in visited:
                    return
                
                visited.add(current_id)
                current_decision = self._decisions[current_id]
                
                # Find potential causes (decisions that influenced this one)
                potential_causes = []
                for entity in current_decision["entities"]:
                    for other_decision_id in self._entity_index.get(entity, set()):
                        if other_decision_id != current_id:
                            other_decision = self._decisions[other_decision_id]
                            if other_decision["timestamp"] < current_decision["timestamp"]:
                                potential_causes.append(other_decision_id)
                
                for cause_id in potential_causes:
                    cause_path = path + [{"from": cause_id, "to": current_id, "type": "influences"}]
                    causal_chain.append(cause_path)
                    trace_recursive(cause_id, depth + 1, cause_path)
            
            trace_recursive(decision_id, 0, [])
            return causal_chain
            
        except Exception as e:
            self.logger.error(f"Causal analysis failed: {e}")
            return [{"error": "Causal analysis failed due to an internal error"}]
    
    def enforce_decision_policy(
        self,
        decision_data: Dict[str, Any],
        policy_rules: Optional[Dict[str, Any]] = None
    ) -> Dict[str, Any]:
        """
        Enforce policies on decision data.
        
        Args:
            decision_data: Decision data to check
            policy_rules: Policy rules to enforce
            
        Returns:
            Policy enforcement results
        """
        # Simple policy enforcement implementation
        violations = []
        warnings = []
        
        # Default policy rules
        default_rules = {
            "min_confidence": 0.7,
            "required_outcomes": ["approved", "rejected", "flagged"],
            "required_metadata": ["decision_maker"],
            "max_reasoning_length": 1000
        }
        
        rules = policy_rules or default_rules
        
        # Check confidence
        if decision_data.get("confidence", 0) < rules.get("min_confidence", 0.7):
            violations.append(f"Confidence too low: {decision_data.get('confidence', 0)}")
        
        # Check outcome
        if decision_data.get("outcome") not in rules.get("required_outcomes", []):
            violations.append(f"Invalid outcome: {decision_data.get('outcome')}")
        
        # Check required metadata
        for required_field in rules.get("required_metadata", []):
            if not decision_data.get(required_field):
                violations.append(f"Missing required field: {required_field}")
        
        # Check reasoning length
        reasoning = decision_data.get("reasoning", "")
        if len(reasoning) > rules.get("max_reasoning_length", 1000):
            warnings.append(f"Reasoning too long: {len(reasoning)} characters")
        
        return {
            "compliant": len(violations) == 0,
            "violations": violations,
            "warnings": warnings,
            "policy_rules": rules
        }
    
    # --- Private helper methods for decision management ---
    
    def _add_decision_to_graph(self, decision: Dict[str, Any]) -> None:
        """Add decision to context graph."""
        try:
            # Add decision node
            self.add_node(
                decision["id"],
                "decision",
                category=decision["category"],
                outcome=decision["outcome"],
                confidence=decision["confidence"],
                timestamp=decision["timestamp"],
                scenario=decision["scenario"][:100] + "..." if len(decision["scenario"]) > 100 else decision["scenario"],
                decision_maker=decision.get("decision_maker", ""),
                reasoning=decision["reasoning"][:200] + "..." if len(decision["reasoning"]) > 200 else decision["reasoning"]
            )
            
            # Add entity nodes and relationships
            for entity in decision["entities"]:
                # Add entity node if not exists
                if not self.find_node(entity):
                    self.add_node(
                        entity,
                        "entity",
                        name=entity
                    )
                
                # Add relationship
                self.add_edge(
                    decision["id"],
                    entity,
                    "involves",
                    confidence=decision["confidence"]
                )
            
            # Add category node and relationship
            category_id = f"category_{decision['category']}"
            if not self.find_node(category_id):
                self.add_node(
                    category_id,
                    "category",
                    name=decision["category"]
                )
            
            self.add_edge(
                decision["id"],
                category_id,
                "belongs_to"
            )
            
            # Add decision maker node if provided
            if decision.get("decision_maker"):
                maker_id = f"maker_{decision['decision_maker']}"
                if not self.find_node(maker_id):
                    self.add_node(
                        maker_id,
                        "decision_maker",
                        name=decision["decision_maker"]
                    )
                
                self.add_edge(
                    decision["id"],
                    maker_id,
                    "made_by"
                )
            
        except Exception as e:
            self.logger.exception("Failed to add decision to graph")
    
    def _calculate_decision_content_similarity(self, scenario: str, decision: Dict[str, Any]) -> float:
        """Calculate content similarity between scenario and decision."""
        try:
            # Simple word-based similarity
            scenario_words = set(scenario.lower().split())
            decision_text = f"{decision['scenario']} {decision['reasoning']} {' '.join(decision['entities'])}"
            decision_words = set(decision_text.lower().split())
            
            intersection = scenario_words.intersection(decision_words)
            union = scenario_words.union(decision_words)
            
            return len(intersection) / len(union) if union else 0.0
            
        except Exception as e:
            self.logger.exception("Content similarity calculation failed")
            return 0.0
    
    def _calculate_structural_similarity_for_decision(self, decision_id: str, scenario: str) -> float:
        """Calculate structural similarity using graph algorithms."""
        try:
            if not self.config.get("advanced_analytics"):
                return 0.0
            
            # Use graph similarity algorithms
            similar_nodes = self.find_similar_nodes(
                decision_id,
                similarity_type="structural",
                top_k=5
            )
            
            if similar_nodes:
                # similar_nodes is List[Tuple[str, float]], extract similarity scores
                return max(similarity for node_id, similarity in similar_nodes)
            
        except Exception as e:
            self.logger.exception("Structural similarity calculation failed")
        
        return 0.0
    
    def _find_indirect_decision_influence(self, decision_id: str, max_depth: int) -> Set[str]:
        """Find indirect influences using graph traversal."""
        try:
            influenced = set()
            
            # Get neighbors in graph
            neighbors = self.get_neighbors(decision_id, hops=max_depth)
            
            for neighbor in neighbors:
                if neighbor.get("type") == "decision":
                    influenced.add(neighbor["id"])
            
            return influenced
            
        except Exception as e:
            self.logger.warning(f"Indirect influence analysis failed: {e}")
            return set()
    
    def _calculate_decision_influence_score(self, source_id: str, target_id: str) -> float:
        """Calculate influence score between two decisions."""
        try:
            if not hasattr(self, '_decisions'):
                return 0.0
                
            source_decision = self._decisions[source_id]
            target_decision = self._decisions[target_id]
            
            # Base score from shared entities
            shared_entities = set(source_decision["entities"]) & set(target_decision["entities"])
            entity_score = len(shared_entities) / max(len(source_decision["entities"]), 1)
            
            # Category similarity
            category_score = 1.0 if source_decision["category"] == target_decision["category"] else 0.0
            
            # Temporal proximity (more recent decisions have higher influence)
            time_diff = abs(source_decision["timestamp"] - target_decision["timestamp"])
            time_score = max(0.0, 1.0 - time_diff / (30 * 24 * 3600))  # 30 days window
            
            # Combined score
            combined_score = 0.5 * entity_score + 0.3 * category_score + 0.2 * time_score
            
            return combined_score
            
        except Exception as e:
            self.logger.warning(f"Influence score calculation failed: {e}")
            return 0.0
    
    def _get_decision_temporal_analysis(self) -> Dict[str, Any]:
        """Get temporal analysis of decisions."""
        try:
            if not hasattr(self, '_temporal_index') or not self._temporal_index:
                return {}
            
            # Group decisions by time periods
            recent_decisions = [did for did, ts in self._temporal_index[:10]]
            
            return {
                "recent_decisions": len(recent_decisions),
                "oldest_decision": min(ts for _, ts in self._temporal_index),
                "newest_decision": max(ts for _, ts in self._temporal_index),
                "time_span": max(ts for _, ts in self._temporal_index) - min(ts for _, ts in self._temporal_index)
            }
            
        except Exception as e:
            self.logger.warning(f"Temporal analysis failed: {e}")
            return {}
    
    def _get_decision_entity_analysis(self) -> Dict[str, Any]:
        """Get entity analysis from decisions."""
        try:
            if not hasattr(self, '_decisions'):
                return {}
                
            entity_counts = {}
            for decision in self._decisions.values():
                for entity in decision["entities"]:
                    entity_counts[entity] = entity_counts.get(entity, 0) + 1
            
            # Get top entities
            top_entities = sorted(entity_counts.items(), key=lambda x: x[1], reverse=True)[:10]
            
            return {
                "total_entities": len(entity_counts),
                "top_entities": top_entities,
                "avg_entities_per_decision": sum(len(d["entities"]) for d in self._decisions.values()) / len(self._decisions)
            }
            
        except Exception as e:
            self.logger.warning(f"Entity analysis failed: {e}")
            return {}
    
    # --- Easy-to-Use Convenience Methods ---
    
    def add_decision_simple(
        self,
        category: str,
        scenario: str,
        reasoning: str,
        outcome: str,
        confidence: float = 0.5,
        entities: Optional[List[str]] = None,
        decision_maker: Optional[str] = "system",
        **kwargs
    ) -> str:
        """
        Easy way to record a decision.
        
        Args:
            category: Decision category (e.g., "loan_approval")
            scenario: What was the situation
            reasoning: Why was this decision made
            outcome: What was decided
            confidence: How confident (0.0 to 1.0)
            entities: Related entities (people, items, etc.)
            decision_maker: Who made the decision
            **kwargs: Additional information
            
        Returns:
            Decision ID for reference
        """
        return self.record_decision(
            category=category,
            scenario=scenario,
            reasoning=reasoning,
            outcome=outcome,
            confidence=confidence,
            entities=entities,
            decision_maker=decision_maker,
            metadata=kwargs
        )
    
    def find_similar_decisions(
        self,
        scenario: str,
        category: Optional[str] = None,
        max_results: int = 10,
        min_similarity: float = 0.3
    ) -> List[Dict[str, Any]]:
        """
        Easy way to find similar past decisions.
        
        Args:
            scenario: What situation are you looking for
            category: Filter by decision type
            max_results: Maximum results to return
            min_similarity: Minimum similarity score
            
        Returns:
            List of similar decisions with similarity scores
        """
        return self.find_precedents_by_scenario(
            scenario=scenario,
            category=category,
            limit=max_results,
            similarity_threshold=min_similarity
        )
    
    def analyze_decision_impact(
        self,
        decision_id: str,
        include_indirect: bool = True
    ) -> Dict[str, Any]:
        """
        Easy way to analyze how a decision impacts others.
        
        Args:
            decision_id: Decision to analyze
            include_indirect: Include indirect impacts
            
        Returns:
            Impact analysis results
        """
        return self.analyze_decision_influence(
            decision_id=decision_id,
            max_depth=3,
            include_indirect=include_indirect
        )
    
    def get_decision_summary(self) -> Dict[str, Any]:
        """
        Easy way to get a summary of all decisions.
        
        Returns:
            Summary statistics and insights
        """
        return self.get_decision_insights()
    
    def trace_decision_chain(
        self,
        decision_id: str,
        max_steps: int = 5
    ) -> List[Dict[str, Any]]:
        """
        Easy way to trace how decisions are connected.
        
        Args:
            decision_id: Starting decision
            max_steps: Maximum steps to trace
            
        Returns:
            Decision chain connections
        """
        return self.trace_decision_causality(
            decision_id=decision_id,
            max_depth=max_steps
        )
    
    def check_decision_rules(
        self,
        decision_data: Dict[str, Any],
        rules: Optional[Dict[str, Any]] = None
    ) -> Dict[str, Any]:
        """
        Easy way to check if a decision follows the rules.
        
        Args:
            decision_data: Decision to check
            rules: Custom rules (uses default if None)
            
        Returns:
            Compliance check results
        """
        return self.enforce_decision_policy(
            decision_data=decision_data,
            policy_rules=rules
        )
    
    def get_graph_summary(self) -> Dict[str, Any]:
        """
        Easy way to get graph statistics.
        
        Returns:
            Graph summary information
        """
        if hasattr(self, 'get_graph_metrics'):
            return self.get_graph_metrics()
        else:
            return {
                "nodes": len(self.nodes),
                "edges": len(self.edges),
                "node_types": self._get_node_type_distribution(),
                "edge_types": self._get_edge_type_distribution()
            }
    
    def find_related_nodes(
        self,
        node_id: str,
        how_many: int = 10,
        similarity_type: str = "content"
    ) -> List[Tuple[str, float]]:
        """
        Easy way to find nodes similar to a given node.
        
        Args:
            node_id: Reference node
            how_many: How many similar nodes to find
            similarity_type: Type of similarity ("content", "structural")
            
        Returns:
            List of (node_id, similarity_score) tuples
        """
        return self.find_similar_nodes(
            node_id=node_id,
            similarity_type=similarity_type,
            top_k=how_many
        )
    
    def get_node_importance(
        self,
        node_id: str
    ) -> Dict[str, float]:
        """
        Easy way to get how important a node is in the graph.
        
        Args:
            node_id: Node to analyze
            
        Returns:
            Centrality measures (importance scores)
        """
        return self.get_node_centrality(node_id)
    
    def analyze_connections(self) -> Dict[str, Any]:
        """
        Easy way to analyze the entire graph structure.
        
        Returns:
            Graph analysis results
        """
        return self.analyze_graph_with_kg()


# For backward compatibility
ContextGraphBuilder = ContextGraph
