//! SocialStoreV2 - High-concurrency social store with API compatibility
//!
//! Wraps ConcurrentGraphStore (GraphShardRouter) to provide the same API
//! as SocialStore while supporting high-concurrency writes.
//!
//! # Features
//!
//! - **API Compatibility**: Same interface as SocialStore for easy migration
//! - **High Concurrency**: Backed by sharded concurrent graph store
//! - **Dual API**: Both sync and async interfaces available
//! - **Performance Monitoring**: Built-in metrics and monitoring
//!
//! # Example
//!
//! ```ignore
//! use aifastdb_core::social::store_v2::{SocialStoreV2, SocialStoreV2Config};
//!
//! #[tokio::main]
//! async fn main() {
//!     let config = SocialStoreV2Config::new("data/social");
//!     let store = SocialStoreV2::new(config);
//!     
//!     // Async API
//!     let person = store.add_person_async(PersonInput { ... }).await;
//!     
//!     // Sync API (for compatibility)
//!     let person = store.add_person(PersonInput { ... });
//! }
//! ```

use std::collections::{BTreeMap, HashMap};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use parking_lot::RwLock;

use crate::types::graph::{Entity, Relation, entity_types, relation_types};
use crate::types::HNSWConfig;
use crate::storage::concurrent::MetricsSnapshot;
use crate::vector::concurrent::{
    ConcurrentHNSWIndex, ConcurrentHNSWConfig,
};

use super::concurrent::{
    GraphShardRouter, ConcurrentGraphConfig, GraphStoreError,
    WriteMetrics, GlobalIndexStats,
};
use super::types::*;
use super::birthday_matcher::{BirthdayIndex, validate_birthday};
use super::vector_wal::VectorWAL;

/// Configuration for optional vector search capability on SocialStoreV2
///
/// When provided, SocialStoreV2 will internally embed a ConcurrentHNSWIndex
/// for semantic similarity search on entities. The entity.id is used as the
/// bridging key between the graph store and the vector index.
///
/// # Example
///
/// ```ignore
/// let config = SocialStoreV2Config::new("data/social")
///     .with_vector_search(VectorSearchConfig::new(384)); // MiniLM dimension
/// ```
#[derive(Debug, Clone)]
pub struct VectorSearchConfig {
    /// Vector dimension (must match embedding model output)
    pub dimension: usize,
    
    /// HNSW M parameter: max connections per node per layer (default: 16)
    pub m: usize,
    
    /// HNSW ef_construction parameter (default: 200)
    pub ef_construction: usize,
    
    /// HNSW ef_search parameter (default: 50)
    pub ef_search: usize,
    
    /// Maximum number of vectors (default: 100,000)
    pub max_elements: usize,
    
    /// Number of HNSW shards (0 = auto, 1 = single shard recommended for small datasets)
    pub shard_count: usize,
}

impl Default for VectorSearchConfig {
    fn default() -> Self {
        Self {
            dimension: 384, // MiniLM default
            m: 16,
            ef_construction: 200,
            ef_search: 50,
            max_elements: 100_000,
            shard_count: 1, // Single shard sufficient for most use cases
        }
    }
}

impl VectorSearchConfig {
    /// Create a new VectorSearchConfig with specified dimension
    pub fn new(dimension: usize) -> Self {
        Self {
            dimension,
            ..Default::default()
        }
    }
    
    /// Set HNSW M parameter
    pub fn with_m(mut self, m: usize) -> Self {
        self.m = m;
        self
    }
    
    /// Set ef_construction parameter
    pub fn with_ef_construction(mut self, ef_construction: usize) -> Self {
        self.ef_construction = ef_construction;
        self
    }
    
    /// Set ef_search parameter
    pub fn with_ef_search(mut self, ef_search: usize) -> Self {
        self.ef_search = ef_search;
        self
    }
    
    /// Set maximum number of elements
    pub fn with_max_elements(mut self, max_elements: usize) -> Self {
        self.max_elements = max_elements;
        self
    }
    
    /// Set HNSW shard count
    pub fn with_shards(mut self, shard_count: usize) -> Self {
        self.shard_count = shard_count;
        self
    }
    
    /// Convert to ConcurrentHNSWConfig
    fn to_hnsw_config(&self) -> ConcurrentHNSWConfig {
        let hnsw = HNSWConfig {
            m: self.m,
            m_max0: self.m * 2,
            ef_construction: self.ef_construction,
            ef_search: self.ef_search,
            dimension: self.dimension,
            max_elements: self.max_elements,
            seed: None,
        };
        ConcurrentHNSWConfig::with_hnsw(hnsw)
            .with_shards(self.shard_count)
    }
}

/// Result of a vector entity search
///
/// Contains the entity data along with the similarity score.
#[derive(Debug, Clone)]
pub struct VectorEntitySearchResult {
    /// Entity ID
    pub entity_id: String,
    
    /// Similarity score (higher is better, typically 0.0 to 1.0)
    pub score: f32,
    
    /// Full entity data
    pub entity: Entity,
}

/// Configuration for SocialStoreV2
#[derive(Debug, Clone)]
pub struct SocialStoreV2Config {
    /// Data directory
    pub path: PathBuf,
    
    /// Number of shards (0 = auto)
    pub shard_count: usize,
    
    /// Enable WAL for durability
    pub wal_enabled: bool,
    
    /// Auto-start workers
    pub auto_start: bool,
    
    /// High-throughput optimizations
    pub high_throughput: bool,
    
    /// Optional shard names for descriptive directory naming
    /// 
    /// When set, WAL directories will be named `shard_{id}_{name}` instead of `shard_{id}`.
    /// Example: `["person", "organization", "relation", "tag"]`
    /// 
    /// This also auto-generates type-to-shard mapping for EntityType strategy.
    pub shard_names: Option<Vec<String>>,
    
    /// Explicit type-to-shard mapping (overrides auto-generated from shard_names)
    /// 
    /// Keys are entity_type strings, values are shard IDs.
    /// Special key "_default" sets the fallback shard for unknown types.
    pub type_shard_mapping: Option<std::collections::HashMap<String, usize>>,
    
    /// Dedicated shard ID for all relations
    /// 
    /// When set, all relations will be routed to this shard regardless of type.
    pub relation_shard_id: Option<usize>,
    
    /// Optional vector search configuration
    /// 
    /// When set, enables vector similarity search on entities.
    /// A ConcurrentHNSWIndex will be created internally.
    pub vector_search: Option<VectorSearchConfig>,
}

impl SocialStoreV2Config {
    /// Create new config with data path
    pub fn new<P: AsRef<Path>>(path: P) -> Self {
        Self {
            path: path.as_ref().to_path_buf(),
            shard_count: 0, // auto
            wal_enabled: true,
            auto_start: true,
            high_throughput: false,
            shard_names: None,
            type_shard_mapping: None,
            relation_shard_id: None,
            vector_search: None,
        }
    }
    
    /// Set shard count
    pub fn with_shard_count(mut self, count: usize) -> Self {
        self.shard_count = count;
        self
    }
    
    /// Enable/disable WAL
    pub fn with_wal(mut self, enabled: bool) -> Self {
        self.wal_enabled = enabled;
        self
    }
    
    /// Enable high-throughput optimizations
    pub fn high_throughput(mut self) -> Self {
        self.high_throughput = true;
        self
    }
    
    /// Set shard names for descriptive WAL directory naming
    /// 
    /// # Example
    /// ```ignore
    /// let config = SocialStoreV2Config::new("data/social")
    ///     .with_shard_count(4)
    ///     .with_shard_names(vec!["person", "organization", "relation", "tag"]);
    /// ```
    pub fn with_shard_names<S: Into<String>>(mut self, names: Vec<S>) -> Self {
        self.shard_names = Some(names.into_iter().map(|s| s.into()).collect());
        self
    }
    
    /// Set explicit type-to-shard mapping
    /// 
    /// This overrides the auto-generated mapping from shard_names.
    /// 
    /// # Example
    /// ```ignore
    /// use std::collections::HashMap;
    /// 
    /// let mut mapping = HashMap::new();
    /// mapping.insert("person".to_string(), 0);
    /// mapping.insert("company".to_string(), 1);
    /// mapping.insert("tag".to_string(), 3);
    /// 
    /// let config = SocialStoreV2Config::new("data/social")
    ///     .with_shard_count(4)
    ///     .with_type_shard_mapping(mapping);
    /// ```
    pub fn with_type_shard_mapping(mut self, mapping: std::collections::HashMap<String, usize>) -> Self {
        self.type_shard_mapping = Some(mapping);
        self
    }
    
    /// Set dedicated shard ID for all relations
    /// 
    /// When set, all relations will be routed to this shard.
    pub fn with_relation_shard_id(mut self, shard_id: usize) -> Self {
        self.relation_shard_id = Some(shard_id);
        self
    }
    
    /// Enable vector search with the given configuration
    /// 
    /// When enabled, SocialStoreV2 will internally embed a ConcurrentHNSWIndex
    /// and support `index_entity()`, `search_entities_by_vector()`, `hybrid_search()` methods.
    /// 
    /// # Example
    /// ```ignore
    /// let config = SocialStoreV2Config::new("data/social")
    ///     .with_vector_search(VectorSearchConfig::new(384));
    /// ```
    pub fn with_vector_search(mut self, config: VectorSearchConfig) -> Self {
        self.vector_search = Some(config);
        self
    }
    
    /// Convert to ConcurrentGraphConfig
    fn to_concurrent_config(&self) -> ConcurrentGraphConfig {
        let mut config = ConcurrentGraphConfig::new(&self.path)
            .with_shard_count(self.shard_count)
            .with_auto_start(self.auto_start)
            .with_wal_enabled(self.wal_enabled);
        
        if self.high_throughput {
            config = config.high_throughput();
        }
        
        if let Some(ref names) = self.shard_names {
            config = config.with_shard_names(names.clone());
        }
        
        // Apply type-to-shard mapping if provided
        if let Some(ref mapping) = self.type_shard_mapping {
            config = config.with_type_shard_mapping(mapping.clone());
        }
        
        // Apply dedicated relation shard if provided
        if let Some(shard_id) = self.relation_shard_id {
            config = config.with_relation_shard_id(shard_id);
        }
        
        config
    }
}

/// High-concurrency social store with SocialStore API compatibility
///
/// This is the V2 implementation backed by sharded concurrent storage.
/// Optionally embeds a ConcurrentHNSWIndex for vector similarity search,
/// using entity.id as the bridging key between graph and vector stores.
pub struct SocialStoreV2 {
    /// Concurrent graph store
    router: Arc<GraphShardRouter>,
    
    /// Birthday index (in-memory for fast lookup)
    birthday_index: RwLock<BirthdayIndex>,
    
    /// Tokio runtime for sync API (None when created via new_async)
    runtime: Option<tokio::runtime::Runtime>,
    
    /// Optional vector search index (bridged via entity.id)
    vector_index: Option<Arc<ConcurrentHNSWIndex>>,
    
    /// Optional vector WAL for crash recovery
    vector_wal: Option<parking_lot::Mutex<VectorWAL>>,
    
    /// Vector search configuration (stored for reference)
    vector_config: Option<VectorSearchConfig>,
}

impl SocialStoreV2 {
    /// Create a new SocialStoreV2 with configuration
    pub fn new(config: SocialStoreV2Config) -> Self {
        // Create a dedicated runtime for sync operations first
        let runtime = tokio::runtime::Builder::new_multi_thread()
            .worker_threads(2)
            .enable_all()
            .build()
            .expect("Failed to create tokio runtime");
        
        // Create config with auto_start disabled initially
        let mut concurrent_config = config.to_concurrent_config();
        let should_auto_start = concurrent_config.auto_start;
        concurrent_config.auto_start = false;
        
        // Create router and start workers within the runtime context
        let router = runtime.block_on(async {
            let router = Arc::new(GraphShardRouter::new(concurrent_config));
            
            // Start workers if auto_start was requested (must be inside async context)
            if should_auto_start {
                router.start_workers();
            }
            
            router
        });
        
        // Initialize optional vector search
        let (vector_index, vector_wal, vector_config) = Self::init_vector_search(
            &config.vector_search,
            &config.path,
            config.wal_enabled,
        );
        
        let store = Self {
            router,
            birthday_index: RwLock::new(BirthdayIndex::new()),
            runtime: Some(runtime),
            vector_index,
            vector_wal,
            vector_config,
        };
        
        // Auto-recover from WAL to restore persisted data
        if let Err(e) = store.recover() {
            tracing::warn!("Failed to recover from WAL: {}", e);
        }
        
        store
    }
    
    /// Create with default config for a path
    pub fn open<P: AsRef<Path>>(path: P) -> Self {
        Self::new(SocialStoreV2Config::new(path))
    }
    
    /// Create a new SocialStoreV2 from within an existing async context
    /// 
    /// Use this when you already have a tokio runtime (e.g., in async tests or applications).
    /// This method must be called from within a tokio runtime context.
    /// 
    /// Note: When using this constructor, sync APIs will use tokio::task::block_in_place
    /// which requires the multi-threaded runtime.
    pub async fn new_async(config: SocialStoreV2Config) -> Self {
        // Create config with auto_start disabled initially
        let mut concurrent_config = config.to_concurrent_config();
        let should_auto_start = concurrent_config.auto_start;
        concurrent_config.auto_start = false;
        
        // Create router
        let router = Arc::new(GraphShardRouter::new(concurrent_config));
        
        // Start workers if auto_start was requested
        if should_auto_start {
            router.start_workers();
        }
        
        // Initialize optional vector search
        let (vector_index, vector_wal, vector_config) = Self::init_vector_search(
            &config.vector_search,
            &config.path,
            config.wal_enabled,
        );
        
        // No internal runtime - will use block_in_place for sync calls
        let store = Self {
            router,
            birthday_index: RwLock::new(BirthdayIndex::new()),
            runtime: None,
            vector_index,
            vector_wal,
            vector_config,
        };
        
        // Auto-recover from WAL to restore persisted data
        if let Err(e) = store.recover() {
            tracing::warn!("Failed to recover from WAL: {}", e);
        }
        
        store
    }
    
    /// Helper to run async code in sync context
    fn block_on<F: std::future::Future>(&self, f: F) -> F::Output {
        match &self.runtime {
            Some(rt) => rt.block_on(f),
            None => {
                // When created via new_async, use the current runtime
                tokio::task::block_in_place(|| {
                    tokio::runtime::Handle::current().block_on(f)
                })
            }
        }
    }
    
    /// Initialize optional vector search components
    fn init_vector_search(
        config: &Option<VectorSearchConfig>,
        base_path: &Path,
        wal_enabled: bool,
    ) -> (
        Option<Arc<ConcurrentHNSWIndex>>,
        Option<parking_lot::Mutex<VectorWAL>>,
        Option<VectorSearchConfig>,
    ) {
        match config {
            Some(vs_config) => {
                // Create ConcurrentHNSWIndex
                let hnsw_config = vs_config.to_hnsw_config();
                let index = Arc::new(ConcurrentHNSWIndex::new(hnsw_config));
                
                // Create Vector WAL
                let wal = if wal_enabled {
                    let wal_path = base_path.join("vector").join("vector.wal");
                    match VectorWAL::new(&wal_path) {
                        Ok(w) => {
                            tracing::info!("Vector WAL initialized at {:?}", wal_path);
                            Some(parking_lot::Mutex::new(w))
                        }
                        Err(e) => {
                            tracing::warn!("Failed to create vector WAL at {:?}: {}", wal_path, e);
                            None
                        }
                    }
                } else {
                    Some(parking_lot::Mutex::new(VectorWAL::disabled()))
                };
                
                tracing::info!(
                    "Vector search enabled: dimension={}, m={}, max_elements={}",
                    vs_config.dimension, vs_config.m, vs_config.max_elements
                );
                
                (Some(index), wal, Some(vs_config.clone()))
            }
            None => (None, None, None),
        }
    }
    
    /// Recover from WAL (call after creation to restore state)
    pub fn recover(&self) -> std::io::Result<()> {
        self.router.recover()?;
        self.rebuild_birthday_index();
        
        // Recover vector index from vector WAL
        if let (Some(ref index), Some(ref wal_mutex)) = (&self.vector_index, &self.vector_wal) {
            self.recover_vector_index(index, wal_mutex)?;
        }
        
        Ok(())
    }
    
    /// Recover vector index by replaying vector WAL entries
    fn recover_vector_index(
        &self,
        index: &Arc<ConcurrentHNSWIndex>,
        wal_mutex: &parking_lot::Mutex<VectorWAL>,
    ) -> std::io::Result<()> {
        let wal = wal_mutex.lock();
        let entries = wal.read_all()?;
        
        if entries.is_empty() {
            return Ok(());
        }
        
        tracing::info!("Recovering vector index: {} WAL entries to replay", entries.len());
        
        let mut indexed = 0usize;
        let mut removed = 0usize;
        let mut errors = 0usize;
        
        for entry in &entries {
            match entry.op {
                super::vector_wal::VectorWALOp::Index => {
                    if let Some(ref embedding) = entry.embedding {
                        match self.block_on(index.insert(entry.entity_id.clone(), embedding.clone())) {
                            Ok(_) => indexed += 1,
                            Err(e) => {
                                // May fail for duplicates during replay - try delete then re-insert
                                let _ = self.block_on(index.delete(&entry.entity_id));
                                match self.block_on(index.insert(entry.entity_id.clone(), embedding.clone())) {
                                    Ok(_) => indexed += 1,
                                    Err(e2) => {
                                        tracing::warn!(
                                            "Failed to recover vector for entity {}: {} (retry: {})",
                                            entry.entity_id, e, e2
                                        );
                                        errors += 1;
                                    }
                                }
                            }
                        }
                    }
                }
                super::vector_wal::VectorWALOp::Remove => {
                    match self.block_on(index.delete(&entry.entity_id)) {
                        Ok(_) => removed += 1,
                        Err(_) => {
                            // Entity may not exist in index (already removed) - that's OK
                            removed += 1;
                        }
                    }
                }
            }
        }
        
        tracing::info!(
            "Vector index recovery complete: {} indexed, {} removed, {} errors, {} total vectors",
            indexed, removed, errors, index.len()
        );
        
        Ok(())
    }
    
