//! Python bindings for SocialGraphV2.
//!
//! Provides `SocialGraphV2` class wrapping `SocialStoreV2` with:
//! - Person CRUD (T25.2)
//! - Friend/Relation management (T25.3)
//! - Tag management (T25.4)
//! - Graph export & stats (T25.5)
//! - Generic entity API (T25.6)
//! - WAL persistence & metrics (T25.7)

use std::collections::BTreeMap;

use pyo3::prelude::*;
use pyo3::types::PyDict;
use pyo3::exceptions::{PyValueError, PyOSError, PyRuntimeError};

use aifastdb_core::social::store_v2::{SocialStoreV2, SocialStoreV2Config, VectorSearchConfig};
use aifastdb_core::social::{
    PersonInput, PersonFilter, FriendConfig, TagInput, TagFilter,
    ExportGraphOptions,
    ItemInput, AttributeInput, PathOptions, SuggestionConfig,
    PathFinder, NetworkAnalyzer, ItemMatcher, AttributeMatcher,
    BirthdayMatcher, FamilyTreeMatcher,
};

use crate::types::{json_value_to_py, py_to_json_value};

// ============================================================================
// Helper: PyDict → serde_json::Value
// ============================================================================

fn pydict_to_json(dict: &Bound<'_, PyDict>) -> PyResult<serde_json::Value> {
    let mut map = serde_json::Map::new();
    for (key, val) in dict.iter() {
        let key_str: String = key.extract()?;
        map.insert(key_str, py_to_json_value(&val)?);
    }
    Ok(serde_json::Value::Object(map))
}

/// Convert serde_json::Value to PyObject (convenience wrapper).
fn json_to_py(py: Python<'_>, val: &serde_json::Value) -> PyResult<PyObject> {
    json_value_to_py(py, val)
}

/// Convert Entity (via serde) to Python dict.
fn entity_to_py(py: Python<'_>, val: serde_json::Value) -> PyResult<PyObject> {
    json_to_py(py, &val)
}

// ============================================================================
// SocialGraphV2 class
// ============================================================================

/// SocialGraphV2 — Python interface for the social graph engine.
///
/// Usage:
///     with SocialGraphV2("/path/to/data") as sg:
///         person = sg.add_person({"name": "Alice"})
///         sg.add_friend(person["id"], other_id)
///         friends = sg.get_friends(person["id"])
#[pyclass(name = "SocialGraphV2")]
pub struct PySocialGraphV2 {
    store: SocialStoreV2,
    path: String,
}

#[pymethods]
impl PySocialGraphV2 {
    // ========================================================================
    // T25.1: Constructor, recover, shutdown, context manager
    // ========================================================================

    /// Create or open a SocialGraphV2 instance.
    ///
    /// Args:
    ///     path: Path to the data directory.
    ///     shard_count: Number of shards (0 = auto). Default: 0.
    ///     wal_enabled: Enable write-ahead logging. Default: True.
    ///     vector_dimension: Vector dimension for enabling vector search (0 = disabled). Default: 0.
    ///     vector_m: HNSW max connections per node (default: 16).
    ///     vector_ef_construction: HNSW construction quality (default: 200).
    ///     vector_ef_search: HNSW search quality (default: 50).
    ///     vector_max_elements: Max vector capacity (default: 100000).
    ///     vector_shard_count: HNSW shard count (default: 4).
    #[new]
    #[pyo3(signature = (path, *, shard_count=0, wal_enabled=true, vector_dimension=0, vector_m=16, vector_ef_construction=200, vector_ef_search=50, vector_max_elements=100000, vector_shard_count=4))]
    fn new(
        path: String,
        shard_count: usize,
        wal_enabled: bool,
        vector_dimension: usize,
        vector_m: usize,
        vector_ef_construction: usize,
        vector_ef_search: usize,
        vector_max_elements: usize,
        vector_shard_count: usize,
    ) -> PyResult<Self> {
        let mut config = SocialStoreV2Config::new(&path);
        if shard_count > 0 {
            config = config.with_shard_count(shard_count);
        }
        config = config.with_wal(wal_enabled);
        
        // Enable vector search if dimension > 0
        if vector_dimension > 0 {
            let vs_config = VectorSearchConfig::new(vector_dimension)
                .with_m(vector_m)
                .with_ef_construction(vector_ef_construction)
                .with_ef_search(vector_ef_search)
                .with_max_elements(vector_max_elements)
                .with_shards(vector_shard_count);
            config = config.with_vector_search(vs_config);
        }

        let store = SocialStoreV2::new(config);
        Ok(Self { store, path })
    }

    /// Recover data from WAL (call after creation to restore persisted state).
    fn recover(&self) -> PyResult<()> {
        self.store.recover()
            .map_err(|e| PyOSError::new_err(format!("Recovery failed: {}", e)))
    }

    /// Shutdown the store gracefully (flushes WAL).
    fn shutdown(&self) -> PyResult<()> {
        self.store.shutdown()
            .map_err(|e| PyOSError::new_err(format!("Shutdown failed: {}", e)))
    }

    /// Context manager entry — returns self.
    fn __enter__(slf: Py<Self>) -> Py<Self> {
        slf
    }

    /// Context manager exit — flush + shutdown.
    #[pyo3(signature = (_exc_type=None, _exc_val=None, _exc_tb=None))]
    fn __exit__(
        &self,
        _exc_type: Option<&Bound<'_, PyAny>>,
        _exc_val: Option<&Bound<'_, PyAny>>,
        _exc_tb: Option<&Bound<'_, PyAny>>,
    ) -> PyResult<bool> {
        let _ = self.store.flush();
        let _ = self.store.shutdown();
        Ok(false)
    }

    /// Get the data path.
    #[getter]
    fn path(&self) -> &str {
        &self.path
    }

    fn __repr__(&self) -> String {
        format!(
            "SocialGraphV2(path='{}', entities={}, relations={})",
            self.path,
            self.store.entity_count(),
            self.store.relation_count()
        )
    }

    // ========================================================================
    // T25.2: Person CRUD
    // ========================================================================

    /// Add a new person.
    ///
    /// Args:
    ///     input: dict with "name" (required), optional "nickname", "email", "phone",
    ///            "avatar", "gender", "birthday", "notes", "properties".
    ///
    /// Returns:
    ///     dict representing the created person entity.
    fn add_person(&self, py: Python<'_>, input: &Bound<'_, PyDict>) -> PyResult<PyObject> {
        let json_input = pydict_to_json(input)?;
        let person_input: PersonInput = serde_json::from_value(json_input)
            .map_err(|e| PyValueError::new_err(format!("Invalid person input: {}", e)))?;

        let entity = self.store.add_person(person_input);
        let val = serde_json::to_value(&entity)
            .map_err(|e| PyRuntimeError::new_err(format!("Serialization error: {}", e)))?;
        entity_to_py(py, val)
    }

