"""
ArangoDB Multi-Model Database Helper Library
A comprehensive helper class for interacting with ArangoDB.
Supports both document and graph operations in a multi-model database.
"""

import os
import asyncio
import json
import time
import logging
import hashlib
from datetime import datetime, timedelta
from typing import List, Dict, Optional, Union, Callable, Any, Set
from dataclasses import dataclass, field
from pathlib import Path

try:
    from arango import ArangoClient
    from arango.exceptions import (
        AQLValidationError,
        DocumentParseError,
        VertexNotFoundError,
        EdgeNotFoundError,
    )
except ImportError:
    raise ImportError("Please install python-arango: pip install python-arango")


@dataclass
class ArangoDBConfig:
    """Configuration class for ArangoDB helper."""
    host: str = "http://127.0.0.1"
    port: int = 8529
    username: str = "root"
    password: str = "password"
    database: str = "_system"
    # Connection settings
    verify: bool = True
    http_client: Optional[Any] = None
    request_timeout: int = 30
    connection_pool_size: int = 10
    # ArangoSearch settings
    enable_caching: bool = True
    cache_ttl: int = 3600
    log_level: str = "INFO"

    def __post_init__(self):
        # Env overrides
        self.host = os.getenv("ARANGODB_HOST", self.host)
        self.port = int(os.getenv("ARANGODB_PORT", str(self.port)))
        self.username = os.getenv("ARANGODB_USERNAME", self.username)
        self.password = os.getenv("ARANGODB_PASSWORD", self.password)
        self.database = os.getenv("ARANGODB_DATABASE", self.database)

        # Build URL
        if not self.host.startswith("http"):
            self.host = f"http://{self.host}"


@dataclass
class CacheEntry:
    """Cache entry with TTL support."""
    data: Any
    created_at: datetime
    ttl: int

    def is_expired(self) -> bool:
        return datetime.now() > self.created_at + timedelta(seconds=self.ttl)


@dataclass
class UsageStats:
    """Usage statistics tracking for ArangoDB operations."""
    total_queries: int = 0
    read_queries: int = 0
    write_queries: int = 0
    failed_queries: int = 0
    total_latency_ms: float = 0.0

    def record_query(self, is_write: bool = False, latency_ms: float = 0.0, success: bool = True):
        self.total_queries += 1
        if is_write:
            self.write_queries += 1
        else:
            self.read_queries += 1
        if not success:
            self.failed_queries += 1
        self.total_latency_ms += latency_ms

    def get_avg_latency(self) -> float:
        if self.total_queries == 0:
            return 0.0
        return self.total_latency_ms / self.total_queries