    /// Get reference to underlying router
    pub fn router(&self) -> &GraphShardRouter {
        &self.router
    }
    
    /// Check if vector search is enabled
    pub fn is_vector_search_enabled(&self) -> bool {
        self.vector_index.is_some()
    }
    
    /// Get vector search configuration (if enabled)
    pub fn vector_search_config(&self) -> Option<&VectorSearchConfig> {
        self.vector_config.as_ref()
    }
    
    /// Get the number of indexed vectors
    pub fn vector_count(&self) -> usize {
        self.vector_index.as_ref().map(|i| i.len()).unwrap_or(0)
    }
    
    /// Get reference to birthday index
    pub fn birthday_index(&self) -> parking_lot::RwLockReadGuard<'_, BirthdayIndex> {
        self.birthday_index.read()
    }
    
    /// Get mutable reference to birthday index
    pub fn birthday_index_mut(&self) -> parking_lot::RwLockWriteGuard<'_, BirthdayIndex> {
        self.birthday_index.write()
    }
    
    // ========================================================================
    // Person Management - Async API
    // ========================================================================
    
    /// Add a new person (async)
    /// 
    /// Uses sync write to ensure the entity is persisted before returning.
    pub async fn add_person_async(&self, input: PersonInput) -> Result<Entity, GraphStoreError> {
        let mut entity = Entity::new(&input.name, entity_types::PERSON);
        
        if let Some(id) = input.id {
            entity = entity.with_id(id);
        }
        
        if let Some(avatar) = input.avatar {
            entity = entity.with_property("avatar", serde_json::json!(avatar));
        }
        if let Some(description) = input.description {
            entity = entity.with_property("description", serde_json::json!(description));
        }
        if let Some(phone) = input.phone {
            entity = entity.with_property("phone", serde_json::json!(phone));
        }
        if let Some(email) = input.email {
            entity = entity.with_property("email", serde_json::json!(email));
        }
        if let Some(wechat) = input.wechat {
            entity = entity.with_property("wechat", serde_json::json!(wechat));
        }
        
        for (key, value) in input.properties {
            entity = entity.with_property(key, value);
        }
        
        // Use sync write to ensure persistence
        self.router.put_entity_sync(entity.clone()).await?;
        Ok(entity)
    }
    
    /// Update a person (async)
    pub async fn update_person_async(&self, id: &str, updates: BTreeMap<String, serde_json::Value>) -> Result<Option<Entity>, GraphStoreError> {
        if let Some(mut entity) = self.get_person(id) {
            for (key, value) in updates {
                if key == "name" {
                    if let Some(name) = value.as_str() {
                        entity.name = name.to_string();
                    }
                } else if key == "properties" {
                    // Special handling for "properties" key: merge inner properties
                    // instead of nesting them
                    if let Some(props) = value.as_object() {
                        for (prop_key, prop_value) in props {
                            entity.properties.insert(prop_key.clone(), prop_value.clone());
                        }
                    }
                } else {
                    entity.properties.insert(key, value);
                }
            }
            entity.updated_at = chrono_now();
            self.router.put_entity_sync(entity.clone()).await?;
            Ok(Some(entity))
        } else {
            Ok(None)
        }
    }
    
    /// Delete a person (async)
    pub async fn delete_person_async(&self, id: &str) -> Result<bool, GraphStoreError> {
        // Check if person exists first
        if self.get_person(id).is_none() {
            return Ok(false);
        }
        
        // Also delete related relations
        let relations = self.router.outgoing_from(id);
        for rel in relations {
            self.router.delete_relation_sync(&rel.id, Some(id), None).await?;
        }
        
        // Delete incoming relations
        let incoming = self.router.incoming_to(id);
        for rel in incoming {
            self.router.delete_relation_sync(&rel.id, Some(&rel.source), None).await?;
        }
        
        self.router.delete_entity_sync(id).await?;
        Ok(true)
    }
    
    // ========================================================================
    // Person Management - Sync API (Compatible with SocialStore)
    // ========================================================================
    
    /// Add a new person
    pub fn add_person(&self, input: PersonInput) -> Entity {
        self.block_on(async {
            self.add_person_async(input).await.expect("Failed to add person")
        })
    }
    
    /// Get a person by ID
    pub fn get_person(&self, id: &str) -> Option<Entity> {
        self.router.get_entity(id).filter(|e| e.entity_type == entity_types::PERSON)
    }
    
    /// Update a person
    pub fn update_person(&self, id: &str, updates: BTreeMap<String, serde_json::Value>) -> Option<Entity> {
        self.block_on(async {
            self.update_person_async(id, updates).await.expect("Failed to update person")
        })
    }
    
    /// Delete a person
    pub fn delete_person(&self, id: &str) -> bool {
        self.block_on(async {
            self.delete_person_async(id).await.unwrap_or(false)
        })
    }
    
    /// List all persons with optional filter
    pub fn list_persons(&self, filter: Option<PersonFilter>) -> Vec<Entity> {
        let mut persons = self.router.find_entities_by_type(entity_types::PERSON);
        
        if let Some(f) = filter {
            if let Some(name) = f.name {
                let name_lower = name.to_lowercase();
                persons.retain(|p| p.name.to_lowercase().contains(&name_lower));
            }
            
            if let Some(tags) = f.tags {
                persons.retain(|p| {
                    let person_tags = self.get_person_tags(&p.id);
                    tags.iter().any(|t| person_tags.iter().any(|pt| pt.id == *t))
                });
            }
            
            if let Some(limit) = f.limit {
                persons.truncate(limit);
            }
        }
        
        persons
    }
    
    // ========================================================================
    // Friend Relationship Management - Async API
    // ========================================================================
    
    /// Add friend relationship (async)
    /// 
    /// Uses deterministic relation ID to prevent duplicate relations.
    /// For bidirectional relations, the ID is normalized (smaller ID first).
    pub async fn add_friend_async(&self, person_a: &str, person_b: &str, config: FriendConfig) -> Result<Option<Relation>, GraphStoreError> {
        // Verify both persons exist
        if self.get_person(person_a).is_none() || self.get_person(person_b).is_none() {
            return Ok(None);
        }
        
        // Generate deterministic ID to prevent duplicate relations
        // For bidirectional relations, normalize the order (smaller ID first)
        let (id_first, id_second) = if config.bidirectional && person_a > person_b {
            (person_b, person_a)
        } else {
            (person_a, person_b)
        };
        let deterministic_id = format!("rel:{}:{}:{}", id_first, id_second, &config.relation_type);
        
        let mut relation = Relation::new(person_a, person_b, &config.relation_type)
            .with_id(deterministic_id)
            .with_weight(config.weight);
        
        if config.bidirectional {
            relation = relation.bidirectional();
        }
        
        if let Some(since) = config.since {
            relation = relation.with_metadata("since", serde_json::json!(since));
        }
        if let Some(via) = config.via {
            relation = relation.with_metadata("via", serde_json::json!(via));
        }
        
        for (key, value) in config.metadata {
            relation = relation.with_metadata(key, value);
        }
        
        self.router.put_relation_sync(relation.clone()).await?;
        Ok(Some(relation))
    }
    
    /// Remove friend relationship (async)
    pub async fn remove_friend_async(&self, person_a: &str, person_b: &str) -> Result<bool, GraphStoreError> {
        let relations = self.router.outgoing_from(person_a);
        for rel in relations {
            if rel.target == person_b && is_social_relation(&rel.relation_type) {
                self.router.delete_relation_sync(&rel.id, Some(person_a), None).await?;
                return Ok(true);
            }
        }
        
        // Check reverse direction for bidirectional
        let relations = self.router.outgoing_from(person_b);
        for rel in relations {
            if rel.target == person_a && is_social_relation(&rel.relation_type) && rel.bidirectional {
                self.router.delete_relation_sync(&rel.id, Some(person_b), None).await?;
                return Ok(true);
            }
        }
        
        Ok(false)
    }
    
    // ========================================================================
    // Friend Relationship Management - Sync API
    // ========================================================================
    
    /// Add friend relationship between two persons
    pub fn add_friend(&self, person_a: &str, person_b: &str, config: FriendConfig) -> Option<Relation> {
        self.block_on(async {
            self.add_friend_async(person_a, person_b, config).await.expect("Failed to add friend")
        })
    }
    
    /// Remove friend relationship
    pub fn remove_friend(&self, person_a: &str, person_b: &str) -> bool {
        self.block_on(async {
            self.remove_friend_async(person_a, person_b).await.unwrap_or(false)
        })
    }
    
    /// Get all friends of a person
    pub fn get_friends(&self, person_id: &str) -> Vec<Entity> {
        let mut friends = Vec::new();
        let mut seen = std::collections::HashSet::new();
        
        // Outgoing relations
        for rel in self.router.outgoing_from(person_id) {
            if is_social_relation(&rel.relation_type) {
                if let Some(entity) = self.get_person(&rel.target) {
                    if seen.insert(entity.id.clone()) {
                        friends.push(entity);
                    }
                }
            }
        }
        
        // Incoming bidirectional relations
        for rel in self.router.incoming_to(person_id) {
            if is_social_relation(&rel.relation_type) && rel.bidirectional {
                if let Some(entity) = self.get_person(&rel.source) {
                    if seen.insert(entity.id.clone()) {
                        friends.push(entity);
                    }
                }
            }
        }
        
        friends
    }
    
    /// Check if two persons are friends
    pub fn are_friends(&self, person_a: &str, person_b: &str) -> bool {
        // Check direct relation
        for rel in self.router.outgoing_from(person_a) {
            if rel.target == person_b && is_social_relation(&rel.relation_type) {
                return true;
            }
        }
        
        // Check bidirectional reverse
        for rel in self.router.outgoing_from(person_b) {
            if rel.target == person_a && is_social_relation(&rel.relation_type) && rel.bidirectional {
                return true;
            }
        }
        
        false
    }
    
    /// Get direct relation between two persons
    pub fn get_direct_relation(&self, person_a: &str, person_b: &str) -> Option<Relation> {
        for rel in self.router.outgoing_from(person_a) {
            if rel.target == person_b && is_social_relation(&rel.relation_type) {
                return Some(rel);
            }
        }
        
        for rel in self.router.outgoing_from(person_b) {
            if rel.target == person_a && is_social_relation(&rel.relation_type) && rel.bidirectional {
                return Some(rel);
            }
        }
        
        None
    }
    
    // ========================================================================
    // Relation Query API
    // ========================================================================
    
    /// List all relations with optional filter
    /// 
    /// Returns all relations in the social graph including friend relations,
    /// person-tag relations, etc. Uses the router's all_relations() for
    /// efficient retrieval across all shards.
    /// 
    /// # Arguments
    /// 
    /// * `filter` - Optional filter criteria (relation_type, source_id, target_id, limit)
    /// 
    /// # Example
    /// 
    /// ```ignore
    /// // Get all relations
    /// let all_relations = store.list_relations(None);
    /// 
    /// // Get only friend relations
    /// let friend_relations = store.list_relations(Some(RelationFilter {
    ///     relation_type: Some("friend".to_string()),
    ///     ..Default::default()
    /// }));
    /// 
    /// // Get relations for a specific person
    /// let person_relations = store.list_relations(Some(RelationFilter {
    ///     source_id: Some(alice.id.clone()),
    ///     ..Default::default()
    /// }));
    /// ```
    pub fn list_relations(&self, filter: Option<RelationFilter>) -> Vec<Relation> {
        let filter = filter.unwrap_or_default();
        
        // Get all relations from router (already parallel across shards)
        let mut results = self.router.all_relations();
        
        // Apply relation_type filter
        if let Some(ref rel_type) = filter.relation_type {
            results.retain(|r| &r.relation_type == rel_type);
        }
        
        // Apply source_id filter (also matches target for bidirectional)
        if let Some(ref source) = filter.source_id {
            results.retain(|r| {
                &r.source == source || (r.bidirectional && &r.target == source)
            });
        }
        
        // Apply target_id filter (also matches source for bidirectional)
        if let Some(ref target) = filter.target_id {
            results.retain(|r| {
                &r.target == target || (r.bidirectional && &r.source == target)
            });
        }
        
        // Apply limit
        if let Some(limit) = filter.limit {
            results.truncate(limit);
        }
        
        results
    }
    
    /// Get a relation by its ID
    /// 
    /// # Arguments
    /// 
    /// * `id` - The relation ID
    /// 
    /// # Returns
    /// 
    /// The relation if found, None otherwise
    pub fn get_relation_by_id(&self, id: &str) -> Option<Relation> {
        self.router.get_relation(id, None)
    }
    
    /// Get relation between two entities (in either direction)
    /// 
    /// Unlike `get_direct_relation` which only checks social relations,
    /// this method checks all relation types.
    /// 
    /// # Arguments
    /// 
    /// * `entity_a` - First entity ID
    /// * `entity_b` - Second entity ID
    /// 
    /// # Returns
    /// 
    /// The relation if found, None otherwise
    pub fn get_relation_between(&self, entity_a: &str, entity_b: &str) -> Option<Relation> {
        // Check outgoing from entity_a to entity_b
        for rel in self.router.outgoing_from(entity_a) {
            if rel.target == entity_b {
                return Some(rel);
            }
        }
        
        // Check outgoing from entity_b to entity_a (for bidirectional or reverse)
        for rel in self.router.outgoing_from(entity_b) {
            if rel.target == entity_a {
                return Some(rel);
            }
        }
        
        None
    }
    
    /// Get all relations involving a specific entity
    /// 
    /// Returns both outgoing and incoming relations.
    /// 
    /// # Arguments
    /// 
    /// * `entity_id` - The entity ID
    /// 
    /// # Returns
    /// 
    /// Vector of relations where the entity is either source or target
    pub fn get_entity_relations(&self, entity_id: &str) -> Vec<Relation> {
        let mut relations = Vec::new();
        
        // Outgoing relations
        relations.extend(self.router.outgoing_from(entity_id));
        
        // Incoming relations
        relations.extend(self.router.incoming_to(entity_id));
        
        relations
    }
    
    // ========================================================================
    // Graph Export API
    // ========================================================================
    
    /// Export the complete social graph for visualization
    /// 
    /// This method collects all nodes (persons, tags, companies) and edges
    /// (relations) into a format suitable for graph visualization libraries
    /// like vis-network.
    /// 
    /// # Arguments
    /// 
    /// * `options` - Optional export configuration (max_nodes, max_edges, filters)
    /// 
    /// # Returns
    /// 
    /// An `ExportedGraph` containing nodes, edges, and statistics
    /// 
    /// # Example
    /// 
    /// ```ignore
    /// // Export with default settings (max 500 nodes)
    /// let graph = store.export_graph(None);
    /// 
    /// // Export with custom settings
    /// let graph = store.export_graph(Some(ExportGraphOptions {
    ///     max_nodes: Some(100),
    ///     include_tags: Some(true),
    ///     include_companies: Some(false),
    ///     ..Default::default()
    /// }));
    /// ```
    pub fn export_graph(&self, options: Option<ExportGraphOptions>) -> ExportedGraph {
        let opts = options.unwrap_or_default();
        let max_nodes = opts.max_nodes.unwrap_or(500);
        let include_tags = opts.include_tags.unwrap_or(true);
        let include_companies = opts.include_companies.unwrap_or(true);
        let include_edge_meta = opts.include_edge_meta.unwrap_or(true);
        let include_node_degree = opts.include_node_degree.unwrap_or(true);
        
        let mut nodes = Vec::new();
        let mut edges = Vec::new();
        let mut node_ids = std::collections::HashSet::new();
        
        // 1. Get all person nodes
        let persons = self.list_persons(None);
        let person_count = persons.len();
        
        for (idx, person) in persons.into_iter().enumerate() {
            if idx >= max_nodes {
                break;
            }
            
            node_ids.insert(person.id.clone());
            nodes.push(Self::entity_to_graph_node(&person, NodeGroup::Person));
        }
        
        // 2. Get all tag/company nodes
        let tags = self.list_tags(None);
        let mut company_count = 0;
        let mut tag_count = 0;
        
        for tag in tags {
            if nodes.len() >= max_nodes {
                break;
            }
            
            let is_company = Self::is_company_entity(&tag);
            
            if is_company && !include_companies {
                continue;
            }
            if !is_company && !include_tags {
                continue;
            }
            
            if is_company {
                company_count += 1;
            } else {
                tag_count += 1;
            }
            
            node_ids.insert(tag.id.clone());
            nodes.push(Self::entity_to_graph_node(
                &tag,
                if is_company { NodeGroup::Company } else { NodeGroup::Tag },
            ));
        }
        
        // 3. Get all relations and filter
        let relations = self.list_relations(None);
        let mut seen_edges = std::collections::HashSet::new();
        
        for rel in relations {
            // Only keep relations where both ends are in the node set
            if !node_ids.contains(&rel.source) || !node_ids.contains(&rel.target) {
                continue;
            }
            
            // Apply relation type filter
            if let Some(ref types) = opts.relation_types {
                if !types.contains(&rel.relation_type) {
                    continue;
                }
            }
            
            // Deduplicate (for bidirectional relations)
            let edge_key = Self::edge_key(&rel.source, &rel.target);
            if seen_edges.contains(&edge_key) {
                continue;
            }
            seen_edges.insert(edge_key);
            
            // Apply max_edges limit
            if let Some(max) = opts.max_edges {
                if edges.len() >= max {
                    break;
                }
            }
            
            edges.push(Self::relation_to_graph_edge(&rel, include_edge_meta));
        }

        // 4. Optional node degree enrichment (connection count)
        if include_node_degree {
            let mut degree_map: std::collections::HashMap<String, usize> = nodes
                .iter()
                .map(|n| (n.id.clone(), 0))
                .collect();

            for edge in &edges {
                if let Some(deg) = degree_map.get_mut(&edge.from) {
                    *deg += 1;
                }
                if let Some(deg) = degree_map.get_mut(&edge.to) {
                    *deg += 1;
                }
            }

            for node in &mut nodes {
                node.degree = Some(degree_map.get(&node.id).copied().unwrap_or(0));
            }
        }
        
        // 5. Build statistics
        let stats = GraphStats {
            persons: person_count,
            companies: company_count,
            tags: tag_count,
            relations: edges.len(),
        };
        
        ExportedGraph { nodes, edges, stats }
    }
    
    // Helper: Convert Entity to GraphNode
    fn entity_to_graph_node(entity: &Entity, group: NodeGroup) -> GraphNode {
        let title = entity.properties
            .get("description")
            .and_then(|v| v.as_str())
            .unwrap_or(&entity.name)
            .to_string();
        
        // Clone properties and add name field for frontend compatibility
        let mut data = entity.properties.clone();
        data.insert("name".to_string(), serde_json::json!(entity.name));
        
        GraphNode {
            id: entity.id.clone(),
            label: entity.name.clone(),
            group,
            title,
            degree: None,
            data,
        }
    }
    
    // Helper: Convert Relation to GraphEdge
    fn relation_to_graph_edge(rel: &Relation, include_meta: bool) -> GraphEdge {
        let label = Self::relation_type_label(&rel.relation_type);
        
        GraphEdge {
            id: rel.id.clone(),
            from: rel.source.clone(),
            to: rel.target.clone(),
            label,
            relation_type: rel.relation_type.clone(),
            weight: rel.weight,
            data: if include_meta {
                let mut data = rel.metadata.clone();
                data.insert("bidirectional".to_string(), serde_json::json!(rel.bidirectional));
                Some(data)
            } else {
                None
            },
        }
    }
    
    // Helper: Get human-readable relation type label
    fn relation_type_label(rel_type: &str) -> String {
        match rel_type {
            "friend" => "好友",
            "knows" => "认识",
            "colleague" => "同事",
            "classmate" => "同学",
            "family" => "家人",
            "mentor" => "导师",
            "tagged" => "标签",
            "belongs_to" => "所属",
            "work_for" => "就职于",
            _ => rel_type,
        }.to_string()
    }
    
    // Helper: Check if entity is a company type
    fn is_company_entity(entity: &Entity) -> bool {
        entity.entity_type == entity_types::COMPANY ||
        entity.properties.get("tagType").map_or(false, |v| v == "company") ||
        entity.properties.get("category").map_or(false, |v| v == "company")
    }
    
    // Helper: Generate unique edge key for deduplication
    fn edge_key(a: &str, b: &str) -> String {
        if a < b {
            format!("{}-{}", a, b)
        } else {
            format!("{}-{}", b, a)
        }
    }
    
    // ========================================================================
    // Tag Management - Async API
    // ========================================================================
    
    /// Add a new tag (async)
    pub async fn add_tag_async(&self, input: TagInput) -> Result<Entity, GraphStoreError> {
        let entity_type = match input.tag_type.as_str() {
            "company" => entity_types::COMPANY,
            "organization" => entity_types::ORGANIZATION,
            "skill" => entity_types::SKILL,
            "interest" => entity_types::INTEREST,
            _ => entity_types::TAG,
        };
        
        let mut entity = Entity::new(&input.name, entity_type);
        
        if let Some(id) = input.id {
            entity = entity.with_id(id);
        }
        
        // Save tagType/category to properties for frontend display
        entity = entity.with_property("tagType", serde_json::json!(&input.tag_type));
        entity = entity.with_property("category", serde_json::json!(&input.tag_type));
        
        if let Some(description) = input.description {
            entity = entity.with_property("description", serde_json::json!(description));
        }
        if let Some(color) = input.color {
            entity = entity.with_property("color", serde_json::json!(color));
        }
        
        for (key, value) in input.properties {
            entity = entity.with_property(key, value);
        }
        
        self.router.put_entity_sync(entity.clone()).await?;
        
        // Add parent tag relations (with deterministic ID to prevent duplicates)
        for parent_id in input.parent_tags {
            if self.router.contains_entity(&parent_id) {
                let deterministic_id = format!("rel:{}:{}:{}", &entity.id, &parent_id, relation_types::PARENT_OF);
                let relation = Relation::new(&entity.id, &parent_id, relation_types::PARENT_OF)
                    .with_id(deterministic_id);
                self.router.put_relation_sync(relation).await?;
            }
        }
        
        Ok(entity)
    }
    
    /// Tag a person (async)
    /// 
    /// Uses deterministic relation ID based on (person_id, tag_id, relation_type)
    /// to prevent duplicate relations. If the relation already exists, this is a no-op.
    /// 
    /// Creates a `has_tags` relation: person --has_tags--> tag
    pub async fn tag_person_async(&self, person_id: &str, tag_id: &str) -> Result<Option<Relation>, GraphStoreError> {
        if self.get_person(person_id).is_none() || self.get_tag(tag_id).is_none() {
            return Ok(None);
        }
        
        // Generate deterministic ID to prevent duplicate relations
        // Format: "rel:{source}:{target}:{relation_type}"
        let deterministic_id = format!("rel:{}:{}:{}", person_id, tag_id, relation_types::HAS_TAGS);
        
        let relation = Relation::new(person_id, tag_id, relation_types::HAS_TAGS)
            .with_id(deterministic_id);
        self.router.put_relation_sync(relation.clone()).await?;
        Ok(Some(relation))
    }
    
    /// Remove tag from person (async)
    /// 
    /// Removes the `has_tags` relation between person and tag.
    /// Also supports legacy `belongs_to` relations for backward compatibility.
    pub async fn untag_person_async(&self, person_id: &str, tag_id: &str) -> Result<bool, GraphStoreError> {
        for rel in self.router.outgoing_from(person_id) {
            // Support both has_tags (new) and belongs_to (legacy) for backward compatibility
            if rel.target == tag_id && (rel.relation_type == relation_types::HAS_TAGS || rel.relation_type == relation_types::BELONGS_TO) {
                self.router.delete_relation_sync(&rel.id, Some(person_id), None).await?;
                return Ok(true);
            }
        }
        Ok(false)
    }
    
    // ========================================================================
    // Tag Management - Sync API
    // ========================================================================
    
    /// Add a new tag
    pub fn add_tag(&self, input: TagInput) -> Entity {
        self.block_on(async {
            self.add_tag_async(input).await.expect("Failed to add tag")
        })
    }
    
    /// Get a tag by ID
    pub fn get_tag(&self, id: &str) -> Option<Entity> {
        self.router.get_entity(id).filter(|e| is_tag_type(&e.entity_type))
    }
    
    /// Tag a person (add person to tag)
    pub fn tag_person(&self, person_id: &str, tag_id: &str) -> Option<Relation> {
        self.block_on(async {
            self.tag_person_async(person_id, tag_id).await.expect("Failed to tag person")
        })
    }
    
    /// Remove tag from person
    pub fn untag_person(&self, person_id: &str, tag_id: &str) -> bool {
        self.block_on(async {
            self.untag_person_async(person_id, tag_id).await.unwrap_or(false)
        })
    }
    
    /// Get all tags of a person
    /// 
    /// This method finds all tag-like entities (tags, companies, skills, etc.)
    /// that are connected to the person via tag relations.
    /// 
    /// Supported relation types:
    /// - `has_tags` - person has tag (new default)
    /// - `belongs_to` - legacy tag membership (for backward compatibility)
    /// - `tagged` - explicit tagging
    /// - `work_for` - employment at a company
    /// - `member_of` - membership in organization
    pub fn get_person_tags(&self, person_id: &str) -> Vec<Entity> {
        self.router.outgoing_from(person_id)
            .into_iter()
            .filter(|r| is_tag_relation(&r.relation_type))
            .filter_map(|r| self.get_tag(&r.target))
            .collect()
    }
    
    /// Get all members of a tag
    /// 
    /// This method finds all persons that are connected to the tag
    /// via tag relations (belongs_to, tagged, work_for, member_of).
    pub fn get_tag_members(&self, tag_id: &str) -> Vec<Entity> {
        self.router.incoming_to(tag_id)
            .into_iter()
            .filter(|r| is_tag_relation(&r.relation_type))
            .filter_map(|r| self.get_person(&r.source))
            .collect()
    }
    
    /// Set parent tag (async)
    /// 
    /// Uses deterministic relation ID to prevent duplicate relations.
    pub async fn set_parent_tag_async(&self, child_id: &str, parent_id: &str) -> Result<Option<Relation>, GraphStoreError> {
        if self.get_tag(child_id).is_none() || self.get_tag(parent_id).is_none() {
            return Ok(None);
        }
        
        // Generate deterministic ID to prevent duplicate relations
        let deterministic_id = format!("rel:{}:{}:{}", child_id, parent_id, relation_types::PARENT_OF);
        let relation = Relation::new(child_id, parent_id, relation_types::PARENT_OF)
            .with_id(deterministic_id);
        self.router.put_relation_sync(relation.clone()).await?;
        Ok(Some(relation))
    }
    
    /// Set parent tag
    pub fn set_parent_tag(&self, child_id: &str, parent_id: &str) -> Option<Relation> {
        self.block_on(async {
            self.set_parent_tag_async(child_id, parent_id).await.expect("Failed to set parent tag")
        })
    }
    
    /// Get parent tags
    pub fn get_parent_tags(&self, tag_id: &str) -> Vec<Entity> {
        self.router.outgoing_from(tag_id)
            .into_iter()
            .filter(|r| r.relation_type == relation_types::PARENT_OF)
            .filter_map(|r| self.get_tag(&r.target))
            .collect()
    }
    
    /// Get child tags
    pub fn get_child_tags(&self, tag_id: &str) -> Vec<Entity> {
        self.router.incoming_to(tag_id)
            .into_iter()
            .filter(|r| r.relation_type == relation_types::PARENT_OF)
            .filter_map(|r| self.get_tag(&r.source))
            .collect()
    }
    
    /// List all tags with optional filter
    /// 
    /// Returns all tag entities (including company, organization, skill, interest types).
    /// Uses the global type index for efficient lookups.
    /// 
    /// # Arguments
    /// 
    /// * `filter` - Optional filter criteria (tag_type, name, limit)
    /// 
    /// # Example
    /// 
    /// ```ignore
    /// // Get all tags
    /// let all_tags = store.list_tags(None);
    /// 
    /// // Get only companies
    /// let companies = store.list_tags(Some(TagFilter {
    ///     tag_type: Some("company".to_string()),
    ///     ..Default::default()
    /// }));
    /// 
    /// // Get tags with name containing "tech"
    /// let tech_tags = store.list_tags(Some(TagFilter {
    ///     name: Some("tech".to_string()),
    ///     limit: Some(10),
    ///     ..Default::default()
    /// }));
    /// ```
    pub fn list_tags(&self, filter: Option<TagFilter>) -> Vec<Entity> {
        let filter = filter.unwrap_or_default();
        
        // Determine which entity types to query based on tag_type filter
        let entity_types_to_query: Vec<&str> = match filter.tag_type.as_deref() {
            Some("company") => vec![entity_types::COMPANY],
            Some("organization") => vec![entity_types::ORGANIZATION],
            Some("skill") => vec![entity_types::SKILL],
            Some("interest") => vec![entity_types::INTEREST],
            Some("tag") => vec![entity_types::TAG],
            None => vec![
                entity_types::TAG,
                entity_types::COMPANY,
                entity_types::ORGANIZATION,
                entity_types::SKILL,
                entity_types::INTEREST,
            ],
            // For unknown tag types, try the TAG entity type
            Some(_) => vec![entity_types::TAG],
        };
        
        // Collect entities from all relevant types using global type index
        let mut results: Vec<Entity> = entity_types_to_query
            .into_iter()
            .flat_map(|t| self.router.find_entities_by_type(t))
            .collect();
        
        // Apply name filter (case-insensitive partial match)
        if let Some(ref name) = filter.name {
            let name_lower = name.to_lowercase();
            results.retain(|e| e.name.to_lowercase().contains(&name_lower));
        }
        
        // Apply limit
        if let Some(limit) = filter.limit {
            results.truncate(limit);
        }
        
        results
    }
    
    // ========================================================================
    // Item Management - Async API
    // ========================================================================
    
    /// Add a new item (async)
    pub async fn add_item_async(&self, input: ItemInput) -> Result<Entity, GraphStoreError> {
        let entity_type = match input.category.as_str() {
            "vehicle" => entity_types::VEHICLE,
            "phone" | "computer" | "camera" | "watch" | "headphone" => entity_types::DEVICE,
            "equipment" => entity_types::EQUIPMENT,
            _ => entity_types::ITEM,
        };
        
        let mut entity = Entity::new(&input.name, entity_type);
        
        if let Some(id) = input.id {
            entity = entity.with_id(id);
        }
        
        entity = entity.with_property("category", serde_json::json!(input.category));
        
        if let Some(brand) = input.brand {
            entity = entity.with_property("brand", serde_json::json!(brand));
        }
        if let Some(model) = input.model {
            entity = entity.with_property("model", serde_json::json!(model));
        }
        
        for (key, value) in input.properties {
            entity = entity.with_property(key, value);
        }
        
        self.router.put_entity_sync(entity.clone()).await?;
        Ok(entity)
    }
    
    /// Add item to person (async)
    /// 
    /// Uses deterministic relation ID to prevent duplicate relations.
    pub async fn add_item_to_person_async(&self, person_id: &str, item_id: &str, relation_type: Option<&str>) -> Result<Option<Relation>, GraphStoreError> {
        if self.get_person(person_id).is_none() || self.get_item(item_id).is_none() {
            return Ok(None);
        }
        
        let rel_type = relation_type.unwrap_or(relation_types::OWNS);
        // Generate deterministic ID to prevent duplicate relations
        let deterministic_id = format!("rel:{}:{}:{}", person_id, item_id, rel_type);
        let relation = Relation::new(person_id, item_id, rel_type)
            .with_id(deterministic_id);
        self.router.put_relation_sync(relation.clone()).await?;
        Ok(Some(relation))
    }
    
    /// Remove item from person (async)
    pub async fn remove_item_from_person_async(&self, person_id: &str, item_id: &str) -> Result<bool, GraphStoreError> {
        for rel in self.router.outgoing_from(person_id) {
            if rel.target == item_id && is_item_relation(&rel.relation_type) {
                self.router.delete_relation_sync(&rel.id, Some(person_id), None).await?;
                return Ok(true);
            }
        }
        Ok(false)
    }
    
    // ========================================================================
    // Item Management - Sync API
    // ========================================================================
    
    /// Add a new item
    pub fn add_item(&self, input: ItemInput) -> Entity {
        self.block_on(async {
            self.add_item_async(input).await.expect("Failed to add item")
        })
    }
    
    /// Get an item by ID
    pub fn get_item(&self, id: &str) -> Option<Entity> {
        self.router.get_entity(id).filter(|e| is_item_type(&e.entity_type))
    }
    
    /// List items with optional category filter
    pub fn list_items(&self, category: Option<&str>) -> Vec<Entity> {
        let mut items = Vec::new();
        
        for entity_type in [entity_types::ITEM, entity_types::VEHICLE, entity_types::DEVICE, entity_types::EQUIPMENT] {
            items.extend(self.router.find_entities_by_type(entity_type));
        }
        
        if let Some(cat) = category {
            items.retain(|i| {
                i.properties.get("category")
                    .and_then(|v| v.as_str())
                    .map(|c| c == cat)
                    .unwrap_or(false)
            });
        }
        
        items
    }
    
    /// Add item to person (ownership relation)
    pub fn add_item_to_person(&self, person_id: &str, item_id: &str, relation_type: Option<&str>) -> Option<Relation> {
        self.block_on(async {
            self.add_item_to_person_async(person_id, item_id, relation_type).await.expect("Failed to add item to person")
        })
    }
    
    /// Remove item from person
    pub fn remove_item_from_person(&self, person_id: &str, item_id: &str) -> bool {
        self.block_on(async {
            self.remove_item_from_person_async(person_id, item_id).await.unwrap_or(false)
        })
    }
    
    /// Get all items of a person
    pub fn get_person_items(&self, person_id: &str) -> Vec<Entity> {
        self.router.outgoing_from(person_id)
            .into_iter()
            .filter(|r| is_item_relation(&r.relation_type))
            .filter_map(|r| self.get_item(&r.target))
            .collect()
    }
    
    /// Get all owners of an item
    pub fn get_item_owners(&self, item_id: &str) -> Vec<Entity> {
        self.router.incoming_to(item_id)
            .into_iter()
            .filter(|r| is_item_relation(&r.relation_type))
            .filter_map(|r| self.get_person(&r.source))
            .collect()
    }
    
    // ========================================================================
    // Attribute Management - Async API
    // ========================================================================
    
    /// Add a new attribute (async)
    pub async fn add_attribute_async(&self, input: AttributeInput) -> Result<Entity, GraphStoreError> {
        let entity_type = match input.category.as_str() {
            "zodiac" => entity_types::ZODIAC,
            "school" | "education" => entity_types::SCHOOL,
            "city" | "location" => entity_types::CITY,
            "hobby" => entity_types::HOBBY,
            "age" | "age_group" => entity_types::AGE_GROUP,
            "industry" => entity_types::INDUSTRY,
            _ => entity_types::ATTRIBUTE,
        };
        
        let mut entity = Entity::new(&input.value, entity_type);
        
        if let Some(id) = input.id {
            entity = entity.with_id(id);
        }
        
        entity = entity.with_property("category", serde_json::json!(input.category));
        
        for (key, value) in input.properties {
            entity = entity.with_property(key, value);
        }
        
        self.router.put_entity_sync(entity.clone()).await?;
        Ok(entity)
    }
    
    /// Set person attribute (async)
    /// 
    /// Uses deterministic relation ID to prevent duplicate relations.
    pub async fn set_person_attribute_async(&self, person_id: &str, attr_id: &str, relation_type: Option<&str>) -> Result<Option<Relation>, GraphStoreError> {
        if self.get_person(person_id).is_none() || self.get_attribute(attr_id).is_none() {
            return Ok(None);
        }
        
        let rel_type = relation_type.unwrap_or(relation_types::HAS_ATTRIBUTE);
        // Generate deterministic ID to prevent duplicate relations
        let deterministic_id = format!("rel:{}:{}:{}", person_id, attr_id, rel_type);
        let relation = Relation::new(person_id, attr_id, rel_type)
            .with_id(deterministic_id);
        self.router.put_relation_sync(relation.clone()).await?;
        Ok(Some(relation))
    }
    
    /// Remove person attribute (async)
    pub async fn remove_person_attribute_async(&self, person_id: &str, attr_id: &str) -> Result<bool, GraphStoreError> {
        for rel in self.router.outgoing_from(person_id) {
            if rel.target == attr_id && is_attribute_relation(&rel.relation_type) {
                self.router.delete_relation_sync(&rel.id, Some(person_id), None).await?;
                return Ok(true);
            }
        }
        Ok(false)
    }
    
    // ========================================================================
    // Attribute Management - Sync API
    // ========================================================================
    
    /// Add a new attribute
    pub fn add_attribute(&self, input: AttributeInput) -> Entity {
        self.block_on(async {
            self.add_attribute_async(input).await.expect("Failed to add attribute")
        })
    }
    
    /// Get an attribute by ID
    pub fn get_attribute(&self, id: &str) -> Option<Entity> {
        self.router.get_entity(id).filter(|e| is_attribute_type(&e.entity_type))
    }
    
    /// Find or create an attribute by category and value
    pub fn find_or_create_attribute(&self, category: &str, value: &str) -> Entity {
        // Search for existing attribute
        for entity_type in [entity_types::ATTRIBUTE, entity_types::ZODIAC, entity_types::SCHOOL,
                           entity_types::CITY, entity_types::HOBBY, entity_types::AGE_GROUP,
                           entity_types::INDUSTRY] {
            for entity in self.router.find_entities_by_type(entity_type) {
                let matches_category = entity.properties.get("category")
                    .and_then(|v| v.as_str())
                    .map(|c| c == category)
                    .unwrap_or(false);
                
                if matches_category && entity.name == value {
                    return entity;
                }
            }
        }
        
        // Create new attribute
        self.add_attribute(AttributeInput {
            id: None,
            category: category.to_string(),
            value: value.to_string(),
            properties: BTreeMap::new(),
        })
    }
    
    /// List attributes with optional category filter
    pub fn list_attributes(&self, category: Option<&str>) -> Vec<Entity> {
        let mut attrs = Vec::new();
        
        for entity_type in [entity_types::ATTRIBUTE, entity_types::ZODIAC, entity_types::SCHOOL,
                           entity_types::CITY, entity_types::HOBBY, entity_types::AGE_GROUP,
                           entity_types::INDUSTRY] {
            attrs.extend(self.router.find_entities_by_type(entity_type));
        }
        
        if let Some(cat) = category {
            attrs.retain(|a| {
                a.properties.get("category")
                    .and_then(|v| v.as_str())
                    .map(|c| c == cat)
                    .unwrap_or(false)
            });
        }
        
        attrs
    }
    
    /// Set person attribute
    pub fn set_person_attribute(&self, person_id: &str, attr_id: &str, relation_type: Option<&str>) -> Option<Relation> {
        self.block_on(async {
            self.set_person_attribute_async(person_id, attr_id, relation_type).await.expect("Failed to set person attribute")
        })
    }
    
    /// Remove person attribute
    pub fn remove_person_attribute(&self, person_id: &str, attr_id: &str) -> bool {
        self.block_on(async {
            self.remove_person_attribute_async(person_id, attr_id).await.unwrap_or(false)
        })
    }
    
    /// Get all attributes of a person
    pub fn get_person_attributes(&self, person_id: &str) -> Vec<Entity> {
        self.router.outgoing_from(person_id)
            .into_iter()
            .filter(|r| is_attribute_relation(&r.relation_type))
            .filter_map(|r| self.get_attribute(&r.target))
            .collect()
    }
    
    /// Get all persons with a specific attribute
    pub fn get_persons_by_attribute(&self, attr_id: &str) -> Vec<Entity> {
        self.router.incoming_to(attr_id)
            .into_iter()
            .filter(|r| is_attribute_relation(&r.relation_type))
            .filter_map(|r| self.get_person(&r.source))
            .collect()
    }
    
    // ========================================================================
    // Birthday Management
    // ========================================================================
    
    /// Set a person's birthday (solar calendar, YYYY-MM-DD format)
    pub fn set_birthday(&self, person_id: &str, birthday: &str) -> bool {
        // Validate person exists
        if self.get_person(person_id).is_none() {
            return false;
        }
        
        // Validate birthday format
        if !validate_birthday(birthday) {
            return false;
        }
        
        // Add to birthday index
        self.birthday_index_mut().add(person_id, birthday);
        
        // Also store birthday in person's properties
        if let Some(mut person) = self.get_person(person_id) {
            person.properties.insert("birthday".to_string(), serde_json::json!(birthday));
            person.updated_at = chrono_now();
            self.block_on(async {
                let _ = self.router.put_entity(person).await;
            });
        }
        
        true
    }
    
    /// Get a person's birthday
    pub fn get_birthday(&self, person_id: &str) -> Option<String> {
        self.birthday_index().get_person_birthday(person_id)
    }
    
    /// Remove a person's birthday
    pub fn remove_birthday(&self, person_id: &str) -> bool {
        let birthday = match self.get_birthday(person_id) {
            Some(b) => b,
            None => return false,
        };
        
        // Remove from index
        self.birthday_index_mut().remove(person_id, &birthday);
        
        // Remove from person's properties
        if let Some(mut person) = self.get_person(person_id) {
            person.properties.remove("birthday");
            person.updated_at = chrono_now();
            self.block_on(async {
                let _ = self.router.put_entity(person).await;
            });
        }
        
        true
    }
    
    /// Rebuild birthday index from all persons
    pub fn rebuild_birthday_index(&self) {
        let mut index = self.birthday_index_mut();
        
        for person in self.list_persons(None) {
            if let Some(birthday) = person.properties.get("birthday") {
                if let Some(birthday_str) = birthday.as_str() {
                    if validate_birthday(birthday_str) {
                        index.add(&person.id, birthday_str);
                    }
                }
            }
        }
    }
    
    // ========================================================================
    // Low-level Graph Operations (Sync wrappers for compatibility)
    // ========================================================================
    
    /// Get outgoing relations from an entity (sync)
    /// 
    /// This is a direct wrapper around the router's outgoing_from method.
    pub fn outgoing_from(&self, entity_id: &str) -> Vec<Relation> {
        self.router.outgoing_from(entity_id)
    }
    
    /// Get incoming relations to an entity (sync)
    /// 
    /// This is a direct wrapper around the router's incoming_to method.
    pub fn incoming_to(&self, entity_id: &str) -> Vec<Relation> {
        self.router.incoming_to(entity_id)
    }
    
    /// Put a relation (sync, blocking)
    /// 
    /// This is a sync wrapper for low-level relation operations.
    /// Prefer using higher-level methods like add_friend() when possible.
    pub fn put_relation(&self, relation: Relation) {
        self.block_on(async {
            let _ = self.router.put_relation_sync(relation).await;
        })
    }
    
    /// Put an entity (sync, blocking)
    /// 
    /// This is a sync wrapper for low-level entity operations.
    /// Prefer using higher-level methods like add_person() when possible.
    pub fn put_entity(&self, entity: Entity) {
        self.block_on(async {
            let _ = self.router.put_entity_sync(entity).await;
        })
    }
    
    /// Delete an entity by ID (sync, blocking)
    /// 
    /// This is a generic delete method that works for any entity type.
    /// Returns true if the entity was found and deleted.
    pub fn delete_entity(&self, id: &str) -> bool {
        // Check if entity exists first
        if !self.router.contains_entity(id) {
            return false;
        }
        
        self.block_on(async {
            self.router.delete_entity_sync(id).await.is_ok()
        })
    }
    
    /// Delete a relation by ID (sync, blocking)
    pub fn delete_relation(&self, id: &str) {
        // Try to find the relation first to get source hint
        let source_hint = self.router.get_relation(id, None)
            .map(|r| r.source.clone());
        
        self.block_on(async {
            let _ = self.router.delete_relation_sync(id, source_hint.as_deref(), None).await;
        })
    }
    
    /// Check if an entity exists
    pub fn contains_entity(&self, id: &str) -> bool {
        self.router.contains_entity(id)
    }
    
    // ========================================================================
    // Federation Adapter API (async + universal lookup)
    // ========================================================================
    //
    // These methods are designed for the federation layer (GraphStoreAdapter)
    // to avoid block_on nesting when called from an async context.
    // They also provide universal entity access regardless of entity_type.
    
    /// Get any entity by ID regardless of type (uses GlobalGraphIndex for O(1) lookup)
    ///
    /// Unlike get_person/get_tag/get_item/get_attribute which filter by type,
    /// this method returns any entity matching the ID, including custom types
    /// created via addEntity().
    pub fn get_entity_by_id(&self, id: &str) -> Option<Entity> {
        self.router.get_entity(id)
    }
    
    /// List entities by any type (including custom types like "article", "category")
    ///
    /// This is the universal version of list_persons/list_tags/list_items etc.
    /// Works with both built-in types and custom entity types.
    pub fn list_entities_by_type(&self, entity_type: &str) -> Vec<Entity> {
        self.router.find_entities_by_type(entity_type)
    }
    
    /// Get all entity types registered in the global index
    pub fn get_all_entity_types(&self) -> Vec<String> {
        self.router.global_index().get_all_entity_types()
    }
    
    /// List entities by type with pagination (Phase-97)
    ///
    /// Returns a `PaginatedResult<Entity>` containing the requested page and total count.
    /// More efficient than `list_entities_by_type()` for large datasets as it avoids
    /// loading all entities into memory.
    ///
    /// # Arguments
    /// * `entity_type` - The entity type to query
    /// * `offset` - Number of items to skip
    /// * `limit` - Maximum number of items to return
    ///
    /// # Example
    /// ```no_run
    /// # use aifastdb_core::social::store_v2::SocialGraphV2;
    /// # let store = todo!();
    /// let page = store.list_entities_by_type_paginated("person", 0, 100);
    /// println!("Page 1: {} of {} total", page.items.len(), page.total);
    /// if page.has_more {
    ///     let page2 = store.list_entities_by_type_paginated("person", 100, 100);
    /// }
    /// ```
    pub fn list_entities_by_type_paginated(
        &self,
        entity_type: &str,
        offset: usize,
        limit: usize,
    ) -> PaginatedResult<Entity> {
        let (entities, total) = self.router
            .find_entities_by_type_paginated(entity_type, offset, limit);
        PaginatedResult::new(entities, total, offset, limit)
    }
    
    /// Count entities of a specific type without loading data (Phase-97)
    ///
    /// More efficient than `list_entities_by_type().len()` as it only
    /// reads the type index counter, not the actual entity data.
    pub fn count_entities_by_type(&self, entity_type: &str) -> usize {
        self.router.count_entities_by_type(entity_type)
    }
    
    /// List relations with pagination (Phase-97)
    ///
    /// Extends `list_relations` with offset/limit for efficient paging through
    /// large relation sets.
    pub fn list_relations_paginated(
        &self,
        filter: Option<RelationFilter>,
        offset: usize,
        limit: usize,
    ) -> PaginatedResult<Relation> {
        let filter = filter.unwrap_or_default();
        
        // Get all relations from router
        let mut results = self.router.all_relations();
        
        // Apply filters (same logic as list_relations)
        if let Some(ref rel_type) = filter.relation_type {
            results.retain(|r| &r.relation_type == rel_type);
        }
        if let Some(ref source) = filter.source_id {
            results.retain(|r| {
                &r.source == source || (r.bidirectional && &r.target == source)
            });
        }
        if let Some(ref target) = filter.target_id {
            results.retain(|r| {
                &r.target == target || (r.bidirectional && &r.source == target)
            });
        }
        
        let total = results.len();
        let page: Vec<Relation> = results
            .into_iter()
            .skip(offset)
            .take(limit)
            .collect();
        PaginatedResult::new(page, total, offset, limit)
    }
    
    /// Export graph with pagination (Phase-97)
    ///
    /// Returns a paginated slice of graph nodes and their connecting edges.
    /// Only edges where both endpoints are in the current page are included.
    ///
    /// This is much more memory-efficient than `export_graph()` for large graphs
    /// as it avoids loading all nodes into memory at once.
    pub fn export_graph_paginated(
        &self,
        options: ExportGraphPaginatedOptions,
    ) -> PaginatedExportedGraph {
        let offset = options.offset;
        let limit = options.get_limit();
        let include_tags = options.include_tags.unwrap_or(true);
        let include_companies = options.include_companies.unwrap_or(true);
        let include_edge_meta = options.include_edge_meta.unwrap_or(true);
        let include_node_degree = options.include_node_degree.unwrap_or(true);
        
        // Collect all node IDs (lightweight — only IDs, not full entities)
        let mut all_node_ids: Vec<(String, String)> = Vec::new(); // (id, entity_type)
        
        if let Some(ref types) = options.entity_types {
            // Specific types requested
            for entity_type in types {
                let ids = self.router.global_index().get_entities_by_type(entity_type);
                for id in ids {
                    all_node_ids.push((id, entity_type.clone()));
                }
            }
        } else {
            // All types
            let persons = self.list_persons(None);
            for p in &persons {
                all_node_ids.push((p.id.clone(), "person".to_string()));
            }
            
            let tags = self.list_tags(None);
            for t in &tags {
                let is_company = Self::is_company_entity(t);
                if is_company && !include_companies {
                    continue;
                }
                if !is_company && !include_tags {
                    continue;
                }
                all_node_ids.push((t.id.clone(), t.entity_type.clone()));
            }
            
            // Include custom entity types
            for entity_type in self.get_all_entity_types() {
                if entity_type == "person" || entity_type == "tag" {
                    continue; // already included above
                }
                let ids = self.router.global_index().get_entities_by_type(&entity_type);
                for id in ids {
                    all_node_ids.push((id, entity_type.clone()));
                }
            }
        }
        
        let total_nodes = all_node_ids.len();
        
        // Paginate node IDs
        let page_node_ids: Vec<(String, String)> = all_node_ids
            .into_iter()
            .skip(offset)
            .take(limit)
            .collect();
        
        // Fetch full entities for this page only
        let mut nodes = Vec::with_capacity(page_node_ids.len());
        let mut node_id_set = std::collections::HashSet::new();
        
        for (id, _entity_type) in &page_node_ids {
            if let Some(entity) = self.router.get_entity(id) {
                node_id_set.insert(id.clone());
                let group = if entity.entity_type == "person" {
                    NodeGroup::Person
                } else if Self::is_company_entity(&entity) {
                    NodeGroup::Company
                } else {
                    NodeGroup::Tag
                };
                nodes.push(Self::entity_to_graph_node(&entity, group));
            }
        }
        
        // Get all relations and filter to page edges
        let all_relations = self.router.all_relations();
        let total_edges = all_relations.len();
        let mut edges = Vec::new();
        let mut seen_edges = std::collections::HashSet::new();
        
        for rel in all_relations {
            if !node_id_set.contains(&rel.source) || !node_id_set.contains(&rel.target) {
                continue;
            }
            if let Some(ref types) = options.relation_types {
                if !types.contains(&rel.relation_type) {
                    continue;
                }
            }
            let edge_key = Self::edge_key(&rel.source, &rel.target);
            if seen_edges.contains(&edge_key) {
                continue;
            }
            seen_edges.insert(edge_key);
            
            let mut edge = GraphEdge::new(&rel.id, &rel.source, &rel.target, &rel.relation_type)
                .with_weight(rel.weight);
            if include_edge_meta && !rel.metadata.is_empty() {
                edge = edge.with_data(rel.metadata);
            }
            edges.push(edge);
        }
        
        // Compute degree if requested
        if include_node_degree {
            let mut degree_map: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
            for edge in &edges {
                *degree_map.entry(edge.from.clone()).or_insert(0) += 1;
                *degree_map.entry(edge.to.clone()).or_insert(0) += 1;
            }
            for node in &mut nodes {
                node.degree = Some(*degree_map.get(&node.id).unwrap_or(&0));
            }
        }
        
        let stats = GraphStats::new(
            nodes.iter().filter(|n| n.group == NodeGroup::Person).count(),
            nodes.iter().filter(|n| n.group == NodeGroup::Company).count(),
            nodes.iter().filter(|n| n.group == NodeGroup::Tag).count(),
            edges.len(),
        );
        
        PaginatedExportedGraph {
            nodes,
            edges,
            stats,
            total_nodes,
            total_edges,
            offset,
            limit,
            has_more: offset + limit < total_nodes,
        }
    }
    
    /// Export graph as compact binary format (Phase-98)
    ///
    /// Returns a `CompactGraphExport` containing a raw binary buffer designed
    /// for zero-copy consumption as TypedArrays in JavaScript/TypeScript.
    ///
    /// The binary format avoids JSON serialization overhead, reducing:
    /// - Serialization time by ~5-10x
    /// - Transfer size by ~3-5x
    /// - Parse time on the client to near-zero (direct ArrayBuffer view)
    ///
    /// See `CompactGraphExport` for the binary format specification.
    pub fn export_graph_compact(&self, options: Option<ExportGraphOptions>) -> CompactGraphExport {
        // Use paginated export (which handles custom entity types via GlobalIndex)
        // with a large limit to get all entities in one page.
        let paginated_opts = ExportGraphPaginatedOptions {
            offset: 0,
            limit: options.as_ref().and_then(|o| o.max_nodes).unwrap_or(1_000_000),
            entity_types: None, // all types
            relation_types: options.as_ref().and_then(|o| o.relation_types.clone()),
            include_tags: options.as_ref().and_then(|o| o.include_tags),
            include_companies: options.as_ref().and_then(|o| o.include_companies),
            include_node_degree: options.as_ref().and_then(|o| o.include_node_degree),
            include_edge_meta: options.as_ref().and_then(|o| o.include_edge_meta),
        };
        let paginated = self.export_graph_paginated(paginated_opts);
        
        // Convert PaginatedExportedGraph into ExportedGraph for binary encoding
        let graph = ExportedGraph {
            nodes: paginated.nodes,
            edges: paginated.edges,
            stats: paginated.stats,
        };
        
        let node_count = graph.nodes.len() as u32;
        let edge_count = graph.edges.len() as u32;
        
        // Build node ID -> index mapping
        let mut node_id_to_idx: std::collections::HashMap<&str, u32> = std::collections::HashMap::new();
        for (i, node) in graph.nodes.iter().enumerate() {
            node_id_to_idx.insert(&node.id, i as u32);
        }
        
        // Estimate buffer size: header(16) + string table + edge table(12*E)
        let estimated_size = 16
            + graph.nodes.iter().map(|n| 2 + n.id.len() + 1 + 2 + n.label.len() + 4).sum::<usize>()
            + (edge_count as usize) * 12;
        let mut buf = Vec::with_capacity(estimated_size);
        
        // Header
        buf.extend_from_slice(&0x41494647u32.to_le_bytes()); // magic "AIFG"
        buf.extend_from_slice(&1u32.to_le_bytes());          // version
        buf.extend_from_slice(&node_count.to_le_bytes());
        buf.extend_from_slice(&edge_count.to_le_bytes());
        
        // String table (per node)
        for node in &graph.nodes {
            let id_bytes = node.id.as_bytes();
            buf.extend_from_slice(&(id_bytes.len() as u16).to_le_bytes());
            buf.extend_from_slice(id_bytes);
            
            let group_byte: u8 = match node.group {
                NodeGroup::Person => 0,
                NodeGroup::Company => 1,
                NodeGroup::Tag => 2,
            };
            buf.push(group_byte);
            
            let label_bytes = node.label.as_bytes();
            buf.extend_from_slice(&(label_bytes.len() as u16).to_le_bytes());
            buf.extend_from_slice(label_bytes);
            
            let degree = node.degree.unwrap_or(0) as u32;
            buf.extend_from_slice(&degree.to_le_bytes());
        }
        
        // Edge table (fixed 12 bytes per edge)
        for edge in &graph.edges {
            let from_idx = node_id_to_idx.get(edge.from.as_str()).copied().unwrap_or(u32::MAX);
            let to_idx = node_id_to_idx.get(edge.to.as_str()).copied().unwrap_or(u32::MAX);
            let weight_x1000 = (edge.weight * 1000.0) as u32;
            
            buf.extend_from_slice(&from_idx.to_le_bytes());
            buf.extend_from_slice(&to_idx.to_le_bytes());
            buf.extend_from_slice(&weight_x1000.to_le_bytes());
        }
        
        CompactGraphExport {
            buffer: buf,
            node_count,
            edge_count,
        }
    }
    
    /// Get entity group summary (Phase-98)
    ///
    /// Returns server-side aggregation of entities grouped by type,
    /// with count and sample IDs for each group. This is much cheaper
    /// than loading all entities — ideal for low-zoom-level cluster views.
    pub fn group_entities_summary(&self) -> EntityGroupAggregation {
        let global_index = self.router.global_index();
        let entity_types = global_index.get_all_entity_types();
        
        let mut groups = Vec::with_capacity(entity_types.len());
        let mut total_entities = 0;
        
        for entity_type in entity_types {
            let count = global_index.count_entities_by_type(&entity_type);
            total_entities += count;
            
            // Get up to 5 sample IDs
            let (sample_ids, _) = global_index
                .get_entities_by_type_paginated(&entity_type, 0, 5);
            
            groups.push(EntityGroupSummary {
                entity_type,
                count,
                sample_ids,
            });
        }
        
        // Sort by count descending
        groups.sort_by(|a, b| b.count.cmp(&a.count));
        
        let total_relations = self.router.relation_count();
        
        EntityGroupAggregation {
            groups,
            total_entities,
            total_relations,
        }
    }
    
    /// Put an entity (async, non-blocking)
    ///
    /// For use by federation adapter or any async caller.
    /// Avoids block_on nesting that would panic/deadlock in async contexts.
    pub async fn put_entity_async(&self, entity: Entity) -> Result<(), GraphStoreError> {
        self.router.put_entity_sync(entity).await.map(|_| ())
    }
    
    /// Delete an entity by ID (async, non-blocking)
    ///
    /// Returns true if the entity was found and deleted.
    pub async fn delete_entity_async(&self, id: &str) -> Result<bool, GraphStoreError> {
        if !self.router.contains_entity(id) {
            return Ok(false);
        }
        self.router.delete_entity_sync(id).await.map(|_| true)
    }
    
    /// Put a relation (async, non-blocking)
    pub async fn put_relation_async(&self, relation: Relation) -> Result<(), GraphStoreError> {
        self.router.put_relation_sync(relation).await.map(|_| ())
    }
    
    /// Delete a relation by ID (async, non-blocking)
    pub async fn delete_relation_async(&self, id: &str) -> Result<(), GraphStoreError> {
        let source_hint = self.router.get_relation(id, None)
            .map(|r| r.source.clone());
        self.router.delete_relation_sync(id, source_hint.as_deref(), None).await.map(|_| ())
    }
    
    /// Flush all pending writes (async, non-blocking)
    pub async fn flush_async(&self) -> Result<(), GraphStoreError> {
        self.router.flush().await
    }
    
    /// Shutdown the store (async, non-blocking)
    pub async fn shutdown_async(&self) -> Result<(), GraphStoreError> {
        self.router.shutdown().await
    }
    
    // ========================================================================
    // Vector Search API
    // ========================================================================
    
    /// Index an entity with an embedding vector for vector similarity search
    /// 
    /// The entity must already exist in the graph store. The entity.id is used
    /// as the bridging key between the graph store and the vector index.
    /// 
    /// # Arguments
    /// * `entity_id` - ID of an existing entity
    /// * `embedding` - Vector embedding (dimension must match VectorSearchConfig.dimension)
    /// 
    /// # Errors
    /// Returns error if vector search is not enabled, entity doesn't exist,
    /// or dimension mismatch.
    pub fn index_entity(&self, entity_id: &str, embedding: Vec<f32>) -> Result<(), GraphStoreError> {
        let index = self.vector_index.as_ref()
            .ok_or_else(|| GraphStoreError::IoError("Vector search not enabled. Configure with_vector_search() first.".into()))?;
        
        // Verify entity exists in graph store
        if self.router.get_entity(entity_id).is_none() {
            return Err(GraphStoreError::EntityNotFound(entity_id.to_string()));
        }
        
        // If entity already has a vector, skip insert (HNSW soft-delete keeps
        // the id_map entry, so re-inserting the same ID would fail with DuplicateId).
        // For true re-index, the caller should remove_entity_vector() first,
        // then call index_entity() again.
        // Here we handle it gracefully: try insert, if DuplicateId just log and continue.
        self.block_on(async {
            match index.insert(entity_id.to_string(), embedding.clone()).await {
                Ok(_) => Ok(()),
                Err(e) => {
                    let err_msg = format!("{}", e);
                    if err_msg.contains("Duplicate") || err_msg.contains("duplicate") {
                        // Already indexed — this is acceptable for re-index scenario
                        tracing::debug!("Entity {} already in vector index, skipping re-insert", entity_id);
                        Ok(())
                    } else {
                        Err(GraphStoreError::IoError(format!("Vector insert failed: {}", e)))
                    }
                }
            }
        })?;
        
        // Write to vector WAL for crash recovery
        if let Some(ref wal_mutex) = self.vector_wal {
            wal_mutex.lock().append_index(entity_id, &embedding)
                .map_err(|e| GraphStoreError::IoError(format!("Vector WAL write failed: {}", e)))?;
        }
        
        Ok(())
    }
    
    /// Index an entity with an embedding vector (async version)
    pub async fn index_entity_async(&self, entity_id: &str, embedding: Vec<f32>) -> Result<(), GraphStoreError> {
        let index = self.vector_index.as_ref()
            .ok_or_else(|| GraphStoreError::IoError("Vector search not enabled".into()))?;
        
        if self.router.get_entity(entity_id).is_none() {
            return Err(GraphStoreError::EntityNotFound(entity_id.to_string()));
        }
        
        // Try insert; handle DuplicateId gracefully (HNSW soft-delete keeps id_map)
        match index.insert(entity_id.to_string(), embedding.clone()).await {
            Ok(_) => {},
            Err(e) => {
                let err_msg = format!("{}", e);
                if err_msg.contains("Duplicate") || err_msg.contains("duplicate") {
                    tracing::debug!("Entity {} already in vector index, skipping re-insert", entity_id);
                } else {
                    return Err(GraphStoreError::IoError(format!("Vector insert failed: {}", e)));
                }
            }
        }
        
        if let Some(ref wal_mutex) = self.vector_wal {
            wal_mutex.lock().append_index(entity_id, &embedding)
                .map_err(|e| GraphStoreError::IoError(format!("Vector WAL write failed: {}", e)))?;
        }
        
        Ok(())
    }
    
    /// Remove an entity's vector from the search index
    /// 
    /// Does not remove the entity from the graph store, only from the vector index.
    pub fn remove_entity_vector(&self, entity_id: &str) -> Result<(), GraphStoreError> {
        let index = self.vector_index.as_ref()
            .ok_or_else(|| GraphStoreError::IoError("Vector search not enabled".into()))?;
        
        self.block_on(async {
            index.delete(entity_id).await
                .map_err(|e| GraphStoreError::IoError(format!("Vector delete failed: {}", e)))?;
            Ok::<(), GraphStoreError>(())
        })?;
        
        if let Some(ref wal_mutex) = self.vector_wal {
            wal_mutex.lock().append_remove(entity_id)
                .map_err(|e| GraphStoreError::IoError(format!("Vector WAL write failed: {}", e)))?;
        }
        
        Ok(())
    }
    
    /// Search entities by vector similarity
    /// 
    /// Returns entities ranked by similarity to the query vector.
    /// Optionally filters by entity_type.
    /// 
    /// # Arguments
    /// * `query` - Query embedding vector
    /// * `top_k` - Maximum number of results to return
    /// * `entity_type_filter` - Optional entity type to filter results
    /// 
    /// # Returns
    /// Vec of VectorEntitySearchResult sorted by descending similarity score
    pub fn search_entities_by_vector(
        &self,
        query: &[f32],
        top_k: usize,
        entity_type_filter: Option<&str>,
    ) -> Result<Vec<VectorEntitySearchResult>, GraphStoreError> {
        let index = self.vector_index.as_ref()
            .ok_or_else(|| GraphStoreError::IoError("Vector search not enabled".into()))?;
        
        // Over-fetch to compensate for filtered-out results
        let fetch_k = if entity_type_filter.is_some() { top_k * 3 } else { top_k };
        let hits = index.search(query, fetch_k);
        
        let mut results = Vec::with_capacity(top_k);
        for hit in hits {
            if let Some(entity) = self.router.get_entity(&hit.id) {
                // Apply entity type filter
                if let Some(type_filter) = entity_type_filter {
                    if entity.entity_type != type_filter {
                        continue;
                    }
                }
                results.push(VectorEntitySearchResult {
                    entity_id: hit.id.clone(),
                    score: 1.0 - hit.distance, // Convert distance to similarity
                    entity,
                });
                if results.len() >= top_k {
                    break;
                }
            }
        }
        
        Ok(results)
    }
    
    /// Hybrid search combining vector similarity and text attribute matching
    /// 
    /// Uses Reciprocal Rank Fusion (RRF) to merge results from:
    /// 1. Vector similarity search (if query_embedding is provided)
    /// 2. Text attribute matching (if text_query is provided)
    /// 
    /// At least one of query_embedding or text_query must be provided.
    /// 
    /// # Arguments
    /// * `query_embedding` - Optional embedding vector for semantic search
    /// * `text_query` - Optional text for keyword matching against entity names/properties
    /// * `entity_type_filter` - Optional entity type to filter results
    /// * `top_k` - Maximum number of results
    /// 
    /// # Returns
    /// Vec of VectorEntitySearchResult sorted by RRF score
    pub fn hybrid_search(
        &self,
        query_embedding: Option<&[f32]>,
        text_query: Option<&str>,
        entity_type_filter: Option<&str>,
        top_k: usize,
    ) -> Result<Vec<VectorEntitySearchResult>, GraphStoreError> {
        if query_embedding.is_none() && text_query.is_none() {
            return Ok(Vec::new());
        }
        
        let mut vector_results: Vec<(String, f32)> = Vec::new();
        let mut text_results: Vec<(String, f32)> = Vec::new();
        
        // 1. Vector similarity search
        if let Some(embedding) = query_embedding {
            if let Some(ref index) = self.vector_index {
                let hits = index.search(embedding, top_k * 3);
                for hit in hits {
                    // Pre-filter by entity type if specified
                    if let Some(type_filter) = entity_type_filter {
                        if let Some(entity) = self.router.get_entity(&hit.id) {
                            if entity.entity_type != type_filter {
                                continue;
                            }
                        } else {
                            continue;
                        }
                    }
                    vector_results.push((hit.id.clone(), 1.0 - hit.distance));
                }
            }
        }
        
        // 2. Text attribute search
        if let Some(text) = text_query {
            let text_lower = text.to_lowercase();
            let entities = if let Some(type_filter) = entity_type_filter {
                self.router.find_entities_by_type(type_filter)
            } else {
                self.router.all_entities()
            };
            
            for entity in entities {
                let mut score = 0.0f32;
                
                // Match name
                if entity.name.to_lowercase().contains(&text_lower) {
                    score += 0.8;
                }
                
                // Match properties
                for (_, value) in &entity.properties {
                    if let Some(s) = value.as_str() {
                        if s.to_lowercase().contains(&text_lower) {
                            score += 0.5;
                            break;
                        }
                    }
                }
                
                if score > 0.0 {
                    text_results.push((entity.id.clone(), score.min(1.0)));
                }
            }
            
            // Sort text results by score descending
            text_results.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
        }
        
        // 3. RRF (Reciprocal Rank Fusion) to merge ranked lists
        let rrf_k = 60.0f32; // Standard RRF constant
        let mut rrf_scores: HashMap<String, f32> = HashMap::new();
        
        for (rank, (id, _)) in vector_results.iter().enumerate() {
            *rrf_scores.entry(id.clone()).or_insert(0.0) += 1.0 / (rrf_k + rank as f32 + 1.0);
        }
        for (rank, (id, _)) in text_results.iter().enumerate() {
            *rrf_scores.entry(id.clone()).or_insert(0.0) += 1.0 / (rrf_k + rank as f32 + 1.0);
        }
        
        // Sort by RRF score
        let mut scored: Vec<(String, f32)> = rrf_scores.into_iter().collect();
        scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
        scored.truncate(top_k);
        
        // 4. Resolve full entities
        let mut results = Vec::with_capacity(scored.len());
        for (id, score) in scored {
            if let Some(entity) = self.router.get_entity(&id) {
                results.push(VectorEntitySearchResult {
                    entity_id: id,
                    score,
                    entity,
                });
            }
        }
        
        Ok(results)
    }
    
    // ========================================================================
    // Utility
    // ========================================================================
    
    /// Flush all pending writes
    pub fn flush(&self) -> std::io::Result<()> {
        self.block_on(async {
            self.router.flush().await.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))
        })?;
        
        // Also sync vector WAL
        if let Some(ref wal_mutex) = self.vector_wal {
            wal_mutex.lock().sync()?;
        }
        
        Ok(())
    }
    
    /// Sync WAL to disk
    pub fn sync(&self) -> std::io::Result<()> {
        self.router.sync_all()
    }
    
    /// Save (alias for sync)
    pub fn save(&self) -> std::io::Result<()> {
        self.sync()
    }
    
    /// Shutdown the store
    pub fn shutdown(&self) -> std::io::Result<()> {
        self.block_on(async {
            self.router.shutdown().await.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))
        })
    }
    
    // ========================================================================
    // Performance Monitoring API
    // ========================================================================
    
    /// Get write metrics
    pub fn metrics(&self) -> &WriteMetrics {
        self.router.metrics()
    }
    
    /// Get metrics snapshot
    pub fn metrics_snapshot(&self) -> MetricsSnapshot {
        self.router.metrics().snapshot()
    }
    
    /// Get global index statistics
    pub fn global_index_stats(&self) -> GlobalIndexStats {
        self.router.global_index_stats()
    }
    
    /// Get total entity count
    pub fn entity_count(&self) -> usize {
        self.router.entity_count()
    }
    
    /// Get total relation count
    pub fn relation_count(&self) -> usize {
        self.router.relation_count()
    }
    
    /// Get shard count
    pub fn shard_count(&self) -> usize {
        self.router.shard_count()
    }
    
    /// Get queue fill ratio
    pub fn queue_fill_ratio(&self) -> f64 {
        self.router.get_total_fill_ratio()
    }
    
    /// Get cross-shard relation count
    pub fn cross_shard_relation_count(&self) -> usize {
        self.router.cross_shard_relation_count()
    }
    
    /// Check if WAL is enabled
    pub fn is_wal_enabled(&self) -> bool {
        self.router.is_wal_enabled()
    }
    
    /// Get performance summary
    pub fn performance_summary(&self) -> PerformanceSummary {
        let metrics = self.metrics_snapshot();
        let index_stats = self.global_index_stats();
        
        PerformanceSummary {
            total_requests: metrics.total_requests,
            successful_writes: metrics.successful_writes,
            failed_writes: metrics.failed_writes,
            rejected_requests: metrics.rejected_requests,
            batches_flushed: metrics.batches_flushed,
            batch_documents: metrics.batch_documents,
            avg_queue_time_us: metrics.avg_queue_time_us,
            avg_write_time_us: metrics.avg_write_time_us,
            entity_count: self.entity_count(),
            relation_count: self.relation_count(),
            cross_shard_count: index_stats.cross_shard_count,
            shard_count: self.shard_count(),
            queue_fill_ratio: self.queue_fill_ratio(),
        }
    }
}