    /// Get a person by ID.
    ///
    /// Returns dict or None if not found.
    fn get_person(&self, py: Python<'_>, id: &str) -> PyResult<Option<PyObject>> {
        match self.store.get_person(id) {
            Some(entity) => {
                let val = serde_json::to_value(&entity)
                    .map_err(|e| PyRuntimeError::new_err(format!("Serialization error: {}", e)))?;
                Ok(Some(entity_to_py(py, val)?))
            }
            None => Ok(None),
        }
    }

    /// Update a person.
    ///
    /// Args:
    ///     id: Person ID.
    ///     updates: dict with fields to update (e.g. {"name": "New Name", "email": "..."}).
    ///
    /// Returns dict of updated person, or None if not found.
    fn update_person(&self, py: Python<'_>, id: &str, updates: &Bound<'_, PyDict>) -> PyResult<Option<PyObject>> {
        let json_updates = pydict_to_json(updates)?;
        let update_map: BTreeMap<String, serde_json::Value> = serde_json::from_value(json_updates)
            .map_err(|e| PyValueError::new_err(format!("Invalid updates: {}", e)))?;

        match self.store.update_person(id, update_map) {
            Some(entity) => {
                let val = serde_json::to_value(&entity)
                    .map_err(|e| PyRuntimeError::new_err(format!("Serialization error: {}", e)))?;
                Ok(Some(entity_to_py(py, val)?))
            }
            None => Ok(None),
        }
    }

    /// Delete a person by ID.
    ///
    /// Returns True if the person was found and deleted.
    fn delete_person(&self, id: &str) -> bool {
        self.store.delete_person(id)
    }

    /// List all persons with optional filter.
    ///
    /// Args:
    ///     filter: Optional dict with filter criteria:
    ///         - "name": partial name match (case-insensitive)
    ///         - "gender": gender filter
    ///         - "limit": max results
    ///
    /// Returns list of person dicts.
    #[pyo3(signature = (filter=None))]
    fn list_persons(&self, py: Python<'_>, filter: Option<&Bound<'_, PyDict>>) -> PyResult<Vec<PyObject>> {
        let person_filter: Option<PersonFilter> = match filter {
            Some(dict) => {
                let json = pydict_to_json(dict)?;
                serde_json::from_value(json).ok()
            }
            None => None,
        };

        let entities = self.store.list_persons(person_filter);
        let mut results = Vec::with_capacity(entities.len());
        for entity in entities {
            let val = serde_json::to_value(&entity)
                .map_err(|e| PyRuntimeError::new_err(format!("Serialization error: {}", e)))?;
            results.push(entity_to_py(py, val)?);
        }
        Ok(results)
    }

    // ========================================================================
    // T25.3: Friend / Relation Management
    // ========================================================================

    /// Add a friend relationship between two persons.
    ///
    /// Args:
    ///     person_a: First person ID.
    ///     person_b: Second person ID.
    ///     config: Optional dict with relationship config (e.g. {"relation_type": "colleague", "weight": 0.8}).
    ///
    /// Returns relation ID string, or None if either person doesn't exist.
    #[pyo3(signature = (person_a, person_b, config=None))]
    fn add_friend(&self, person_a: &str, person_b: &str, config: Option<&Bound<'_, PyDict>>) -> PyResult<Option<String>> {
        let friend_config: FriendConfig = match config {
            Some(dict) => {
                let json = pydict_to_json(dict)?;
                serde_json::from_value(json).unwrap_or_default()
            }
            None => FriendConfig::default(),
        };

        Ok(self.store.add_friend(person_a, person_b, friend_config)
            .map(|r| r.id))
    }

    /// Remove a friend relationship.
    ///
    /// Returns True if the relationship was found and removed.
    fn remove_friend(&self, person_a: &str, person_b: &str) -> bool {
        self.store.remove_friend(person_a, person_b)
    }

    /// Get all friends of a person.
    ///
    /// Returns list of person entity dicts.
    fn get_friends(&self, py: Python<'_>, person_id: &str) -> PyResult<Vec<PyObject>> {
        let friends = self.store.get_friends(person_id);
        let mut results = Vec::with_capacity(friends.len());
        for entity in friends {
            let val = serde_json::to_value(&entity)
                .map_err(|e| PyRuntimeError::new_err(format!("Serialization error: {}", e)))?;
            results.push(entity_to_py(py, val)?);
        }
        Ok(results)
    }

    /// Check if two persons are friends.
    fn are_friends(&self, person_a: &str, person_b: &str) -> bool {
        self.store.are_friends(person_a, person_b)
    }

    /// Get the direct relation between two persons.
    ///
    /// Returns relation dict, or None if no direct relation exists.
    fn get_direct_relation(&self, py: Python<'_>, person_a: &str, person_b: &str) -> PyResult<Option<PyObject>> {
        match self.store.get_direct_relation(person_a, person_b) {
            Some(relation) => {
                let val = serde_json::to_value(&relation)
                    .map_err(|e| PyRuntimeError::new_err(format!("Serialization error: {}", e)))?;
                Ok(Some(json_to_py(py, &val)?))
            }
            None => Ok(None),
        }
    }

    // ========================================================================
    // T25.4: Tag Management
    // ========================================================================

    /// Add a tag.
    ///
    /// Args:
    ///     input: dict with "name" (required), optional "tag_type", "properties".
    ///
    /// Returns dict representing the created tag entity.
    fn add_tag(&self, py: Python<'_>, input: &Bound<'_, PyDict>) -> PyResult<PyObject> {
        let json_input = pydict_to_json(input)?;
        let tag_input: TagInput = serde_json::from_value(json_input)
            .map_err(|e| PyValueError::new_err(format!("Invalid tag input: {}", e)))?;

        let entity = self.store.add_tag(tag_input);
        let val = serde_json::to_value(&entity)
            .map_err(|e| PyRuntimeError::new_err(format!("Serialization error: {}", e)))?;
        entity_to_py(py, val)
    }