class ArangoDBHelper:
    """Enhanced ArangoDB helper with advanced features."""

    def __init__(self, config: Optional[ArangoDBConfig] = None):
        self.config = config or ArangoDBConfig()
        self._client: Optional[ArangoClient] = None
        self._db = None
        self._setup_logging()
        self._setup_cache()
        self._setup_usage_tracking()

    def _setup_logging(self):
        logging.basicConfig(
            level=getattr(logging, self.config.log_level.upper(), logging.INFO),
            format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
        )
        self.logger = logging.getLogger(self.__class__.__name__)

    def _setup_cache(self):
        self._cache: Dict[str, CacheEntry] = {}

    def _setup_usage_tracking(self):
        self.usage_stats = UsageStats()

    # ---------------- Connection Management ----------------
    def connect(self) -> "ArangoDBHelper":
        """Establish connection to ArangoDB."""
        if self._client is None:
            # Build URL
            host = self.config.host
            if self.config.port:
                host = f"{self.config.host}:{self.config.port}"

            self._client = ArangoClient(
                host=host,
                verify=self.config.verify,
                http_client=self.config.http_client,
                request_timeout=self.config.request_timeout,
                connection_pool_size=self.config.connection_pool_size,
            )

            # Connect to database
            self._db = self._client.db(
                self.config.database,
                username=self.config.username,
                password=self.config.password,
            )

            # Verify connectivity
            if not self._db.is_ready():
                raise ConnectionError("ArangoDB is not ready")

            self.logger.info(f"Connected to ArangoDB at {host}, database: {self.config.database}")

        return self

    def connect_async(self) -> "ArangoDBHelper":
        """Connect method alias for compatibility."""
        return self.connect()

    def close(self):
        """Close the ArangoDB connection."""
        if self._client:
            self._client.close()
            self._client = None
            self._db = None
            self.logger.info("ArangoDB connection closed")

    async def close_async(self):
        """Close async connection."""
        self.close()

    def verify_connectivity(self) -> bool:
        """Verify that the connection is alive."""
        try:
            if self._db is None:
                self.connect()
            return self._db.is_ready()
        except Exception as e:
            self.logger.error(f"Connectivity check failed: {e}")
            return False

    # ---------------- Cache helpers ----------------
    def _cache_key(self, *args, **kwargs) -> str:
        key_data = {"args": args, "kwargs": kwargs}
        key_str = json.dumps(key_data, sort_keys=True, default=str)
        return hashlib.md5(key_str.encode()).hexdigest()

    def _get_from_cache(self, key: str) -> Optional[Any]:
        if not self.config.enable_caching:
            return None
        entry = self._cache.get(key)
        if entry and not entry.is_expired():
            self.logger.debug(f"Cache hit for key: {key}")
            return entry.data
        if entry:
            self._cache.pop(key, None)
        return None

    def _set_cache(self, key: str, data: Any, ttl: Optional[int] = None):
        if not self.config.enable_caching:
            return
        ttl = ttl or self.config.cache_ttl
        self._cache[key] = CacheEntry(data=data, created_at=datetime.now(), ttl=ttl)
        self.logger.debug(f"Cache set for key: {key}")

    # ---------------- Database Operations ----------------
    def list_databases(self) -> List[str]:
        """List all databases."""
        return self._client.databases()

    def create_database(self, name: str) -> bool:
        """Create a new database."""
        try:
            self._client.create_database(name)
            return True
        except Exception as e:
            self.logger.error(f"Failed to create database: {e}")
            return False

    def use_database(self, name: str) -> bool:
        """Switch to a different database."""
        try:
            self._db = self._client.db(name, username=self.config.username, password=self.config.password)
            self.config.database = name
            return True
        except Exception as e:
            self.logger.error(f"Failed to use database: {e}")
            return False

    # ---------------- Collection (Document) Operations ----------------
    def list_collections(self) -> List[str]:
        """List all collections."""
        return [col["name"] for col in self._db.collections()]

    def create_collection(self, name: str, edge: bool = False) -> bool:
        """Create a collection (document or edge)."""
        try:
            self._db.create_collection(name, edge=edge)
            return True
        except Exception as e:
            self.logger.error(f"Failed to create collection: {e}")
            return False

    def get_collection(self, name: str):
        """Get a collection object."""
        return self._db.collection(name)

    def delete_collection(self, name: str) -> bool:
        """Delete a collection."""
        try:
            self._db.delete_collection(name)
            return True
        except Exception as e:
            self.logger.error(f"Failed to delete collection: {e}")
            return False

    # ---------------- Document Operations ----------------
    def insert_document(
        self,
        collection: str,
        document: Dict[str, Any],
        return_new: bool = False,
    ) -> Optional[Dict[str, Any]]:
        """Insert a document."""
        start_time = time.time()
        try:
            result = self._db.collection(collection).insert(document, return_new=return_new)
            latency_ms = (time.time() - start_time) * 1000
            self.usage_stats.record_query(is_write=True, latency_ms=latency_ms, success=True)
            return result
        except Exception as e:
            latency_ms = (time.time() - start_time) * 1000
            self.usage_stats.record_query(is_write=True, latency_ms=latency_ms, success=False)
            self.logger.error(f"Insert document failed: {e}")
            raise

    def insert_documents(
        self,
        collection: str,
        documents: List[Dict[str, Any]],
    ) -> List[Dict[str, Any]]:
        """Insert multiple documents."""
        start_time = time.time()
        try:
            result = self._db.collection(collection).insert_many(documents)
            latency_ms = (time.time() - start_time) * 1000
            self.usage_stats.record_query(is_write=True, latency_ms=latency_ms, success=True)
            return result
        except Exception as e:
            latency_ms = (time.time() - start_time) * 1000
            self.usage_stats.record_query(is_write=True, latency_ms=latency_ms, success=False)
            self.logger.error(f"Insert documents failed: {e}")
            raise

    def get_document(
        self,
        collection: str,
        document_key: str,
    ) -> Optional[Dict[str, Any]]:
        """Get a document by key."""
        start_time = time.time()
        try:
            result = self._db.collection(collection).get(document_key)
            latency_ms = (time.time() - start_time) * 1000
            self.usage_stats.record_query(is_write=False, latency_ms=latency_ms, success=True)
            return result
        except Exception as e:
            latency_ms = (time.time() - start_time) * 1000
            self.usage_stats.record_query(is_write=False, latency_ms=latency_ms, success=False)
            return None

    def get_documents(
        self,
        collection: str,
        filter_key: Optional[str] = None,
        filter_value: Any = None,
        limit: int = 100,
    ) -> List[Dict[str, Any]]:
        """Get documents with optional filtering."""
        start_time = time.time()

        try:
            if filter_key and filter_value:
                aql = f"FOR doc IN {collection} FILTER doc.{filter_key} == @value LIMIT {limit} RETURN doc"
                bind_vars = {"value": filter_value}
                cursor = self._db.aql.execute(aql, bind_vars=bind_vars)
            else:
                cursor = self._db.collection(collection).all(limit=limit)

            result = list(cursor)
            latency_ms = (time.time() - start_time) * 1000
            self.usage_stats.record_query(is_write=False, latency_ms=latency_ms, success=True)
            return result

        except Exception as e:
            latency_ms = (time.time() - start_time) * 1000
            self.usage_stats.record_query(is_write=False, latency_ms=latency_ms, success=False)
            self.logger.error(f"Get documents failed: {e}")
            return []

    def update_document(
        self,
        collection: str,
        document_key: str,
        document: Dict[str, Any],
        return_new: bool = True,
    ) -> Optional[Dict[str, Any]]:
        """Update a document."""
        start_time = time.time()
        try:
            result = self._db.collection(collection).update(document_key, document, return_new=return_new)
            latency_ms = (time.time() - start_time) * 1000
            self.usage_stats.record_query(is_write=True, latency_ms=latency_ms, success=True)
            return result
        except Exception as e:
            latency_ms = (time.time() - start_time) * 1000
            self.usage_stats.record_query(is_write=True, latency_ms=latency_ms, success=False)
            self.logger.error(f"Update document failed: {e}")
            raise

    def replace_document(
        self,
        collection: str,
        document_key: str,
        document: Dict[str, Any],
        return_new: bool = True,
    ) -> Optional[Dict[str, Any]]:
        """Replace a document."""
        start_time = time.time()
        try:
            result = self._db.collection(collection).replace(document_key, document, return_new=return_new)
            latency_ms = (time.time() - start_time) * 1000
            self.usage_stats.record_query(is_write=True, latency_ms=latency_ms, success=True)
            return result
        except Exception as e:
            latency_ms = (time.time() - start_time) * 1000
            self.usage_stats.record_query(is_write=True, latency_ms=latency_ms, success=False)
            self.logger.error(f"Replace document failed: {e}")
            raise

    def delete_document(
        self,
        collection: str,
        document_key: str,
    ) -> bool:
        """Delete a document."""
        start_time = time.time()
        try:
            result = self._db.collection(collection).delete(document_key)
            latency_ms = (time.time() - start_time) * 1000
            self.usage_stats.record_query(is_write=True, latency_ms=latency_ms, success=True)
            return True
        except Exception as e:
            latency_ms = (time.time() - start_time) * 1000
            self.usage_stats.record_query(is_write=True, latency_ms=latency_ms, success=False)
            return False

    def count_documents(self, collection: str) -> int:
        """Count documents in a collection."""
        try:
            return self._db.collection(collection).count()
        except Exception:
            return 0

    # ---------------- AQL Query Execution ----------------
    def execute_aql(
        self,
        query: str,
        bind_vars: Optional[Dict[str, Any]] = None,
        use_cache: bool = False,
    ) -> List[Dict[str, Any]]:
        """Execute an AQL query."""
        start_time = time.time()

        try:
            if use_cache and self.config.enable_caching:
                cache_key = self._cache_key("aql", query, bind_vars)
                cached = self._get_from_cache(cache_key)
                if cached is not None:
                    return cached

            cursor = self._db.aql.execute(query, bind_vars=bind_vars or {})
            result = list(cursor)

            latency_ms = (time.time() - start_time) * 1000
            is_write = query.strip().upper().startswith(("INSERT", "UPDATE", "REPLACE", "REMOVE", "UPSERT"))
            self.usage_stats.record_query(is_write=is_write, latency_ms=latency_ms, success=True)

            if use_cache and self.config.enable_caching:
                self._set_cache(cache_key, result)

            return result

        except Exception as e:
            latency_ms = (time.time() - start_time) * 1000
            self.usage_stats.record_query(latency_ms=latency_ms, success=False)
            self.logger.error(f"AQL query failed: {e}")
            raise

    def execute_aql_batch(
        self,
        queries: List[Dict[str, str]],
    ) -> List[List[Dict[str, Any]]]:
        """Execute multiple AQL queries in batch."""
        results = []
        for q in queries:
            query = q.get("query", "")
            bind_vars = q.get("bind_vars", {})
            try:
                result = self.execute_aql(query, bind_vars)
                results.append(result)
            except Exception as e:
                self.logger.error(f"Query failed: {query}, Error: {e}")
                results.append([])
        return results

    # ---------------- Graph Operations ----------------
    def list_graphs(self) -> List[str]:
        """List all graphs."""
        return [graph["name"] for graph in self._db.graphs()]

    def create_graph(
        self,
        name: str,
        edge_definitions: Optional[List[Dict[str, Any]]] = None,
    ) -> bool:
        """Create a graph."""
        try:
            self._db.create_graph(name, edge_definitions=edge_definitions or [])
            return True
        except Exception as e:
            self.logger.error(f"Failed to create graph: {e}")
            return False

    def get_graph(self, name: str):
        """Get a graph object."""
        return self._db.graph(name)

    def delete_graph(self, name: str) -> bool:
        """Delete a graph."""
        try:
            self._db.delete_graph(name)
            return True
        except Exception as e:
            self.logger.error(f"Failed to delete graph: {e}")
            return False

    def add_edge_definition(
        self,
        graph_name: str,
        edge_collection: str,
        from_collections: List[str],
        to_collections: List[str],
    ) -> bool:
        """Add an edge definition to a graph."""
        try:
            graph = self._db.graph(graph_name)
            graph.create_edge_definition(
                edge_collection=edge_collection,
                from_collections=from_collections,
                to_collections=to_collections,
            )
            return True
        except Exception as e:
            self.logger.error(f"Failed to add edge definition: {e}")
            return False

    # ---------------- Vertex Operations ----------------
    def insert_vertex(
        self,
        graph_name: str,
        collection: str,
        vertex: Dict[str, Any],
    ) -> Optional[Dict[str, Any]]:
        """Insert a vertex in a graph."""
        start_time = time.time()
        try:
            graph = self._db.graph(graph_name)
            result = graph.vertex_collection(collection).insert(vertex)
            latency_ms = (time.time() - start_time) * 1000
            self.usage_stats.record_query(is_write=True, latency_ms=latency_ms, success=True)
            return result
        except Exception as e:
            latency_ms = (time.time() - start_time) * 1000
            self.usage_stats.record_query(is_write=True, latency_ms=latency_ms, success=False)
            self.logger.error(f"Insert vertex failed: {e}")
            raise

    def get_vertex(
        self,
        graph_name: str,
        collection: str,
        vertex_key: str,
    ) -> Optional[Dict[str, Any]]:
        """Get a vertex from a graph."""
        start_time = time.time()
        try:
            graph = self._db.graph(graph_name)
            result = graph.vertex_collection(collection).get(vertex_key)
            latency_ms = (time.time() - start_time) * 1000
            self.usage_stats.record_query(is_write=False, latency_ms=latency_ms, success=True)
            return result
        except Exception as e:
            latency_ms = (time.time() - start_time) * 1000
            self.usage_stats.record_query(is_write=False, latency_ms=latency_ms, success=False)
            return None

    def update_vertex(
        self,
        graph_name: str,
        collection: str,
        vertex_key: str,
        vertex: Dict[str, Any],
    ) -> Optional[Dict[str, Any]]:
        """Update a vertex in a graph."""
        start_time = time.time()
        try:
            graph = self._db.graph(graph_name)
            result = graph.vertex_collection(collection).update(vertex_key, vertex)
            latency_ms = (time.time() - start_time) * 1000
            self.usage_stats.record_query(is_write=True, latency_ms=latency_ms, success=True)
            return result
        except Exception as e:
            latency_ms = (time.time() - start_time) * 1000
            self.usage_stats.record_query(is_write=True, latency_ms=latency_ms, success=False)
            self.logger.error(f"Update vertex failed: {e}")
            raise

    def delete_vertex(
        self,
        graph_name: str,
        collection: str,
        vertex_key: str,
    ) -> bool:
        """Delete a vertex from a graph."""
        start_time = time.time()
        try:
            graph = self._db.graph(graph_name)
            graph.vertex_collection(collection).delete(vertex_key)
            latency_ms = (time.time() - start_time) * 1000
            self.usage_stats.record_query(is_write=True, latency_ms=latency_ms, success=True)
            return True
        except Exception as e:
            latency_ms = (time.time() - start_time) * 1000
            self.usage_stats.record_query(is_write=True, latency_ms=latency_ms, success=False)
            return False

    # ---------------- Edge Operations ----------------
    def insert_edge(
        self,
        graph_name: str,
        edge_collection: str,
        edge: Dict[str, Any],
    ) -> Optional[Dict[str, Any]]:
        """Insert an edge in a graph."""
        start_time = time.time()
        try:
            graph = self._db.graph(graph_name)
            result = graph.edge_collection(edge_collection).insert(edge)
            latency_ms = (time.time() - start_time) * 1000
            self.usage_stats.record_query(is_write=True, latency_ms=latency_ms, success=True)
            return result
        except Exception as e:
            latency_ms = (time.time() - start_time) * 1000
            self.usage_stats.record_query(is_write=True, latency_ms=latency_ms, success=False)
            self.logger.error(f"Insert edge failed: {e}")
            raise

    def get_edge(
        self,
        graph_name: str,
        edge_collection: str,
        edge_key: str,
    ) -> Optional[Dict[str, Any]]:
        """Get an edge from a graph."""
        start_time = time.time()
        try:
            graph = self._db.graph(graph_name)
            result = graph.edge_collection(edge_collection).get(edge_key)
            latency_ms = (time.time() - start_time) * 1000
            self.usage_stats.record_query(is_write=False, latency_ms=latency_ms, success=True)
            return result
        except Exception as e:
            latency_ms = (time.time() - start_time) * 1000
            self.usage_stats.record_query(is_write=False, latency_ms=latency_ms, success=False)
            return None

    def get_edges(
        self,
        graph_name: str,
        edge_collection: str,
        vertex_key: str,
        direction: str = "any",
    ) -> List[Dict[str, Any]]:
        """Get edges connected to a vertex."""
        start_time = time.time()
        try:
            graph = self._db.graph(graph_name)
            result = list(graph.edge_collection(edge_collection).edges(
                vertex_key,
                direction=direction,
            ))
            latency_ms = (time.time() - start_time) * 1000
            self.usage_stats.record_query(is_write=False, latency_ms=latency_ms, success=True)
            return result
        except Exception as e:
            latency_ms = (time.time() - start_time) * 1000
            self.usage_stats.record_query(is_write=False, latency_ms=latency_ms, success=False)
            return []

    def delete_edge(
        self,
        graph_name: str,
        edge_collection: str,
        edge_key: str,
    ) -> bool:
        """Delete an edge from a graph."""
        start_time = time.time()
        try:
            graph = self._db.graph(graph_name)
            graph.edge_collection(edge_collection).delete(edge_key)
            latency_ms = (time.time() - start_time) * 1000
            self.usage_stats.record_query(is_write=True, latency_ms=latency_ms, success=True)
            return True
        except Exception as e:
            latency_ms = (time.time() - start_time) * 1000
            self.usage_stats.record_query(is_write=True, latency_ms=latency_ms, success=False)
            return False

    # ---------------- Graph Traversal ----------------
    def traverse(
        self,
        graph_name: str,
        start_vertex: str,
        direction: str = "any",
        max_depth: int = 3,
        edge_collection: Optional[str] = None,
    ) -> List[Dict[str, Any]]:
        """Traverse the graph from a starting vertex."""
        query = """
        FOR vertex, edge, path IN @minDepth..@maxDepth @direction @start
        @filter
        RETURN path
        """

        direction_map = {
            "outbound": "OUTBOUND",
            "inbound": "INBOUND",
            "any": "ANY",
        }
        dir_str = direction_map.get(direction, "ANY")

        if edge_collection:
            filter_str = f"FILTER edge.collection == '{edge_collection}'"
        else:
            filter_str = ""

        query = query.replace("@direction", dir_str)
        query = query.replace("@start", f"'{start_vertex}'")
        query = query.replace("@minDepth", "1")
        query = query.replace("@maxDepth", str(max_depth))
        query = query.replace("@filter", filter_str)

        return self.execute_aql(query)

    # ---------------- ArangoSearch Views ----------------
    def create_view(
        self,
        name: str,
        view_type: str = "arangosearch",
    ) -> bool:
        """Create an ArangoSearch view."""
        try:
            self._db.create_view(name, view_type=view_type)
            return True
        except Exception as e:
            self.logger.error(f"Failed to create view: {e}")
            return False

    def list_views(self) -> List[str]:
        """List all views."""
        return [view["name"] for view in self._db.views()]

    def search(
        self,
        view_name: str,
        search_term: str,
        limit: int = 10,
    ) -> List[Dict[str, Any]]:
        """Search using ArangoSearch."""
        query = f"""
        FOR doc IN {view_name}
        SEARCH PHRASE(doc, @searchTerm)
        LIMIT {limit}
        RETURN doc
        """
        bind_vars = {"searchTerm": search_term}
        return self.execute_aql(query, bind_vars=bind_vars)

    # ---------------- Index Operations ----------------
    def create_index(
        self,
        collection: str,
        index_type: str,
        fields: List[str],
        unique: bool = False,
    ) -> Optional[Dict[str, Any]]:
        """Create an index on a collection."""
        try:
            collection_obj = self._db.collection(collection)
            if index_type == "hash":
                return collection_obj.add_hash_index(fields, unique=unique)
            elif index_type == "skiplist":
                return collection_obj.add_skiplist_index(fields, unique=unique)
            elif index_type == "persistent":
                return collection_obj.add_persistent_index(fields, unique=unique)
            elif index_type == "ttl":
                return collection_obj.add_ttl_index(fields)
            elif index_type == "geo":
                return collection_obj.add_geo_index(fields)
            return None
        except Exception as e:
            self.logger.error(f"Failed to create index: {e}")
            return None

    def list_indexes(self, collection: str) -> List[Dict[str, Any]]:
        """List all indexes on a collection."""
        try:
            return self._db.collection(collection).indexes()
        except Exception:
            return []

    # ---------------- Utility Methods ----------------
    def truncate_collection(self, collection: str) -> bool:
        """Truncate a collection (delete all documents)."""
        try:
            self._db.collection(collection).truncate()
            return True
        except Exception as e:
            self.logger.error(f"Failed to truncate collection: {e}")
            return False

    def explain_aql(self, query: str) -> Dict[str, Any]:
        """Explain an AQL query."""
        return self._db.aql.explain(query)

    def validate_aql(self, query: str) -> bool:
        """Validate an AQL query without executing."""
        try:
            self._db.aql.validate(query)
            return True
        except AQLValidationError:
            return False

    # ---------------- Introspection & utilities ----------------
    def get_usage_stats(self) -> Dict[str, Any]:
        return {
            "total_queries": self.usage_stats.total_queries,
            "read_queries": self.usage_stats.read_queries,
            "write_queries": self.usage_stats.write_queries,
            "failed_queries": self.usage_stats.failed_queries,
            "avg_latency_ms": round(self.usage_stats.get_avg_latency(), 2),
            "cache_size": len(self._cache),
        }

    def clear_cache(self):
        """Clear the cache."""
        self._cache.clear()
        self.logger.info("Cache cleared")

    def reset_usage_stats(self):
        """Reset usage statistics."""
        self.usage_stats = UsageStats()
        self.logger.info("Usage statistics reset")

    def __enter__(self):
        return self.connect()

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.close()
        stats = self.get_usage_stats()
        self.logger.info(f"Session complete. Usage: {stats}")


# Convenience functions
def create_document(**kwargs) -> Dict[str, Any]:
    """Create a document dictionary."""
    return kwargs


def create_edge(from_key: str, to_key: str, **kwargs) -> Dict[str, Any]:
    """Create an edge dictionary."""
    edge = {"_from": from_key, "_to": to_key}
    edge.update(kwargs)
    return edge