/// Performance summary for monitoring
#[derive(Debug, Clone)]
pub struct PerformanceSummary {
    /// Total requests received
    pub total_requests: u64,
    /// Successful writes
    pub successful_writes: u64,
    /// Failed writes
    pub failed_writes: u64,
    /// Rejected requests (backpressure)
    pub rejected_requests: u64,
    /// Total batches flushed
    pub batches_flushed: u64,
    /// Total documents in batches
    pub batch_documents: u64,
    /// Average queue time in microseconds
    pub avg_queue_time_us: f64,
    /// Average write time in microseconds
    pub avg_write_time_us: f64,
    /// Total entity count
    pub entity_count: usize,
    /// Total relation count
    pub relation_count: usize,
    /// Cross-shard relation count
    pub cross_shard_count: usize,
    /// Number of shards
    pub shard_count: usize,
    /// Queue fill ratio
    pub queue_fill_ratio: f64,
}

impl PerformanceSummary {
    /// Calculate success rate as a percentage
    pub fn success_rate(&self) -> f64 {
        if self.total_requests == 0 {
            100.0
        } else {
            (self.successful_writes as f64 / self.total_requests as f64) * 100.0
        }
    }
    
    /// Calculate throughput (writes per second) given elapsed time
    pub fn throughput(&self, elapsed_secs: f64) -> f64 {
        if elapsed_secs <= 0.0 {
            0.0
        } else {
            self.successful_writes as f64 / elapsed_secs
        }
    }
}