    /// Tag a person (assign a tag to a person).
    ///
    /// Returns relation ID string, or None if person/tag not found.
    fn tag_person(&self, person_id: &str, tag_id: &str) -> Option<String> {
        self.store.tag_person(person_id, tag_id).map(|r| r.id)
    }

    /// Remove a tag from a person.
    ///
    /// Returns True if the tag assignment was found and removed.
    fn untag_person(&self, person_id: &str, tag_id: &str) -> bool {
        self.store.untag_person(person_id, tag_id)
    }

    /// Get all tags assigned to a person.
    ///
    /// Returns list of tag entity dicts.
    fn get_person_tags(&self, py: Python<'_>, person_id: &str) -> PyResult<Vec<PyObject>> {
        let tags = self.store.get_person_tags(person_id);
        let mut results = Vec::with_capacity(tags.len());
        for entity in tags {
            let val = serde_json::to_value(&entity)
                .map_err(|e| PyRuntimeError::new_err(format!("Serialization error: {}", e)))?;
            results.push(entity_to_py(py, val)?);
        }
        Ok(results)
    }

    /// List all tags with optional filter.
    ///
    /// Args:
    ///     filter: Optional dict with filter criteria:
    ///         - "tag_type": filter by tag type
    ///         - "name": partial name match
    ///         - "limit": max results
    ///
    /// Returns list of tag entity dicts.
    #[pyo3(signature = (filter=None))]
    fn list_tags(&self, py: Python<'_>, filter: Option<&Bound<'_, PyDict>>) -> PyResult<Vec<PyObject>> {
        let tag_filter: Option<TagFilter> = match filter {
            Some(dict) => {
                let json = pydict_to_json(dict)?;
                serde_json::from_value(json).ok()
            }
            None => None,
        };

        let tags = self.store.list_tags(tag_filter);
        let mut results = Vec::with_capacity(tags.len());
        for entity in tags {
            let val = serde_json::to_value(&entity)
                .map_err(|e| PyRuntimeError::new_err(format!("Serialization error: {}", e)))?;
            results.push(entity_to_py(py, val)?);
        }
        Ok(results)
    }

    /// Get a tag by ID.
    ///
    /// Returns tag dict or None.
    fn get_tag(&self, py: Python<'_>, id: &str) -> PyResult<Option<PyObject>> {
        match self.store.get_tag(id) {
            Some(entity) => {
                let val = serde_json::to_value(&entity)
                    .map_err(|e| PyRuntimeError::new_err(format!("Serialization error: {}", e)))?;
                Ok(Some(entity_to_py(py, val)?))
            }
            None => Ok(None),
        }
    }

    // ========================================================================
    // T25.5: Graph Export
    // ========================================================================

    /// Export the social graph for visualization.
    ///
    /// Args:
    ///     options: Optional dict with export options:
    ///         - "max_nodes": max nodes to return (default: 500)
    ///         - "max_edges": max edges to return
    ///         - "include_tags": include tag nodes (default: true)
    ///         - "include_companies": include company nodes (default: true)
    ///
    /// Returns dict with "nodes", "edges", "stats".
    #[pyo3(signature = (options=None))]
    fn export_graph(&self, py: Python<'_>, options: Option<&Bound<'_, PyDict>>) -> PyResult<PyObject> {
        let export_options: Option<ExportGraphOptions> = match options {
            Some(dict) => {
                let json = pydict_to_json(dict)?;
                serde_json::from_value(json).ok()
            }
            None => None,
        };

        let graph = self.store.export_graph(export_options);
        let val = serde_json::to_value(&graph)
            .map_err(|e| PyRuntimeError::new_err(format!("Serialization error: {}", e)))?;
        json_to_py(py, &val)
    }

    /// Get graph statistics (entity/relation counts) without exporting full data.
    ///
    /// Returns dict with "persons", "companies", "tags", "relations".
    fn get_graph_stats(&self, py: Python<'_>) -> PyResult<PyObject> {
        let graph = self.store.export_graph(Some(ExportGraphOptions {
            max_nodes: Some(0),
            max_edges: Some(0),
            ..Default::default()
        }));

        let stats = serde_json::json!({
            "persons": graph.stats.persons,
            "companies": graph.stats.companies,
            "tags": graph.stats.tags,
            "relations": self.store.relation_count()
        });

        json_to_py(py, &stats)
    }

    // ========================================================================
    // T25.6: Generic Entity API
    // ========================================================================

    /// Add a generic entity of any type.
    ///
    /// Args:
    ///     name: Entity name.
    ///     entity_type: Type string (e.g. "article", "category", "product").
    ///     properties: Optional dict of properties.
    ///
    /// Returns dict of the created entity.
    #[pyo3(signature = (name, entity_type, properties=None))]
    fn add_entity(
        &self,
        py: Python<'_>,
        name: &str,
        entity_type: &str,
        properties: Option<&Bound<'_, PyDict>>,
    ) -> PyResult<PyObject> {
        use aifastdb_core::types::Entity;
        use chrono::Utc;

        let now = Utc::now().timestamp_millis();
        let mut entity = Entity::new(name, entity_type);
        entity.created_at = now;
        entity.updated_at = now;

        if let Some(props) = properties {
            for (key, val) in props.iter() {
                let key_str: String = key.extract()?;
                let json_val = py_to_json_value(&val)?;
                entity = entity.with_property(key_str, json_val);
            }
        }

        let result = serde_json::to_value(&entity)
            .map_err(|e| PyRuntimeError::new_err(format!("Serialization error: {}", e)))?;

        self.store.put_entity(entity);
        entity_to_py(py, result)
    }

    /// Update an entity by ID (works for any entity type).
    ///
    /// Args:
    ///     id: Entity ID.
    ///     updates: dict with fields to update. "name" updates the name,
    ///              "properties" merges into existing properties,
    ///              other keys are treated as properties.
    ///
    /// Returns updated entity dict, or None if not found.
    fn update_entity(&self, py: Python<'_>, id: &str, updates: &Bound<'_, PyDict>) -> PyResult<Option<PyObject>> {
        use aifastdb_core::types::Entity;
        use chrono::Utc;

        let existing = match self.store.router().get_entity(id) {
            Some(e) => e,
            None => return Ok(None),
        };

        let mut entity = Entity::new(&existing.name, &existing.entity_type)
            .with_id(id.to_string());

        // Copy existing properties
        for (key, value) in existing.properties.iter() {
            entity = entity.with_property(key.to_string(), value.clone());
        }

        // Apply updates from dict
        let json_updates = pydict_to_json(updates)?;
        if let Some(obj) = json_updates.as_object() {
            if let Some(new_name) = obj.get("name").and_then(|v| v.as_str()) {
                entity.name = new_name.to_string();
            }
            if let Some(props) = obj.get("properties").and_then(|v| v.as_object()) {
                for (key, value) in props.iter() {
                    entity = entity.with_property(key.to_string(), value.clone());
                }
            } else {
                for (key, value) in obj.iter() {
                    if key != "name" {
                        entity = entity.with_property(key.to_string(), value.clone());
                    }
                }
            }
        }

        entity.updated_at = Utc::now().timestamp_millis();
        self.store.put_entity(entity.clone());

        let val = serde_json::to_value(&entity)
            .map_err(|e| PyRuntimeError::new_err(format!("Serialization error: {}", e)))?;
        Ok(Some(entity_to_py(py, val)?))
    }

    /// Delete an entity by ID (works for any entity type).
    ///
    /// Returns True if found and deleted.
    fn delete_entity(&self, id: &str) -> bool {
        self.store.delete_entity(id)
    }

    /// List entities by type.
    ///
    /// Args:
    ///     entity_type: Type string (e.g. "person", "tag", "article").
    ///
    /// Returns list of entity dicts.
    fn list_entities_by_type(&self, py: Python<'_>, entity_type: &str) -> PyResult<Vec<PyObject>> {
        let entities = self.store.list_entities_by_type(entity_type);
        let mut results = Vec::with_capacity(entities.len());
        for entity in entities {
            let val = serde_json::to_value(&entity)
                .map_err(|e| PyRuntimeError::new_err(format!("Serialization error: {}", e)))?;
            results.push(entity_to_py(py, val)?);
        }
        Ok(results)
    }

    /// Get all registered entity types.
    ///
    /// Returns list of type strings (e.g. ["person", "tag", "article"]).
    fn get_entity_types(&self) -> Vec<String> {
        self.store.get_all_entity_types()
    }

    /// Check if an entity exists.
    fn contains_entity(&self, id: &str) -> bool {
        self.store.contains_entity(id)
    }

    /// Get entity count.
    fn entity_count(&self) -> usize {
        self.store.entity_count()
    }

    /// Get relation count.
    fn relation_count(&self) -> usize {
        self.store.relation_count()
    }

    // ========================================================================
    // T25.7: WAL & Persistence & Metrics
    // ========================================================================

    /// Flush all pending writes to WAL.
    fn flush(&self) -> PyResult<()> {
        self.store.flush()
            .map_err(|e| PyOSError::new_err(format!("Flush failed: {}", e)))
    }

    /// Sync WAL to disk.
    fn sync(&self) -> PyResult<()> {
        self.store.sync()
            .map_err(|e| PyOSError::new_err(format!("Sync failed: {}", e)))
    }

    /// Check if WAL is enabled.
    fn is_wal_enabled(&self) -> bool {
        self.store.is_wal_enabled()
    }

    /// Get WAL diagnostics for troubleshooting.
    ///
    /// Returns dict with WAL path, shard info, orphaned directories, etc.
    fn get_wal_diagnostics(&self, py: Python<'_>) -> PyResult<PyObject> {
        let config = self.store.router().config();
        let wal_path = config.path.join("wal");
        let configured_shards = self.store.shard_count();
        let shard_names: Vec<String> = config.shard_names.clone().unwrap_or_default();

        let mut shards_info = Vec::new();
        let mut orphaned: Vec<String> = Vec::new();

        if let Ok(entries) = std::fs::read_dir(&wal_path) {
            for entry in entries.flatten() {
                if let Ok(ft) = entry.file_type() {
                    if ft.is_dir() {
                        let dir_name = entry.file_name().to_string_lossy().to_string();
                        if let Some(shard_id) = parse_shard_id(&dir_name) {
                            let is_orphaned = shard_id >= configured_shards;
                            if is_orphaned {
                                orphaned.push(dir_name.clone());
                            }
                            shards_info.push(serde_json::json!({
                                "directory": dir_name,
                                "shard_id": shard_id,
                                "is_orphaned": is_orphaned,
                            }));
                        }
                    }
                }
            }
        }

        shards_info.sort_by_key(|s| s["shard_id"].as_u64().unwrap_or(0));
        orphaned.sort();

        let diagnostics = serde_json::json!({
            "wal_path": wal_path.to_string_lossy(),
            "configured_shard_count": configured_shards,
            "actual_shard_count": shards_info.len(),
            "shard_names": shard_names,
            "shards": shards_info,
            "orphaned_directories": orphaned,
            "has_orphaned_shards": !orphaned.is_empty(),
        });

        json_to_py(py, &diagnostics)
    }

    /// Get performance metrics.
    ///
    /// Returns dict with request counts, timing, entity/relation counts, etc.
    fn get_metrics(&self, py: Python<'_>) -> PyResult<PyObject> {
        let summary = self.store.performance_summary();
        let metrics = serde_json::json!({
            "total_requests": summary.total_requests,
            "successful_writes": summary.successful_writes,
            "failed_writes": summary.failed_writes,
            "rejected_requests": summary.rejected_requests,
            "batches_flushed": summary.batches_flushed,
            "avg_queue_time_us": summary.avg_queue_time_us,
            "avg_write_time_us": summary.avg_write_time_us,
            "entity_count": summary.entity_count,
            "relation_count": summary.relation_count,
            "cross_shard_count": summary.cross_shard_count,
            "shard_count": summary.shard_count,
            "queue_fill_ratio": summary.queue_fill_ratio,
            "success_rate": summary.success_rate(),
        });

        json_to_py(py, &metrics)
    }

    /// Get shard count.
    fn shard_count(&self) -> usize {
        self.store.shard_count()
    }

    // ========================================================================
    // T26.1: Path Finding
    // ========================================================================

    /// Find the shortest path between two persons.
    ///
    /// Args:
    ///     from_id: Source person ID.
    ///     to_id: Target person ID.
    ///     options: Optional dict with path options:
    ///         - "max_hops": Maximum number of hops (default 6).
    ///         - "relation_types": List of relation types to follow.
    ///         - "bidirectional_search": Use bidirectional BFS (default false).
    ///
    /// Returns dict with "path" (list of entities), "hops", "intermediaries", "relation_chain",
    /// or None if no path found.
    #[pyo3(signature = (from_id, to_id, options=None))]
    fn find_path(
        &self,
        py: Python<'_>,
        from_id: &str,
        to_id: &str,
        options: Option<&Bound<'_, PyDict>>,
    ) -> PyResult<Option<PyObject>> {
        let path_options: Option<PathOptions> = match options {
            Some(dict) => {
                let json = pydict_to_json(dict)?;
                serde_json::from_value(json).ok()
            }
            None => None,
        };

        let finder = PathFinder::new(&self.store);
        match finder.find_shortest_path(from_id, to_id, path_options) {
            Some(result) => {
                let val = serde_json::to_value(&result)
                    .map_err(|e| PyRuntimeError::new_err(format!("Serialization error: {}", e)))?;
                Ok(Some(json_to_py(py, &val)?))
            }
            None => Ok(None),
        }
    }