// ============================================================================
// Helper Functions
// ============================================================================

fn chrono_now() -> i64 {
    use std::time::{SystemTime, UNIX_EPOCH};
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_millis() as i64
}

fn is_social_relation(rel_type: &str) -> bool {
    matches!(rel_type,
        relation_types::FRIEND |
        relation_types::KNOWS |
        relation_types::COLLEAGUE |
        relation_types::CLASSMATE |
        relation_types::FAMILY |
        relation_types::MENTOR
    )
}

fn is_tag_type(entity_type: &str) -> bool {
    matches!(entity_type,
        entity_types::TAG |
        entity_types::COMPANY |
        entity_types::ORGANIZATION |
        entity_types::SKILL |
        entity_types::INTEREST |
        entity_types::COMMUNITY
    )
}

fn is_item_type(entity_type: &str) -> bool {
    matches!(entity_type,
        entity_types::ITEM |
        entity_types::VEHICLE |
        entity_types::DEVICE |
        entity_types::EQUIPMENT
    )
}

fn is_item_relation(rel_type: &str) -> bool {
    matches!(rel_type,
        relation_types::OWNS |
        relation_types::USED_TO_OWN |
        relation_types::WANTS
    )
}

fn is_attribute_type(entity_type: &str) -> bool {
    matches!(entity_type,
        entity_types::ATTRIBUTE |
        entity_types::ZODIAC |
        entity_types::SCHOOL |
        entity_types::CITY |
        entity_types::HOBBY |
        entity_types::AGE_GROUP |
        entity_types::INDUSTRY
    )
}