    /// Find all paths between two persons.
    ///
    /// Args:
    ///     from_id: Source person ID.
    ///     to_id: Target person ID.
    ///     max_hops: Maximum hops per path (default 6).
    ///     max_paths: Maximum number of paths to find (default 10).
    ///
    /// Returns dict with "paths" (list of path results) and "shortest_hops".
    #[pyo3(signature = (from_id, to_id, max_hops=6, max_paths=10))]
    fn find_all_paths(
        &self,
        py: Python<'_>,
        from_id: &str,
        to_id: &str,
        max_hops: usize,
        max_paths: usize,
    ) -> PyResult<PyObject> {
        let finder = PathFinder::new(&self.store);
        let result = finder.find_all_paths(from_id, to_id, max_hops, max_paths);
        let val = serde_json::to_value(&result)
            .map_err(|e| PyRuntimeError::new_err(format!("Serialization error: {}", e)))?;
        json_to_py(py, &val)
    }

    /// Find all intermediaries between two persons.
    ///
    /// Args:
    ///     person_a: First person ID.
    ///     person_b: Second person ID.
    ///     max_hops: Maximum hops (default 6).
    ///
    /// Returns list of person entity dicts who are intermediaries.
    #[pyo3(signature = (person_a, person_b, max_hops=6))]
    fn find_intermediaries(
        &self,
        py: Python<'_>,
        person_a: &str,
        person_b: &str,
        max_hops: usize,
    ) -> PyResult<Vec<PyObject>> {
        let finder = PathFinder::new(&self.store);
        let entities = finder.find_intermediaries(person_a, person_b, max_hops);
        entities_to_py_vec(py, &entities)
    }

    // ========================================================================
    // T26.2: Network Analysis
    // ========================================================================

    /// Get mutual friends between two persons.
    ///
    /// Returns list of person entity dicts.
    fn get_mutual_friends(&self, py: Python<'_>, person_a: &str, person_b: &str) -> PyResult<Vec<PyObject>> {
        let analyzer = NetworkAnalyzer::new(&self.store);
        let entities = analyzer.get_mutual_friends(person_a, person_b);
        entities_to_py_vec(py, &entities)
    }

    /// Calculate relationship strength between two persons.
    ///
    /// Returns dict with "direct_strength", "mutual_friends_count",
    /// "mutual_tags_count", "mutual_items_count", "mutual_attributes_count",
    /// "shortest_path_length", "overall_score".
    fn calculate_relationship_strength(
        &self,
        py: Python<'_>,
        person_a: &str,
        person_b: &str,
    ) -> PyResult<PyObject> {
        let analyzer = NetworkAnalyzer::new(&self.store);
        let strength = analyzer.calculate_relationship_strength(person_a, person_b);
        let val = serde_json::to_value(&strength)
            .map_err(|e| PyRuntimeError::new_err(format!("Serialization error: {}", e)))?;
        json_to_py(py, &val)
    }

    /// Suggest connections for a person.
    ///
    /// Args:
    ///     person_id: Person ID.
    ///     config: Optional dict with suggestion config.
    ///
    /// Returns list of suggestion dicts with "person", "score", "reasons", "via".
    #[pyo3(signature = (person_id, config=None))]
    fn suggest_connections(
        &self,
        py: Python<'_>,
        person_id: &str,
        config: Option<&Bound<'_, PyDict>>,
    ) -> PyResult<Vec<PyObject>> {
        let suggestion_config: Option<SuggestionConfig> = match config {
            Some(dict) => {
                let json = pydict_to_json(dict)?;
                serde_json::from_value(json).ok()
            }
            None => None,
        };

        let analyzer = NetworkAnalyzer::new(&self.store);
        let suggestions = analyzer.suggest_connections(person_id, suggestion_config);
        let mut results = Vec::with_capacity(suggestions.len());
        for suggestion in suggestions {
            let val = serde_json::to_value(&suggestion)
                .map_err(|e| PyRuntimeError::new_err(format!("Serialization error: {}", e)))?;
            results.push(json_to_py(py, &val)?);
        }
        Ok(results)
    }

    // ========================================================================
    // T26.3: N-Degree Contacts & Network Stats
    // ========================================================================

    /// Get contacts within N degrees of separation.
    ///
    /// Args:
    ///     person_id: Person ID.
    ///     max_degree: Maximum degree (1-3, default 3).
    ///
    /// Returns dict with "degree1", "degree2", "degree3" (lists of person dicts).
    #[pyo3(signature = (person_id, max_degree=3))]
    fn get_nth_degree_contacts(
        &self,
        py: Python<'_>,
        person_id: &str,
        max_degree: usize,
    ) -> PyResult<PyObject> {
        let analyzer = NetworkAnalyzer::new(&self.store);
        let contacts = analyzer.get_nth_degree_contacts(person_id, max_degree);
        let val = serde_json::to_value(&contacts)
            .map_err(|e| PyRuntimeError::new_err(format!("Serialization error: {}", e)))?;
        json_to_py(py, &val)
    }

    /// Get network statistics for a person.
    ///
    /// Returns dict with "total_friends", "total_tags", "total_items",
    /// "total_attributes", "network_reach_2hop", "network_reach_3hop",
    /// "avg_path_length", "clustering_coefficient".
    fn get_network_stats(&self, py: Python<'_>, person_id: &str) -> PyResult<PyObject> {
        let analyzer = NetworkAnalyzer::new(&self.store);
        let stats = analyzer.get_network_stats(person_id);
        let val = serde_json::to_value(&stats)
            .map_err(|e| PyRuntimeError::new_err(format!("Serialization error: {}", e)))?;
        json_to_py(py, &val)
    }

    // ========================================================================
    // T26.4: Item Management
    // ========================================================================

    /// Add a new item.
    ///
    /// Args:
    ///     input: dict with "name" (required), "category" (required),
    ///            optional "brand", "model", "properties".
    ///
    /// Returns dict representing the created item entity.
    fn add_item(&self, py: Python<'_>, input: &Bound<'_, PyDict>) -> PyResult<PyObject> {
        let json_input = pydict_to_json(input)?;
        let item_input: ItemInput = serde_json::from_value(json_input)
            .map_err(|e| PyValueError::new_err(format!("Invalid item input: {}", e)))?;

        let entity = self.store.add_item(item_input);
        let val = serde_json::to_value(&entity)
            .map_err(|e| PyRuntimeError::new_err(format!("Serialization error: {}", e)))?;
        entity_to_py(py, val)
    }

    /// Add an item to a person (ownership relation).
    ///
    /// Args:
    ///     person_id: Person ID.
    ///     item_id: Item ID.
    ///     relation_type: Optional relation type (default "owns").
    ///
    /// Returns relation ID string, or None if person/item not found.
    #[pyo3(signature = (person_id, item_id, relation_type=None))]
    fn add_item_to_person(
        &self,
        person_id: &str,
        item_id: &str,
        relation_type: Option<&str>,
    ) -> Option<String> {
        self.store.add_item_to_person(person_id, item_id, relation_type)
            .map(|r| r.id)
    }

    /// Remove item from person.
    ///
    /// Returns True if the relation was found and removed.
    fn remove_item_from_person(&self, person_id: &str, item_id: &str) -> bool {
        self.store.remove_item_from_person(person_id, item_id)
    }

    /// Get all items owned by a person.
    ///
    /// Returns list of item entity dicts.
    fn get_person_items(&self, py: Python<'_>, person_id: &str) -> PyResult<Vec<PyObject>> {
        let items = self.store.get_person_items(person_id);
        entities_to_py_vec(py, &items)
    }

    /// Find shared items between two persons.
    ///
    /// Returns list of item entity dicts.
    fn find_shared_items(&self, py: Python<'_>, person_a: &str, person_b: &str) -> PyResult<Vec<PyObject>> {
        let matcher = ItemMatcher::new(&self.store);
        let items = matcher.find_shared_items(person_a, person_b);
        entities_to_py_vec(py, &items)
    }

    /// Find people who share items with a given person.
    ///
    /// Args:
    ///     person_id: Person ID.
    ///     category: Optional item category filter.
    ///
    /// Returns list of match result dicts with "person", "shared_items", "match_score", "match_details".
    #[pyo3(signature = (person_id, category=None))]
    fn find_people_by_shared_items(
        &self,
        py: Python<'_>,
        person_id: &str,
        category: Option<&str>,
    ) -> PyResult<Vec<PyObject>> {
        let matcher = ItemMatcher::new(&self.store);
        let results = matcher.find_people_by_shared_items(person_id, category);
        let mut out = Vec::with_capacity(results.len());
        for result in results {
            let val = serde_json::to_value(&result)
                .map_err(|e| PyRuntimeError::new_err(format!("Serialization error: {}", e)))?;
            out.push(json_to_py(py, &val)?);
        }
        Ok(out)
    }

    /// List all items with optional category filter.
    ///
    /// Args:
    ///     category: Optional category string filter.
    ///
    /// Returns list of item entity dicts.
    #[pyo3(signature = (category=None))]
    fn list_items(&self, py: Python<'_>, category: Option<&str>) -> PyResult<Vec<PyObject>> {
        let items = self.store.list_items(category);
        entities_to_py_vec(py, &items)
    }

    // ========================================================================
    // T26.5: Attribute Management
    // ========================================================================

    /// Add a new attribute.
    ///
    /// Args:
    ///     input: dict with "category" (required), "value" (required),
    ///            optional "id", "properties".
    ///
    /// Returns dict representing the created attribute entity.
    fn add_attribute(&self, py: Python<'_>, input: &Bound<'_, PyDict>) -> PyResult<PyObject> {
        let json_input = pydict_to_json(input)?;
        let attr_input: AttributeInput = serde_json::from_value(json_input)
            .map_err(|e| PyValueError::new_err(format!("Invalid attribute input: {}", e)))?;

        let entity = self.store.add_attribute(attr_input);
        let val = serde_json::to_value(&entity)
            .map_err(|e| PyRuntimeError::new_err(format!("Serialization error: {}", e)))?;
        entity_to_py(py, val)
    }

    /// Find or create an attribute by category and value.
    ///
    /// Args:
    ///     category: Attribute category (e.g. "school", "city", "zodiac").
    ///     value: Attribute value (e.g. "Tsinghua University", "Beijing").
    ///
    /// Returns dict representing the attribute entity.
    fn find_or_create_attribute(
        &self,
        py: Python<'_>,
        category: &str,
        value: &str,
    ) -> PyResult<PyObject> {
        let entity = self.store.find_or_create_attribute(category, value);
        let val = serde_json::to_value(&entity)
            .map_err(|e| PyRuntimeError::new_err(format!("Serialization error: {}", e)))?;
        entity_to_py(py, val)
    }

    /// Set a person's attribute.
    ///
    /// Args:
    ///     person_id: Person ID.
    ///     attr_id: Attribute ID.
    ///     relation_type: Optional relation type (default "has_attribute").
    ///
    /// Returns relation ID string, or None if person/attribute not found.
    #[pyo3(signature = (person_id, attr_id, relation_type=None))]
    fn set_person_attribute(
        &self,
        person_id: &str,
        attr_id: &str,
        relation_type: Option<&str>,
    ) -> Option<String> {
        self.store.set_person_attribute(person_id, attr_id, relation_type)
            .map(|r| r.id)
    }

    /// Remove a person's attribute.
    ///
    /// Returns True if the attribute was found and removed.
    fn remove_person_attribute(&self, person_id: &str, attr_id: &str) -> bool {
        self.store.remove_person_attribute(person_id, attr_id)
    }

    /// Get all attributes of a person.
    ///
    /// Returns list of attribute entity dicts.
    fn get_person_attributes(&self, py: Python<'_>, person_id: &str) -> PyResult<Vec<PyObject>> {
        let attrs = self.store.get_person_attributes(person_id);
        entities_to_py_vec(py, &attrs)
    }

    /// List all attributes with optional category filter.
    ///
    /// Args:
    ///     category: Optional category string filter.
    ///
    /// Returns list of attribute entity dicts.
    #[pyo3(signature = (category=None))]
    fn list_attributes(&self, py: Python<'_>, category: Option<&str>) -> PyResult<Vec<PyObject>> {
        let attrs = self.store.list_attributes(category);
        entities_to_py_vec(py, &attrs)
    }