fn is_attribute_relation(rel_type: &str) -> bool {
    matches!(rel_type,
        relation_types::HAS_ATTRIBUTE |
        relation_types::GRADUATED_FROM |
        relation_types::LIVES_IN |
        relation_types::BORN_IN |
        relation_types::INTERESTED_IN
    )
}

/// Check if a relation type represents a person-to-tag relationship
/// 
/// This includes:
/// - `has_tags` - person has tag (new default)
/// - `belongs_to` - legacy tag membership (for backward compatibility)
/// - `tagged` - explicit tagging (not in relation_types constants, but used in data)
/// - `work_for` - employment at a company
/// - `member_of` - membership in organization
fn is_tag_relation(rel_type: &str) -> bool {
    matches!(rel_type,
        relation_types::HAS_TAGS |    // "has_tags" (new default)
        relation_types::BELONGS_TO |  // "belongs_to" (legacy, for backward compatibility)
        relation_types::WORK_FOR |    // "work_for"
        relation_types::MEMBER_OF |   // "member_of"
        "tagged"                      // explicit tagging (common in data)
    )
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::tempdir;
    
    fn create_test_store() -> SocialStoreV2 {
        let dir = tempdir().unwrap();
        let config = SocialStoreV2Config::new(dir.path())
            .with_shard_count(4)
            .with_wal(false);
        SocialStoreV2::new(config)
    }
    
    #[test]
    fn test_person_crud() {
        let store = create_test_store();
        
        // Create
        let person = store.add_person(PersonInput {
            id: None,
            name: "Alice".to_string(),
            avatar: Some("avatar.jpg".to_string()),
            description: Some("Engineer".to_string()),
            phone: None,
            email: Some("alice@example.com".to_string()),
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        assert_eq!(person.name, "Alice");
        
        // Read
        let retrieved = store.get_person(&person.id).unwrap();
        assert_eq!(retrieved.name, "Alice");
        
        // Update
        let mut updates = BTreeMap::new();
        updates.insert("name".to_string(), serde_json::json!("Alice Smith"));
        let updated = store.update_person(&person.id, updates).unwrap();
        assert_eq!(updated.name, "Alice Smith");
        
        // List
        let persons = store.list_persons(None);
        assert_eq!(persons.len(), 1);
        
        // Delete
        assert!(store.delete_person(&person.id));
        assert!(store.get_person(&person.id).is_none());
    }
    
    #[test]
    fn test_friend_relationship() {
        let store = create_test_store();
        
        let alice = store.add_person(PersonInput {
            id: None,
            name: "Alice".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        let bob = store.add_person(PersonInput {
            id: None,
            name: "Bob".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        // Add friend
        store.add_friend(&alice.id, &bob.id, FriendConfig::default());
        
        // Check friendship
        assert!(store.are_friends(&alice.id, &bob.id));
        assert!(store.are_friends(&bob.id, &alice.id)); // bidirectional
        
        // Get friends
        let alice_friends = store.get_friends(&alice.id);
        assert_eq!(alice_friends.len(), 1);
        assert_eq!(alice_friends[0].id, bob.id);
        
        // Remove friend
        assert!(store.remove_friend(&alice.id, &bob.id));
        assert!(!store.are_friends(&alice.id, &bob.id));
    }
    
    #[test]
    fn test_tag_management() {
        let store = create_test_store();
        
        let person = store.add_person(PersonInput {
            id: None,
            name: "Alice".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        let tag = store.add_tag(TagInput {
            id: None,
            name: "Engineers".to_string(),
            tag_type: "organization".to_string(),
            description: None,
            color: Some("#3498db".to_string()),
            parent_tags: vec![],
            properties: BTreeMap::new(),
        });
        
        // Tag person
        store.tag_person(&person.id, &tag.id);
        
        // Get person tags
        let tags = store.get_person_tags(&person.id);
        assert_eq!(tags.len(), 1);
        assert_eq!(tags[0].name, "Engineers");
        
        // Get tag members
        let members = store.get_tag_members(&tag.id);
        assert_eq!(members.len(), 1);
        assert_eq!(members[0].id, person.id);
        
        // Untag
        assert!(store.untag_person(&person.id, &tag.id));
        assert!(store.get_person_tags(&person.id).is_empty());
    }
    
    #[test]
    fn test_item_management() {
        let store = create_test_store();
        
        let person = store.add_person(PersonInput {
            id: None,
            name: "Alice".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        let item = store.add_item(ItemInput {
            id: None,
            name: "Tesla Model Y".to_string(),
            category: "vehicle".to_string(),
            brand: Some("Tesla".to_string()),
            model: Some("Model Y".to_string()),
            properties: BTreeMap::new(),
        });
        
        // Add item to person
        store.add_item_to_person(&person.id, &item.id, None);
        
        // Get person items
        let items = store.get_person_items(&person.id);
        assert_eq!(items.len(), 1);
        assert_eq!(items[0].name, "Tesla Model Y");
        
        // Get item owners
        let owners = store.get_item_owners(&item.id);
        assert_eq!(owners.len(), 1);
        assert_eq!(owners[0].id, person.id);
    }
    
    #[test]
    fn test_attribute_management() {
        let store = create_test_store();
        
        let person = store.add_person(PersonInput {
            id: None,
            name: "Alice".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        // Find or create attribute
        let zodiac = store.find_or_create_attribute("zodiac", "Leo");
        
        // Set person attribute
        store.set_person_attribute(&person.id, &zodiac.id, None);
        
        // Get person attributes
        let attrs = store.get_person_attributes(&person.id);
        assert_eq!(attrs.len(), 1);
        assert_eq!(attrs[0].name, "Leo");
        
        // Get persons by attribute
        let persons = store.get_persons_by_attribute(&zodiac.id);
        assert_eq!(persons.len(), 1);
        assert_eq!(persons[0].id, person.id);
        
        // Find or create same attribute should return existing
        let zodiac2 = store.find_or_create_attribute("zodiac", "Leo");
        assert_eq!(zodiac.id, zodiac2.id);
    }
    
    #[test]
    fn test_birthday_management() {
        let store = create_test_store();
        
        let person = store.add_person(PersonInput {
            id: None,
            name: "Alice".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        // Set birthday
        assert!(store.set_birthday(&person.id, "1990-05-15"));
        
        // Get birthday
        let birthday = store.get_birthday(&person.id);
        assert_eq!(birthday, Some("1990-05-15".to_string()));
        
        // Invalid birthday
        assert!(!store.set_birthday(&person.id, "invalid-date"));
        
        // Remove birthday
        assert!(store.remove_birthday(&person.id));
        assert!(store.get_birthday(&person.id).is_none());
    }
    
    #[test]
    fn test_list_tags_all() {
        let store = create_test_store();
        
        // Add various tag types
        store.add_tag(TagInput {
            id: None,
            name: "Tech".to_string(),
            tag_type: "tag".to_string(),
            description: None,
            color: None,
            parent_tags: vec![],
            properties: BTreeMap::new(),
        });
        
        store.add_tag(TagInput {
            id: None,
            name: "Google".to_string(),
            tag_type: "company".to_string(),
            description: Some("Tech company".to_string()),
            color: None,
            parent_tags: vec![],
            properties: BTreeMap::new(),
        });
        
        store.add_tag(TagInput {
            id: None,
            name: "Rust Guild".to_string(),
            tag_type: "organization".to_string(),
            description: None,
            color: None,
            parent_tags: vec![],
            properties: BTreeMap::new(),
        });
        
        store.add_tag(TagInput {
            id: None,
            name: "Programming".to_string(),
            tag_type: "skill".to_string(),
            description: None,
            color: None,
            parent_tags: vec![],
            properties: BTreeMap::new(),
        });
        
        // List all tags
        let all_tags = store.list_tags(None);
        assert_eq!(all_tags.len(), 4);
    }
    
    #[test]
    fn test_list_tags_filter_by_type() {
        let store = create_test_store();
        
        // Add tags of different types
        store.add_tag(TagInput {
            id: None,
            name: "Google".to_string(),
            tag_type: "company".to_string(),
            description: None,
            color: None,
            parent_tags: vec![],
            properties: BTreeMap::new(),
        });
        
        store.add_tag(TagInput {
            id: None,
            name: "Apple".to_string(),
            tag_type: "company".to_string(),
            description: None,
            color: None,
            parent_tags: vec![],
            properties: BTreeMap::new(),
        });
        
        store.add_tag(TagInput {
            id: None,
            name: "Tech".to_string(),
            tag_type: "tag".to_string(),
            description: None,
            color: None,
            parent_tags: vec![],
            properties: BTreeMap::new(),
        });
        
        // Filter by company type
        let companies = store.list_tags(Some(TagFilter {
            tag_type: Some("company".to_string()),
            name: None,
            limit: None,
        }));
        assert_eq!(companies.len(), 2);
        
        // Filter by tag type
        let tags = store.list_tags(Some(TagFilter {
            tag_type: Some("tag".to_string()),
            name: None,
            limit: None,
        }));
        assert_eq!(tags.len(), 1);
    }
    
    #[test]
    fn test_list_tags_filter_by_name() {
        let store = create_test_store();
        
        store.add_tag(TagInput {
            id: None,
            name: "TechCorp".to_string(),
            tag_type: "company".to_string(),
            description: None,
            color: None,
            parent_tags: vec![],
            properties: BTreeMap::new(),
        });
        
        store.add_tag(TagInput {
            id: None,
            name: "Technology".to_string(),
            tag_type: "tag".to_string(),
            description: None,
            color: None,
            parent_tags: vec![],
            properties: BTreeMap::new(),
        });
        
        store.add_tag(TagInput {
            id: None,
            name: "Finance".to_string(),
            tag_type: "tag".to_string(),
            description: None,
            color: None,
            parent_tags: vec![],
            properties: BTreeMap::new(),
        });
        
        // Filter by name (case-insensitive partial match)
        let tech_tags = store.list_tags(Some(TagFilter {
            tag_type: None,
            name: Some("tech".to_string()),
            limit: None,
        }));
        assert_eq!(tech_tags.len(), 2);
        
        // Filter by name with limit
        let limited = store.list_tags(Some(TagFilter {
            tag_type: None,
            name: Some("tech".to_string()),
            limit: Some(1),
        }));
        assert_eq!(limited.len(), 1);
    }
    
    #[test]
    fn test_list_tags_empty() {
        let store = create_test_store();
        
        // No tags added
        let tags = store.list_tags(None);
        assert!(tags.is_empty());
    }
    
    // ========================================================================
    // list_relations Tests
    // ========================================================================
    
    #[test]
    fn test_list_relations_all() {
        let store = create_test_store();
        
        // Add persons
        let alice = store.add_person(PersonInput {
            id: None,
            name: "Alice".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        let bob = store.add_person(PersonInput {
            id: None,
            name: "Bob".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        let charlie = store.add_person(PersonInput {
            id: None,
            name: "Charlie".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        // Add friend relationships
        store.add_friend(&alice.id, &bob.id, Default::default());
        store.add_friend(&bob.id, &charlie.id, Default::default());
        
        // Add a tag and tag person
        let tech_tag = store.add_tag(TagInput {
            id: None,
            name: "Tech".to_string(),
            tag_type: "tag".to_string(),
            description: None,
            color: None,
            parent_tags: vec![],
            properties: BTreeMap::new(),
        });
        store.tag_person(&alice.id, &tech_tag.id);
        
        // List all relations
        let all_relations = store.list_relations(None);
        // Should have friend relations + tag relation
        assert!(all_relations.len() >= 3);
    }
    
    #[test]
    fn test_list_relations_filter_by_type() {
        let store = create_test_store();
        
        // Add persons
        let alice = store.add_person(PersonInput {
            id: None,
            name: "Alice".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        let bob = store.add_person(PersonInput {
            id: None,
            name: "Bob".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        // Add friend with specific type
        store.add_friend(&alice.id, &bob.id, FriendConfig {
            relation_type: "colleague".to_string(),
            ..Default::default()
        });
        
        // Add another friend with different type
        let charlie = store.add_person(PersonInput {
            id: None,
            name: "Charlie".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        store.add_friend(&alice.id, &charlie.id, FriendConfig {
            relation_type: "friend".to_string(),
            ..Default::default()
        });
        
        // Filter by colleague
        let colleague_relations = store.list_relations(Some(RelationFilter {
            relation_type: Some("colleague".to_string()),
            source_id: None,
            target_id: None,
            limit: None,
        }));
        assert_eq!(colleague_relations.len(), 1);
        
        // Filter by friend
        let friend_relations = store.list_relations(Some(RelationFilter {
            relation_type: Some("friend".to_string()),
            source_id: None,
            target_id: None,
            limit: None,
        }));
        assert_eq!(friend_relations.len(), 1);
    }
    
    #[test]
    fn test_list_relations_filter_by_source() {
        let store = create_test_store();
        
        // Add persons
        let alice = store.add_person(PersonInput {
            id: None,
            name: "Alice".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        let bob = store.add_person(PersonInput {
            id: None,
            name: "Bob".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        let charlie = store.add_person(PersonInput {
            id: None,
            name: "Charlie".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        // Alice -> Bob
        store.add_friend(&alice.id, &bob.id, Default::default());
        // Bob -> Charlie
        store.add_friend(&bob.id, &charlie.id, Default::default());
        
        // Filter by alice as source
        let alice_relations = store.list_relations(Some(RelationFilter {
            relation_type: None,
            source_id: Some(alice.id.clone()),
            target_id: None,
            limit: None,
        }));
        assert!(alice_relations.len() >= 1);
        
        // Verify alice is involved in returned relations
        for rel in &alice_relations {
            assert!(rel.source == alice.id || (rel.bidirectional && rel.target == alice.id));
        }
    }
    
    #[test]
    fn test_list_relations_with_limit() {
        let store = create_test_store();
        
        // Add 5 persons and create relations
        let mut persons = Vec::new();
        for i in 0..5 {
            let person = store.add_person(PersonInput {
                id: None,
                name: format!("Person {}", i),
                avatar: None,
                description: None,
                phone: None,
                email: None,
                wechat: None,
                properties: BTreeMap::new(),
            });
            persons.push(person);
        }
        
        // Create chain of friendships
        for i in 0..4 {
            store.add_friend(&persons[i].id, &persons[i + 1].id, Default::default());
        }
        
        // List with limit
        let limited = store.list_relations(Some(RelationFilter {
            relation_type: None,
            source_id: None,
            target_id: None,
            limit: Some(2),
        }));
        assert_eq!(limited.len(), 2);
    }
    
    #[test]
    fn test_list_relations_empty() {
        let store = create_test_store();
        
        // No relations
        let relations = store.list_relations(None);
        assert!(relations.is_empty());
    }
    
    #[test]
    fn test_get_relation_by_id() {
        let store = create_test_store();
        
        // Add persons
        let alice = store.add_person(PersonInput {
            id: None,
            name: "Alice".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        let bob = store.add_person(PersonInput {
            id: None,
            name: "Bob".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        // Add friend
        store.add_friend(&alice.id, &bob.id, Default::default());
        
        // Get relation by listing first
        let relations = store.list_relations(None);
        assert!(!relations.is_empty());
        
        let rel_id = relations[0].id.clone();
        
        // Get by ID
        let relation = store.get_relation_by_id(&rel_id);
        assert!(relation.is_some());
        assert_eq!(relation.unwrap().id, rel_id);
        
        // Non-existent ID
        let not_found = store.get_relation_by_id("non_existent_id");
        assert!(not_found.is_none());
    }
    
    #[test]
    fn test_get_relation_between() {
        let store = create_test_store();
        
        // Add persons
        let alice = store.add_person(PersonInput {
            id: None,
            name: "Alice".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        let bob = store.add_person(PersonInput {
            id: None,
            name: "Bob".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        let charlie = store.add_person(PersonInput {
            id: None,
            name: "Charlie".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        // Add friend Alice -> Bob
        store.add_friend(&alice.id, &bob.id, Default::default());
        
        // Get relation between alice and bob (forward)
        let rel1 = store.get_relation_between(&alice.id, &bob.id);
        assert!(rel1.is_some());
        
        // Get relation between bob and alice (reverse should also work)
        let rel2 = store.get_relation_between(&bob.id, &alice.id);
        assert!(rel2.is_some());
        
        // No relation between alice and charlie
        let rel3 = store.get_relation_between(&alice.id, &charlie.id);
        assert!(rel3.is_none());
    }
    
    #[test]
    fn test_get_entity_relations() {
        let store = create_test_store();
        
        // Add persons
        let alice = store.add_person(PersonInput {
            id: None,
            name: "Alice".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        let bob = store.add_person(PersonInput {
            id: None,
            name: "Bob".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        let charlie = store.add_person(PersonInput {
            id: None,
            name: "Charlie".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        // Alice -> Bob
        store.add_friend(&alice.id, &bob.id, Default::default());
        // Charlie -> Alice
        store.add_friend(&charlie.id, &alice.id, Default::default());
        
        // Add tag to alice
        let tag = store.add_tag(TagInput {
            id: None,
            name: "Tech".to_string(),
            tag_type: "tag".to_string(),
            description: None,
            color: None,
            parent_tags: vec![],
            properties: BTreeMap::new(),
        });
        store.tag_person(&alice.id, &tag.id);
        
        // Get all relations for alice
        let alice_relations = store.get_entity_relations(&alice.id);
        // Should have: outgoing to Bob, outgoing tag, incoming from Charlie
        assert!(alice_relations.len() >= 2);
    }
    
    #[test]
    fn test_relation_count() {
        let store = create_test_store();
        
        // Initially no relations
        assert_eq!(store.relation_count(), 0);
        
        // Add persons
        let alice = store.add_person(PersonInput {
            id: None,
            name: "Alice".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        let bob = store.add_person(PersonInput {
            id: None,
            name: "Bob".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        // Add friend
        store.add_friend(&alice.id, &bob.id, Default::default());
        
        // Now should have at least 1 relation
        assert!(store.relation_count() >= 1);
    }
    
    // ========================================================================
    // export_graph Tests
    // ========================================================================
    
    #[test]
    fn test_export_graph_basic() {
        let store = create_test_store();
        
        // Add persons
        let alice = store.add_person(PersonInput {
            id: None,
            name: "Alice".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        let bob = store.add_person(PersonInput {
            id: None,
            name: "Bob".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        // Add friend relationship
        store.add_friend(&alice.id, &bob.id, Default::default());
        
        // Add a tag
        let tech_tag = store.add_tag(TagInput {
            id: None,
            name: "Tech".to_string(),
            tag_type: "tag".to_string(),
            description: None,
            color: None,
            parent_tags: vec![],
            properties: BTreeMap::new(),
        });
        store.tag_person(&alice.id, &tech_tag.id);
        
        // Export graph
        let graph = store.export_graph(None);
        
        // Verify nodes
        assert!(graph.nodes.len() >= 3); // 2 persons + 1 tag
        
        // Verify edges
        assert!(graph.edges.len() >= 2); // friend + tag relation
        
        // Verify stats
        assert_eq!(graph.stats.persons, 2);
        assert!(graph.stats.tags >= 1);
    }
    
    #[test]
    fn test_export_graph_with_max_nodes() {
        let store = create_test_store();
        
        // Add 10 persons
        for i in 0..10 {
            store.add_person(PersonInput {
                id: None,
                name: format!("Person {}", i),
                avatar: None,
                description: None,
                phone: None,
                email: None,
                wechat: None,
                properties: BTreeMap::new(),
            });
        }
        
        // Export with max 5 nodes
        let graph = store.export_graph(Some(ExportGraphOptions {
            max_nodes: Some(5),
            ..Default::default()
        }));
        
        // Should have at most 5 nodes
        assert!(graph.nodes.len() <= 5);
    }
    
    #[test]
    fn test_export_graph_exclude_tags() {
        let store = create_test_store();
        
        // Add person
        let alice = store.add_person(PersonInput {
            id: None,
            name: "Alice".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        // Add tag
        let tag = store.add_tag(TagInput {
            id: None,
            name: "Tech".to_string(),
            tag_type: "tag".to_string(),
            description: None,
            color: None,
            parent_tags: vec![],
            properties: BTreeMap::new(),
        });
        store.tag_person(&alice.id, &tag.id);
        
        // Export without tags
        let graph = store.export_graph(Some(ExportGraphOptions {
            include_tags: Some(false),
            ..Default::default()
        }));
        
        // Should only have person node
        assert_eq!(graph.nodes.len(), 1);
        assert_eq!(graph.stats.tags, 0);
    }
    
    #[test]
    fn test_export_graph_with_company() {
        let store = create_test_store();
        
        // Add person
        let alice = store.add_person(PersonInput {
            id: None,
            name: "Alice".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        // Add company
        let company = store.add_tag(TagInput {
            id: None,
            name: "Google".to_string(),
            tag_type: "company".to_string(),
            description: Some("Tech Company".to_string()),
            color: None,
            parent_tags: vec![],
            properties: BTreeMap::new(),
        });
        store.tag_person(&alice.id, &company.id);
        
        // Export with companies
        let graph = store.export_graph(Some(ExportGraphOptions {
            include_companies: Some(true),
            ..Default::default()
        }));
        
        // Should have person and company
        assert!(graph.nodes.len() >= 2);
        assert_eq!(graph.stats.companies, 1);
        
        // Export without companies
        let graph_no_company = store.export_graph(Some(ExportGraphOptions {
            include_companies: Some(false),
            ..Default::default()
        }));
        
        // Should only have person
        assert_eq!(graph_no_company.stats.companies, 0);
    }
    
    #[test]
    fn test_export_graph_edge_deduplication() {
        let store = create_test_store();
        
        // Add persons
        let alice = store.add_person(PersonInput {
            id: None,
            name: "Alice".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        let bob = store.add_person(PersonInput {
            id: None,
            name: "Bob".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        // Add bidirectional friend relationship
        store.add_friend(&alice.id, &bob.id, FriendConfig {
            bidirectional: true,
            ..Default::default()
        });
        
        // Export graph
        let graph = store.export_graph(None);
        
        // Count edges between alice and bob
        let edge_count = graph.edges.iter()
            .filter(|e| {
                (e.from == alice.id && e.to == bob.id) ||
                (e.from == bob.id && e.to == alice.id)
            })
            .count();
        
        // Should only have 1 edge (deduplicated)
        assert_eq!(edge_count, 1);
    }
    
    #[test]
    fn test_export_graph_empty() {
        let store = create_test_store();
        
        // Export empty graph
        let graph = store.export_graph(None);
        
        assert!(graph.nodes.is_empty());
        assert!(graph.edges.is_empty());
        assert_eq!(graph.stats.persons, 0);
        assert_eq!(graph.stats.companies, 0);
        assert_eq!(graph.stats.tags, 0);
        assert_eq!(graph.stats.relations, 0);
    }

    #[test]
    fn test_export_graph_node_degree_default_enabled() {
        let store = create_test_store();

        let alice = store.add_person(PersonInput {
            id: None,
            name: "Alice".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });

        let bob = store.add_person(PersonInput {
            id: None,
            name: "Bob".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });

        store.add_friend(&alice.id, &bob.id, Default::default());

        let graph = store.export_graph(None);
        let alice_node = graph.nodes.iter().find(|n| n.id == alice.id).unwrap();
        let bob_node = graph.nodes.iter().find(|n| n.id == bob.id).unwrap();

        assert_eq!(alice_node.degree, Some(1));
        assert_eq!(bob_node.degree, Some(1));
    }

    #[test]
    fn test_export_graph_node_degree_can_be_disabled() {
        let store = create_test_store();

        let alice = store.add_person(PersonInput {
            id: None,
            name: "Alice".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });

        let bob = store.add_person(PersonInput {
            id: None,
            name: "Bob".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });

        store.add_friend(&alice.id, &bob.id, Default::default());

        let graph = store.export_graph(Some(ExportGraphOptions {
            include_node_degree: Some(false),
            ..Default::default()
        }));

        let alice_node = graph.nodes.iter().find(|n| n.id == alice.id).unwrap();
        let bob_node = graph.nodes.iter().find(|n| n.id == bob.id).unwrap();

        assert_eq!(alice_node.degree, None);
        assert_eq!(bob_node.degree, None);
    }
    
    #[test]
    fn test_performance_summary() {
        let store = create_test_store();
        
        // Add some data
        for i in 0..10 {
            store.add_person(PersonInput {
                id: None,
                name: format!("Person {}", i),
                avatar: None,
                description: None,
                phone: None,
                email: None,
                wechat: None,
                properties: BTreeMap::new(),
            });
        }
        
        let summary = store.performance_summary();
        assert_eq!(summary.entity_count, 10);
        assert!(summary.shard_count > 0);
    }
    
    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn test_async_api() {
        let dir = tempdir().unwrap();
        let config = SocialStoreV2Config::new(dir.path())
            .with_shard_count(4)
            .with_wal(false);
        // Use new_async to avoid nested runtime
        let store = SocialStoreV2::new_async(config).await;
        
        // Test async person creation
        let person = store.add_person_async(PersonInput {
            id: None,
            name: "Alice".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        }).await.unwrap();
        
        assert_eq!(person.name, "Alice");
        
        // Test async update
        let mut updates = BTreeMap::new();
        updates.insert("description".to_string(), serde_json::json!("Updated description"));
        let updated = store.update_person_async(&person.id, updates).await.unwrap();
        assert!(updated.is_some());
        
        // Test async delete
        let deleted = store.delete_person_async(&person.id).await.unwrap();
        assert!(deleted);
    }
    
    // ========================================================================
    // Vector Search Tests
    // ========================================================================
    
    fn create_test_store_with_vector() -> (tempfile::TempDir, SocialStoreV2) {
        let dir = tempdir().unwrap();
        let config = SocialStoreV2Config::new(dir.path())
            .with_shard_count(2)
            .with_wal(true)
            .with_vector_search(VectorSearchConfig::new(4)); // 4-dim for tests
        let store = SocialStoreV2::new(config);
        (dir, store)
    }
    
    fn make_embedding(seed: f32) -> Vec<f32> {
        // Create a simple 4-dim normalized-ish vector from a seed
        let raw = vec![seed, seed * 0.5, 1.0 - seed, seed * 0.3];
        let norm: f32 = raw.iter().map(|x| x * x).sum::<f32>().sqrt();
        if norm > 0.0 {
            raw.into_iter().map(|x| x / norm).collect()
        } else {
            vec![1.0, 0.0, 0.0, 0.0]
        }
    }
    
    #[test]
    fn test_vector_search_config() {
        let config = VectorSearchConfig::new(384);
        assert_eq!(config.dimension, 384);
        assert_eq!(config.m, 16);
        assert_eq!(config.ef_construction, 200);
        assert_eq!(config.ef_search, 50);
        assert_eq!(config.max_elements, 100_000);
        
        let config2 = VectorSearchConfig::new(768)
            .with_m(32)
            .with_ef_construction(400)
            .with_ef_search(100)
            .with_max_elements(500_000);
        assert_eq!(config2.dimension, 768);
        assert_eq!(config2.m, 32);
        assert_eq!(config2.ef_construction, 400);
        assert_eq!(config2.ef_search, 100);
        assert_eq!(config2.max_elements, 500_000);
    }
    
    #[test]
    fn test_vector_search_not_enabled() {
        let store = create_test_store();
        assert!(!store.is_vector_search_enabled());
        assert_eq!(store.vector_count(), 0);
        
        let result = store.index_entity("test-id", vec![0.1, 0.2, 0.3, 0.4]);
        assert!(result.is_err());
    }
    
    #[test]
    fn test_vector_search_enabled() {
        let (_dir, store) = create_test_store_with_vector();
        assert!(store.is_vector_search_enabled());
        assert_eq!(store.vector_count(), 0);
        
        let config = store.vector_search_config().unwrap();
        assert_eq!(config.dimension, 4);
    }
    
    #[test]
    fn test_index_entity_and_search() {
        let (_dir, store) = create_test_store_with_vector();
        
        // Add some entities
        let p1 = store.add_person(PersonInput {
            id: None,
            name: "Alice".to_string(),
            avatar: None,
            description: Some("Machine learning engineer".to_string()),
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        let p2 = store.add_person(PersonInput {
            id: None,
            name: "Bob".to_string(),
            avatar: None,
            description: Some("Data scientist".to_string()),
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        let p3 = store.add_person(PersonInput {
            id: None,
            name: "Charlie".to_string(),
            avatar: None,
            description: Some("Frontend developer".to_string()),
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        // Index entities with embeddings
        let emb1 = make_embedding(0.8);
        let emb2 = make_embedding(0.7); // Similar to emb1
        let emb3 = make_embedding(0.1); // Different from emb1
        
        store.index_entity(&p1.id, emb1.clone()).unwrap();
        store.index_entity(&p2.id, emb2.clone()).unwrap();
        store.index_entity(&p3.id, emb3.clone()).unwrap();
        
        assert_eq!(store.vector_count(), 3);
        
        // Search with emb1 as query — p1 should be closest, p2 next, p3 furthest
        let results = store.search_entities_by_vector(&emb1, 3, None).unwrap();
        assert_eq!(results.len(), 3);
        assert_eq!(results[0].entity_id, p1.id); // Exact match
        assert_eq!(results[1].entity_id, p2.id); // Similar
        assert!(results[0].score >= results[1].score);
        assert!(results[1].score >= results[2].score);
    }
    
    #[test]
    fn test_index_entity_not_found() {
        let (_dir, store) = create_test_store_with_vector();
        let result = store.index_entity("nonexistent-id", make_embedding(0.5));
        assert!(result.is_err());
    }
    
    #[test]
    fn test_remove_entity_vector() {
        let (_dir, store) = create_test_store_with_vector();
        
        let p = store.add_person(PersonInput {
            id: None,
            name: "Alice".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        store.index_entity(&p.id, make_embedding(0.5)).unwrap();
        assert_eq!(store.vector_count(), 1);
        
        store.remove_entity_vector(&p.id).unwrap();
        // After removal, search should not find this entity
        let results = store.search_entities_by_vector(&make_embedding(0.5), 10, None).unwrap();
        assert!(results.is_empty() || !results.iter().any(|r| r.entity_id == p.id));
    }
    
    #[test]
    fn test_search_with_entity_type_filter() {
        let (_dir, store) = create_test_store_with_vector();
        
        // Add a person
        let p = store.add_person(PersonInput {
            id: None,
            name: "Alice".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        // Add a tag (different entity type)
        let tag = store.add_tag(TagInput {
            id: None,
            name: "machine_learning".to_string(),
            tag_type: "tag".to_string(),
            description: None,
            color: None,
            parent_tags: vec![],
            properties: BTreeMap::new(),
        });
        
        // Index both
        let emb = make_embedding(0.6);
        store.index_entity(&p.id, emb.clone()).unwrap();
        store.index_entity(&tag.id, make_embedding(0.55)).unwrap();
        
        // Search with person type filter
        let results = store.search_entities_by_vector(
            &emb, 10, Some(entity_types::PERSON)
        ).unwrap();
        assert!(results.iter().all(|r| r.entity.entity_type == entity_types::PERSON));
        
        // Search with tag type filter
        let results = store.search_entities_by_vector(
            &emb, 10, Some(entity_types::TAG)
        ).unwrap();
        assert!(results.iter().all(|r| r.entity.entity_type == entity_types::TAG));
    }
    
    #[test]
    fn test_hybrid_search() {
        let (_dir, store) = create_test_store_with_vector();
        
        let p1 = store.add_person(PersonInput {
            id: None,
            name: "Alice Machine Learning".to_string(),
            avatar: None,
            description: Some("Expert in ML and AI".to_string()),
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        let p2 = store.add_person(PersonInput {
            id: None,
            name: "Bob Backend Dev".to_string(),
            avatar: None,
            description: Some("Backend developer".to_string()),
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        let emb1 = make_embedding(0.8);
        let emb2 = make_embedding(0.2);
        store.index_entity(&p1.id, emb1.clone()).unwrap();
        store.index_entity(&p2.id, emb2).unwrap();
        
        // Hybrid: vector similar to p1 + text matches p1's name
        let results = store.hybrid_search(
            Some(&emb1),
            Some("Alice"),
            None,
            10,
        ).unwrap();
        assert!(!results.is_empty());
        // Alice should score highest because both vector AND text match
        assert_eq!(results[0].entity_id, p1.id);
        
        // Text-only search
        let results = store.hybrid_search(
            None,
            Some("Backend"),
            None,
            10,
        ).unwrap();
        assert!(!results.is_empty());
        assert_eq!(results[0].entity_id, p2.id);
        
        // Vector-only search
        let results = store.hybrid_search(
            Some(&emb1),
            None,
            None,
            10,
        ).unwrap();
        assert!(!results.is_empty());
        assert_eq!(results[0].entity_id, p1.id);
        
        // Empty search
        let results = store.hybrid_search(None, None, None, 10).unwrap();
        assert!(results.is_empty());
    }
    
    #[test]
    #[allow(unused_assignments)]
    fn test_vector_wal_recovery() {
        let dir = tempdir().unwrap();
        let emb1 = make_embedding(0.9);
        let emb2 = make_embedding(0.3);
        let mut person_id = String::new();
        let mut person2_id = String::new();
        
        // Phase 1: Create store, index entities, then drop
        {
            let config = SocialStoreV2Config::new(dir.path())
                .with_shard_count(2)
                .with_wal(true)
                .with_vector_search(VectorSearchConfig::new(4));
            let store = SocialStoreV2::new(config);
            
            let p1 = store.add_person(PersonInput {
                id: None,
                name: "Alice".to_string(),
                avatar: None,
                description: None,
                phone: None,
                email: None,
                wechat: None,
                properties: BTreeMap::new(),
            });
            let p2 = store.add_person(PersonInput {
                id: None,
                name: "Bob".to_string(),
                avatar: None,
                description: None,
                phone: None,
                email: None,
                wechat: None,
                properties: BTreeMap::new(),
            });
            
            person_id = p1.id.clone();
            person2_id = p2.id.clone();
            
            store.index_entity(&p1.id, emb1.clone()).unwrap();
            store.index_entity(&p2.id, emb2.clone()).unwrap();
            store.flush().unwrap();
            
            assert_eq!(store.vector_count(), 2);
            // Store drops here
        }
        
        // Phase 2: Reopen — should recover both graph and vector data from WAL
        {
            let config = SocialStoreV2Config::new(dir.path())
                .with_shard_count(2)
                .with_wal(true)
                .with_vector_search(VectorSearchConfig::new(4));
            let store = SocialStoreV2::new(config);
            
            // Graph entities should be recovered
            assert!(store.get_person(&person_id).is_some(), "Person 1 should be recovered");
            assert!(store.get_person(&person2_id).is_some(), "Person 2 should be recovered");
            
            // Vector index should be recovered
            assert_eq!(store.vector_count(), 2, "Vector count should be 2 after recovery");
            
            // Search should work
            let results = store.search_entities_by_vector(&emb1, 2, None).unwrap();
            assert_eq!(results.len(), 2);
            assert_eq!(results[0].entity_id, person_id); // emb1 closest to itself
        }
    }
    
    #[test]
    fn test_reindex_entity_idempotent() {
        let (_dir, store) = create_test_store_with_vector();
        
        let p = store.add_person(PersonInput {
            id: None,
            name: "Alice".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        // Index with first embedding
        let emb1 = make_embedding(0.1);
        store.index_entity(&p.id, emb1.clone()).unwrap();
        
        // Re-index same entity should not error (idempotent)
        let emb2 = make_embedding(0.9);
        store.index_entity(&p.id, emb2.clone()).unwrap();
        
        // Count stays at 1 (duplicate insert is silently skipped)
        assert_eq!(store.vector_count(), 1);
        
        // Search should still find the entity
        let results = store.search_entities_by_vector(&emb1, 1, None).unwrap();
        assert_eq!(results.len(), 1);
        assert_eq!(results[0].entity_id, p.id);
    }
    
    #[test]
    fn test_remove_entity_vector_and_search() {
        let (_dir, store) = create_test_store_with_vector();
        
        let p1 = store.add_person(PersonInput {
            id: None,
            name: "Alice".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        let p2 = store.add_person(PersonInput {
            id: None,
            name: "Bob".to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
        
        let emb1 = make_embedding(0.8);
        let emb2 = make_embedding(0.2);
        store.index_entity(&p1.id, emb1.clone()).unwrap();
        store.index_entity(&p2.id, emb2.clone()).unwrap();
        assert_eq!(store.vector_count(), 2);
        
        // Remove p1's vector
        store.remove_entity_vector(&p1.id).unwrap();
        
        // Search should only find p2 now
        let results = store.search_entities_by_vector(&emb1, 10, None).unwrap();
        // p1 should not appear in results after removal
        assert!(results.iter().all(|r| r.entity_id != p1.id));
        // p2 should still be findable
        assert!(results.iter().any(|r| r.entity_id == p2.id));
    }
    
    // ========================================================================
    // Phase-97: Pagination API Tests
    // ========================================================================
    
    fn create_paginated_test_store() -> SocialStoreV2 {
        let dir = tempdir().unwrap();
        let config = SocialStoreV2Config::new(dir.path())
            .with_shard_count(4)
            .with_wal(false);
        SocialStoreV2::new(config)
    }
    
    fn add_test_person(store: &SocialStoreV2, id: &str, name: &str) {
        store.add_person(PersonInput {
            id: Some(id.to_string()),
            name: name.to_string(),
            avatar: None,
            description: None,
            phone: None,
            email: None,
            wechat: None,
            properties: BTreeMap::new(),
        });
    }
    
    #[test]
    fn test_list_entities_by_type_paginated() {
        let store = create_paginated_test_store();
        
        // Add 20 persons
        for i in 0..20 {
            add_test_person(&store, &format!("person_{}", i), &format!("Person {}", i));
        }
        
        // Page 1: offset=0, limit=5
        let page1 = store.list_entities_by_type_paginated("person", 0, 5);
        assert_eq!(page1.total, 20);
        assert_eq!(page1.items.len(), 5);
        assert_eq!(page1.offset, 0);
        assert_eq!(page1.limit, 5);
        assert!(page1.has_more);
        
        // Page 2
        let page2 = store.list_entities_by_type_paginated("person", 5, 5);
        assert_eq!(page2.total, 20);
        assert_eq!(page2.items.len(), 5);
        assert_eq!(page2.offset, 5);
        assert!(page2.has_more);
        
        // Last page
        let last = store.list_entities_by_type_paginated("person", 15, 5);
        assert_eq!(last.total, 20);
        assert_eq!(last.items.len(), 5);
        assert!(!last.has_more);
        
        // Beyond range
        let empty = store.list_entities_by_type_paginated("person", 100, 5);
        assert_eq!(empty.total, 20);
        assert_eq!(empty.items.len(), 0);
        assert!(!empty.has_more);
        
        // Non-existent type
        let none = store.list_entities_by_type_paginated("nonexistent", 0, 10);
        assert_eq!(none.total, 0);
        assert_eq!(none.items.len(), 0);
        assert!(!none.has_more);
    }
    
    #[test]
    fn test_count_entities_by_type_efficient() {
        let store = create_paginated_test_store();
        
        for i in 0..15 {
            add_test_person(&store, &format!("p_{}", i), &format!("P{}", i));
        }
        
        assert_eq!(store.count_entities_by_type("person"), 15);
        assert_eq!(store.count_entities_by_type("nonexistent"), 0);
    }
    
    #[test]
    fn test_list_relations_paginated() {
        let store = create_paginated_test_store();
        
        // Add persons
        for i in 0..10 {
            add_test_person(&store, &format!("p_{}", i), &format!("Person {}", i));
        }
        
        // Add friend relationships (9 relations: p_0-p_1, p_1-p_2, ..., p_8-p_9)
        for i in 0..9 {
            store.add_friend(
                &format!("p_{}", i),
                &format!("p_{}", i + 1),
                FriendConfig::default(),
            );
        }
        
        // Paginate all relations
        let page1 = store.list_relations_paginated(None, 0, 3);
        assert_eq!(page1.total, 9);
        assert_eq!(page1.items.len(), 3);
        assert!(page1.has_more);
        
        let page2 = store.list_relations_paginated(None, 3, 3);
        assert_eq!(page2.total, 9);
        assert_eq!(page2.items.len(), 3);
        assert!(page2.has_more);
        
        let page3 = store.list_relations_paginated(None, 6, 3);
        assert_eq!(page3.total, 9);
        assert_eq!(page3.items.len(), 3);
        assert!(!page3.has_more);
        
        // With filter
        let filtered = store.list_relations_paginated(
            Some(RelationFilter {
                source_id: Some("p_0".to_string()),
                ..Default::default()
            }),
            0,
            100,
        );
        // p_0 has friend relation with p_1 (bidirectional)
        assert!(filtered.total >= 1);
    }
    
    #[test]
    fn test_export_graph_paginated() {
        let store = create_paginated_test_store();
        
        // Add 10 persons
        for i in 0..10 {
            add_test_person(&store, &format!("p_{}", i), &format!("Person {}", i));
        }
        
        // Add some relations
        for i in 0..5 {
            store.add_friend(
                &format!("p_{}", i),
                &format!("p_{}", i + 5),
                FriendConfig::default(),
            );
        }
        
        // First page: 5 nodes
        let page1 = store.export_graph_paginated(ExportGraphPaginatedOptions {
            offset: 0,
            limit: 5,
            include_node_degree: Some(true),
            ..Default::default()
        });
        assert_eq!(page1.total_nodes, 10);
        assert_eq!(page1.nodes.len(), 5);
        assert_eq!(page1.offset, 0);
        assert_eq!(page1.limit, 5);
        assert!(page1.has_more);
        // Edges only where both endpoints are in this page
        assert!(page1.edges.len() <= 5);
        
        // Second page
        let page2 = store.export_graph_paginated(ExportGraphPaginatedOptions {
            offset: 5,
            limit: 5,
            ..Default::default()
        });
        assert_eq!(page2.total_nodes, 10);
        assert_eq!(page2.nodes.len(), 5);
        assert!(!page2.has_more);
        
        // Full graph in one page
        let full = store.export_graph_paginated(ExportGraphPaginatedOptions {
            offset: 0,
            limit: 100,
            include_node_degree: Some(true),
            ..Default::default()
        });
        assert_eq!(full.total_nodes, 10);
        assert_eq!(full.nodes.len(), 10);
        assert_eq!(full.edges.len(), 5);
        assert!(!full.has_more);
        
        // Verify degree is populated
        let has_degree = full.nodes.iter().any(|n| n.degree.unwrap_or(0) > 0);
        assert!(has_degree, "At least some nodes should have degree > 0");
    }
    
    #[test]
    fn test_export_graph_paginated_with_entity_type_filter() {
        let store = create_paginated_test_store();
        
        // Add persons
        for i in 0..5 {
            add_test_person(&store, &format!("p_{}", i), &format!("Person {}", i));
        }
        
        // Add tags
        for i in 0..3 {
            store.add_tag(TagInput {
                id: Some(format!("t_{}", i)),
                name: format!("Tag {}", i),
                tag_type: "tag".to_string(),
                description: None,
                color: None,
                parent_tags: Vec::new(),
                properties: BTreeMap::new(),
            });
        }
        
        // Filter to persons only
        let persons_only = store.export_graph_paginated(ExportGraphPaginatedOptions {
            offset: 0,
            limit: 100,
            entity_types: Some(vec!["person".to_string()]),
            ..Default::default()
        });
        assert_eq!(persons_only.total_nodes, 5);
        assert_eq!(persons_only.nodes.len(), 5);
    }
    
    // ========================================================================
    // Phase-98: Compact Binary Export + Aggregation Tests
    // ========================================================================
    
    #[test]
    fn test_export_graph_compact_format() {
        let store = create_paginated_test_store();
        
        // Add 5 persons
        for i in 0..5 {
            add_test_person(&store, &format!("p_{}", i), &format!("Person {}", i));
        }
        
        // Add 3 relations
        for i in 0..3 {
            store.add_friend(
                &format!("p_{}", i),
                &format!("p_{}", i + 1),
                FriendConfig::default(),
            );
        }
        
        let compact = store.export_graph_compact(Some(ExportGraphOptions {
            max_nodes: Some(500),
            ..Default::default()
        }));
        
        assert_eq!(compact.node_count, 5);
        assert_eq!(compact.edge_count, 3);
        assert!(!compact.buffer.is_empty());
        
        // Verify header
        let buf = &compact.buffer;
        assert!(buf.len() >= 16, "Buffer must have at least 16 bytes header");
        
        let magic = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
        assert_eq!(magic, 0x41494647, "Magic should be 'AIFG'");
        
        let version = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);
        assert_eq!(version, 1);
        
        let node_count = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]);
        assert_eq!(node_count, 5);
        
        let edge_count = u32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]);
        assert_eq!(edge_count, 3);
    }
    
    #[test]
    fn test_export_graph_compact_parse_string_table() {
        let store = create_paginated_test_store();
        
        add_test_person(&store, "alice", "Alice");
        add_test_person(&store, "bob", "Bob");
        
        let compact = store.export_graph_compact(None);
        let buf = &compact.buffer;
        
        // Parse first node from string table (after 16-byte header)
        let mut offset = 16usize;
        
        // Parse two nodes
        let mut parsed_nodes: Vec<(String, u8, String, u32)> = Vec::new();
        for _ in 0..2 {
            let id_len = u16::from_le_bytes([buf[offset], buf[offset + 1]]) as usize;
            offset += 2;
            let id = std::str::from_utf8(&buf[offset..offset + id_len]).unwrap().to_string();
            offset += id_len;
            
            let group = buf[offset];
            offset += 1;
            
            let label_len = u16::from_le_bytes([buf[offset], buf[offset + 1]]) as usize;
            offset += 2;
            let label = std::str::from_utf8(&buf[offset..offset + label_len]).unwrap().to_string();
            offset += label_len;
            
            let degree = u32::from_le_bytes([buf[offset], buf[offset + 1], buf[offset + 2], buf[offset + 3]]);
            offset += 4;
            
            parsed_nodes.push((id, group, label, degree));
        }
        
        // Verify we got both nodes (order may vary)
        let ids: Vec<&str> = parsed_nodes.iter().map(|(id, _, _, _)| id.as_str()).collect();
        assert!(ids.contains(&"alice") || ids.contains(&"bob"), "Should contain at least one expected ID");
        
        // All should be Person group (0)
        for (_, group, _, _) in &parsed_nodes {
            assert_eq!(*group, 0, "Person nodes should have group=0");
        }
    }
    
    #[test]
    fn test_export_graph_compact_edge_table() {
        let store = create_paginated_test_store();
        
        add_test_person(&store, "a", "A");
        add_test_person(&store, "b", "B");
        
        store.add_friend("a", "b", FriendConfig::default());
        
        let compact = store.export_graph_compact(Some(ExportGraphOptions {
            max_nodes: Some(500),
            ..Default::default()
        }));
        let buf = &compact.buffer;
        
        assert_eq!(compact.node_count, 2);
        assert_eq!(compact.edge_count, 1);
        
        // Skip header + string table to find edge table
        let mut offset = 16usize;
        for _ in 0..compact.node_count {
            let id_len = u16::from_le_bytes([buf[offset], buf[offset + 1]]) as usize;
            offset += 2 + id_len + 1; // id_len(2) + id + group(1)
            let label_len = u16::from_le_bytes([buf[offset], buf[offset + 1]]) as usize;
            offset += 2 + label_len + 4; // label_len(2) + label + degree(4)
        }
        
        // Parse edge
        let from_idx = u32::from_le_bytes([buf[offset], buf[offset + 1], buf[offset + 2], buf[offset + 3]]);
        let to_idx = u32::from_le_bytes([buf[offset + 4], buf[offset + 5], buf[offset + 6], buf[offset + 7]]);
        let weight_x1000 = u32::from_le_bytes([buf[offset + 8], buf[offset + 9], buf[offset + 10], buf[offset + 11]]);
        
        assert!(from_idx < 2, "from_idx should be 0 or 1");
        assert!(to_idx < 2, "to_idx should be 0 or 1");
        assert_ne!(from_idx, to_idx, "from and to should differ");
        assert_eq!(weight_x1000, 1000, "default weight=1.0 -> 1000");
    }
    
    #[test]
    fn test_export_graph_compact_empty() {
        let store = create_paginated_test_store();
        
        let compact = store.export_graph_compact(None);
        assert_eq!(compact.node_count, 0);
        assert_eq!(compact.edge_count, 0);
        assert_eq!(compact.buffer.len(), 16); // header only
    }
    
    #[test]
    fn test_group_entities_summary() {
        let store = create_paginated_test_store();
        
        // Add persons
        for i in 0..10 {
            add_test_person(&store, &format!("p_{}", i), &format!("Person {}", i));
        }
        
        // Add tags
        for i in 0..5 {
            store.add_tag(TagInput {
                id: Some(format!("t_{}", i)),
                name: format!("Tag {}", i),
                tag_type: "tag".to_string(),
                description: None,
                color: None,
                parent_tags: Vec::new(),
                properties: BTreeMap::new(),
            });
        }
        
        // Add relations
        for i in 0..3 {
            store.add_friend(
                &format!("p_{}", i),
                &format!("p_{}", i + 1),
                FriendConfig::default(),
            );
        }
        
        let agg = store.group_entities_summary();
        
        assert_eq!(agg.total_entities, 15); // 10 persons + 5 tags
        assert_eq!(agg.total_relations, 3);
        assert!(!agg.groups.is_empty());
        
        // Find person group
        let person_group = agg.groups.iter().find(|g| g.entity_type == "person");
        assert!(person_group.is_some(), "Should have person group");
        let pg = person_group.unwrap();
        assert_eq!(pg.count, 10);
        assert!(pg.sample_ids.len() <= 5, "Should have at most 5 sample IDs");
        
        // Find tag group
        let tag_group = agg.groups.iter().find(|g| g.entity_type == "tag");
        assert!(tag_group.is_some(), "Should have tag group");
        assert_eq!(tag_group.unwrap().count, 5);
        
        // Groups should be sorted by count descending
        assert!(agg.groups[0].count >= agg.groups[1].count);
    }
    
    #[test]
    fn test_group_entities_summary_empty() {
        let store = create_paginated_test_store();
        
        let agg = store.group_entities_summary();
        assert_eq!(agg.total_entities, 0);
        assert_eq!(agg.total_relations, 0);
        assert!(agg.groups.is_empty());
    }
    
    #[test]
    fn test_compact_vs_json_size_comparison() {
        let store = create_paginated_test_store();
        
        // Add 100 persons with relations
        for i in 0..100 {
            add_test_person(&store, &format!("person_{}", i), &format!("Person Number {}", i));
        }
        for i in 0..50 {
            store.add_friend(
                &format!("person_{}", i),
                &format!("person_{}", i + 50),
                FriendConfig::default(),
            );
        }
        
        // JSON export
        let json_graph = store.export_graph(Some(ExportGraphOptions {
            max_nodes: Some(500),
            ..Default::default()
        }));
        let json_bytes = serde_json::to_vec(&json_graph).unwrap();
        
        // Binary export
        let compact = store.export_graph_compact(Some(ExportGraphOptions {
            max_nodes: Some(500),
            ..Default::default()
        }));
        
        println!("JSON size: {} bytes", json_bytes.len());
        println!("Binary size: {} bytes", compact.buffer.len());
        println!("Ratio: {:.2}x smaller", json_bytes.len() as f64 / compact.buffer.len() as f64);
        
        // Binary should be significantly smaller
        assert!(
            compact.buffer.len() < json_bytes.len(),
            "Binary ({}) should be smaller than JSON ({})",
            compact.buffer.len(),
            json_bytes.len()
        );
    }
}