    /// Find alumni (people who went to the same school).
    ///
    /// Returns list of person entity dicts.
    fn find_alumni(&self, py: Python<'_>, person_id: &str) -> PyResult<Vec<PyObject>> {
        let matcher = AttributeMatcher::new(&self.store);
        let entities = matcher.find_alumni(person_id);
        entities_to_py_vec(py, &entities)
    }

    /// Find people in the same city.
    ///
    /// Returns list of person entity dicts.
    fn find_same_city(&self, py: Python<'_>, person_id: &str) -> PyResult<Vec<PyObject>> {
        let matcher = AttributeMatcher::new(&self.store);
        let entities = matcher.find_same_city(person_id);
        entities_to_py_vec(py, &entities)
    }

    /// Find shared attributes between two people.
    ///
    /// Returns list of shared attribute detail dicts.
    fn find_shared_attributes(
        &self,
        py: Python<'_>,
        person_a: &str,
        person_b: &str,
    ) -> PyResult<Vec<PyObject>> {
        let matcher = AttributeMatcher::new(&self.store);
        let shared = matcher.find_shared_attributes(person_a, person_b);
        let mut results = Vec::with_capacity(shared.len());
        for detail in shared {
            let val = serde_json::to_value(&detail)
                .map_err(|e| PyRuntimeError::new_err(format!("Serialization error: {}", e)))?;
            results.push(json_to_py(py, &val)?);
        }
        Ok(results)
    }

    // ========================================================================
    // T26.6: Birthday & Family Tree
    // ========================================================================

    /// Set a person's birthday.
    ///
    /// Args:
    ///     person_id: Person ID.
    ///     birthday: Birthday string in YYYY-MM-DD format.
    ///
    /// Returns True if set successfully, False if person not found or invalid format.
    fn set_birthday(&self, person_id: &str, birthday: &str) -> bool {
        self.store.set_birthday(person_id, birthday)
    }

    /// Get a person's birthday.
    ///
    /// Returns birthday string in YYYY-MM-DD format, or None.
    fn get_birthday(&self, person_id: &str) -> Option<String> {
        self.store.get_birthday(person_id)
    }

    /// Find people with upcoming birthdays within N days.
    ///
    /// Args:
    ///     days: Number of days to look ahead (default 30).
    ///
    /// Returns list of dicts with "person", "birthday", "match_type".
    #[pyo3(signature = (days=30))]
    fn find_upcoming_birthdays(&self, py: Python<'_>, days: u32) -> PyResult<Vec<PyObject>> {
        let matcher = BirthdayMatcher::new(&self.store);
        let results = matcher.find_upcoming_birthdays(days);
        let mut out = Vec::with_capacity(results.len());
        for result in results {
            let val = serde_json::to_value(&result)
                .map_err(|e| PyRuntimeError::new_err(format!("Serialization error: {}", e)))?;
            out.push(json_to_py(py, &val)?);
        }
        Ok(out)
    }

    /// Add a parent-child relationship.
    ///
    /// Args:
    ///     parent_id: Parent person ID.
    ///     child_id: Child person ID.
    ///
    /// Returns relation ID string, or None if person not found.
    fn add_parent_child(&self, parent_id: &str, child_id: &str) -> Option<String> {
        let tree = FamilyTreeMatcher::new(&self.store);
        tree.add_parent_child(parent_id, child_id).map(|r| r.id)
    }

    /// Get ancestors of a person.
    ///
    /// Args:
    ///     person_id: Person ID.
    ///     max_generations: Maximum generations to traverse (default 5).
    ///
    /// Returns list of ancestor entity dicts.
    #[pyo3(signature = (person_id, max_generations=5))]
    fn get_ancestors(
        &self,
        py: Python<'_>,
        person_id: &str,
        max_generations: usize,
    ) -> PyResult<Vec<PyObject>> {
        let tree = FamilyTreeMatcher::new(&self.store);
        let entities = tree.get_ancestors(person_id, max_generations);
        entities_to_py_vec(py, &entities)
    }

    /// Get descendants of a person.
    ///
    /// Args:
    ///     person_id: Person ID.
    ///     max_generations: Maximum generations to traverse (default 5).
    ///
    /// Returns list of descendant entity dicts.
    #[pyo3(signature = (person_id, max_generations=5))]
    fn get_descendants(
        &self,
        py: Python<'_>,
        person_id: &str,
        max_generations: usize,
    ) -> PyResult<Vec<PyObject>> {
        let tree = FamilyTreeMatcher::new(&self.store);
        let entities = tree.get_descendants(person_id, max_generations);
        entities_to_py_vec(py, &entities)
    }

    /// Get parents of a person.
    ///
    /// Returns list of parent entity dicts.
    fn get_parents(&self, py: Python<'_>, person_id: &str) -> PyResult<Vec<PyObject>> {
        let tree = FamilyTreeMatcher::new(&self.store);
        let entities = tree.get_parents(person_id);
        entities_to_py_vec(py, &entities)
    }

    /// Get children of a person.
    ///
    /// Returns list of child entity dicts.
    fn get_children(&self, py: Python<'_>, person_id: &str) -> PyResult<Vec<PyObject>> {
        let tree = FamilyTreeMatcher::new(&self.store);
        let entities = tree.get_children(person_id);
        entities_to_py_vec(py, &entities)
    }

    /// Add spouse relationship.
    ///
    /// Returns relation ID string, or None if person not found.
    fn add_spouse(&self, person_a: &str, person_b: &str) -> Option<String> {
        let tree = FamilyTreeMatcher::new(&self.store);
        tree.add_spouse(person_a, person_b).map(|r| r.id)
    }

    /// Add sibling relationship.
    ///
    /// Args:
    ///     person_a: First person ID.
    ///     person_b: Second person ID.
    ///     half_sibling: Whether this is a half-sibling relation (default False).
    ///
    /// Returns relation ID string, or None if person not found.
    #[pyo3(signature = (person_a, person_b, half_sibling=false))]
    fn add_sibling(&self, person_a: &str, person_b: &str, half_sibling: bool) -> Option<String> {
        let tree = FamilyTreeMatcher::new(&self.store);
        tree.add_sibling(person_a, person_b, half_sibling).map(|r| r.id)
    }

    // ========================================================================
    // T26.7: Compatibility Analysis
    // ========================================================================

    /// Analyze compatibility between two persons.
    ///
    /// Returns dict with "overall_score", "shared_attributes",
    /// "complementary_attributes", "analysis".
    fn analyze_compatibility(
        &self,
        py: Python<'_>,
        person_a: &str,
        person_b: &str,
    ) -> PyResult<PyObject> {
        let matcher = AttributeMatcher::new(&self.store);
        let result = matcher.analyze_compatibility(person_a, person_b);
        let val = serde_json::to_value(&result)
            .map_err(|e| PyRuntimeError::new_err(format!("Serialization error: {}", e)))?;
        json_to_py(py, &val)
    }

    // ========================================================================
    // T32.5-T32.7: Vector Search API
    // ========================================================================

    /// Check if vector search is enabled.
    ///
    /// Returns:
    ///     bool: True if vector search was configured at construction time.
    fn is_vector_search_enabled(&self) -> bool {
        self.store.is_vector_search_enabled()
    }

    /// Get the number of vectors currently indexed.
    ///
    /// Returns:
    ///     int: Number of indexed vectors (0 if vector search is disabled).
    fn vector_count(&self) -> usize {
        self.store.vector_count()
    }

    /// Index an entity with a vector embedding.
    ///
    /// The entity must already exist in the graph store.
    /// If the entity already has a vector, this is idempotent (no error).
    ///
    /// Args:
    ///     entity_id: The entity ID to index.
    ///     embedding: A list of floats representing the vector.
    ///
    /// Raises:
    ///     OSError: If vector search is not enabled or entity not found.
    fn index_entity(&self, entity_id: &str, embedding: Vec<f32>) -> PyResult<()> {
        self.store.index_entity(entity_id, embedding)
            .map_err(|e| PyOSError::new_err(format!("index_entity failed: {}", e)))
    }

    /// Remove an entity's vector from the search index.
    ///
    /// Does NOT remove the entity from the graph store, only from the vector index.
    ///
    /// Args:
    ///     entity_id: The entity ID whose vector to remove.
    ///
    /// Raises:
    ///     OSError: If vector search is not enabled.
    fn remove_entity_vector(&self, entity_id: &str) -> PyResult<()> {
        self.store.remove_entity_vector(entity_id)
            .map_err(|e| PyOSError::new_err(format!("remove_entity_vector failed: {}", e)))
    }

    /// Search entities by vector similarity (k-NN search).
    ///
    /// Args:
    ///     embedding: Query vector (list of floats).
    ///     k: Number of results to return.
    ///     entity_type_filter: Optional entity type filter (e.g. "person").
    ///
    /// Returns:
    ///     list[dict]: List of dicts with "entity_id", "score", "entity" keys,
    ///     sorted by similarity (highest first).
    ///
    /// Raises:
    ///     OSError: If vector search is not enabled.
    #[pyo3(signature = (embedding, k, entity_type_filter=None))]
    fn search_entities_by_vector(
        &self,
        py: Python<'_>,
        embedding: Vec<f32>,
        k: usize,
        entity_type_filter: Option<&str>,
    ) -> PyResult<Vec<PyObject>> {
        let results = self.store.search_entities_by_vector(
            &embedding,
            k,
            entity_type_filter,
        ).map_err(|e| PyOSError::new_err(format!("search_entities_by_vector failed: {}", e)))?;
        
        vector_results_to_py(py, results)
    }

    /// Hybrid search combining vector similarity and text attribute matching.
    ///
    /// Uses RRF (Reciprocal Rank Fusion) to fuse vector and text scores.
    /// Either embedding or text_query (or both) can be provided.
    ///
    /// Args:
    ///     embedding: Optional query vector (list of floats).
    ///     text_query: Optional text to match against entity attributes.
    ///     k: Number of results to return.
    ///     entity_type_filter: Optional entity type filter.
    ///
    /// Returns:
    ///     list[dict]: List of dicts with "entity_id", "score", "entity" keys.
    ///
    /// Raises:
    ///     OSError: If vector search is not enabled.
    #[pyo3(signature = (k, *, embedding=None, text_query=None, entity_type_filter=None))]
    fn hybrid_search(
        &self,
        py: Python<'_>,
        k: usize,
        embedding: Option<Vec<f32>>,
        text_query: Option<&str>,
        entity_type_filter: Option<&str>,
    ) -> PyResult<Vec<PyObject>> {
        let results = self.store.hybrid_search(
            embedding.as_deref(),
            text_query,
            entity_type_filter,
            k,
        ).map_err(|e| PyOSError::new_err(format!("hybrid_search failed: {}", e)))?;
        
        vector_results_to_py(py, results)
    }
}

/// Helper: Convert VectorEntitySearchResult to Python list of dicts.
fn vector_results_to_py(
    py: Python<'_>,
    results: Vec<aifastdb_core::social::store_v2::VectorEntitySearchResult>,
) -> PyResult<Vec<PyObject>> {
    use crate::types::json_value_to_py;
    
    let mut out = Vec::with_capacity(results.len());
    for r in results {
        let dict = pyo3::types::PyDict::new_bound(py);
        dict.set_item("entity_id", &r.entity_id)?;
        dict.set_item("score", r.score)?;
        let entity_val = serde_json::to_value(&r.entity)
            .map_err(|e| PyRuntimeError::new_err(format!("Serialization error: {}", e)))?;
        dict.set_item("entity", json_value_to_py(py, &entity_val)?)?;
        out.push(dict.into());
    }
    Ok(out)
}

/// Helper: Convert a Vec<Entity> to Vec<PyObject>.
fn entities_to_py_vec(py: Python<'_>, entities: &[aifastdb_core::types::Entity]) -> PyResult<Vec<PyObject>> {
    let mut results = Vec::with_capacity(entities.len());
    for entity in entities {
        let val = serde_json::to_value(entity)
            .map_err(|e| PyRuntimeError::new_err(format!("Serialization error: {}", e)))?;
        results.push(entity_to_py(py, val)?);
    }
    Ok(results)
}

/// Parse shard ID from directory name like "shard_0" or "shard_0_person".
fn parse_shard_id(dir_name: &str) -> Option<usize> {
    if !dir_name.starts_with("shard_") {
        return None;
    }
    let rest = &dir_name[6..];
    let num_end = rest.find('_').unwrap_or(rest.len());
    rest[..num_end].parse().ok()
}

/// Register SocialGraphV2 class into the Python module.
pub fn register_social_graph(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_class::<PySocialGraphV2>()?;
    Ok(())
}
