//! Graph Shard Router for distributing writes across shards
//!
//! Routes write requests to appropriate shards based on configurable
//! strategies (hash, entity type, round-robin).
//!
//! Maintains a GlobalGraphIndex for efficient cross-shard queries.

use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::hash::{Hash, Hasher};
use std::collections::hash_map::DefaultHasher;

use crate::types::graph::{Entity, Relation};
use crate::storage::concurrent::{WriteMetrics, BackpressureController};

use super::config::{ConcurrentGraphConfig, ShardStrategy, GraphShardConfig};
use super::shard::GraphShard;
use super::types::{GraphWriteOp, GraphWriteResult, GraphStoreError};
use super::wal::{GraphWALConfig, GraphRecoveryResult, GraphCompactResult};
use super::global_index::{GlobalGraphIndex, GlobalIndexConfig, GlobalIndexStats, CrossShardRef};

/// Graph Shard Router
///
/// Distributes write requests across multiple shards for parallel processing.
/// Maintains a GlobalGraphIndex for efficient cross-shard queries.
pub struct GraphShardRouter {
    /// Shards
    shards: Vec<Arc<GraphShard>>,
    
    /// Number of shards
    shard_count: usize,
    
    /// Routing strategy
    strategy: ShardStrategy,
    
    /// Round-robin counter (for RoundRobin strategy)
    round_robin_counter: AtomicUsize,
    
    /// Backpressure controller
    backpressure: BackpressureController,
    
    /// Global index for cross-shard queries
    global_index: Arc<GlobalGraphIndex>,
    
    /// Metrics
    metrics: Arc<WriteMetrics>,
    
    /// Configuration
    config: ConcurrentGraphConfig,
}

impl GraphShardRouter {
    /// Create a new shard router
    pub fn new(config: ConcurrentGraphConfig) -> Self {
        // Auto-generate type mapping from shard_names if not already configured
        let mut config = config;
        config.auto_generate_type_mapping();
        
        let shard_count = config.actual_shard_count();
        let strategy = config.shard_strategy;
        let auto_start = config.auto_start;
        let wal_enabled = config.wal_enabled;
        let metrics = Arc::new(WriteMetrics::new());
        let backpressure = BackpressureController::new(
            config.backpressure_config.clone(),
            metrics.clone(),
        );
        
        // Create global index
        let index_path = config.path.join("global_index.json");
        let index_config = GlobalIndexConfig {
            persist_enabled: true,
            persist_interval: 1000,
            enable_type_index: true,
            max_entries: 0,
        };
        let global_index = Arc::new(GlobalGraphIndex::with_path(index_config, index_path));
        
        // Create shards (with or without WAL)
        let shards: Vec<Arc<GraphShard>> = (0..shard_count)
            .map(|id| {
                let shard_config = GraphShardConfig::from_concurrent_config(id, &config);
                
                if wal_enabled {
                    // Create shard with WAL
                    // Use descriptive shard directory name if configured
                    let shard_dir_name = config.shard_dir_name(id);
                    let wal_base = config.path.join("wal");
                    let new_wal_path = wal_base.join(&shard_dir_name);
                    
                    // Migration: if new path doesn't exist but old format (shard_X) exists, rename it
                    let old_wal_path = wal_base.join(format!("shard_{}", id));
                    if !new_wal_path.exists() && old_wal_path.exists() && shard_dir_name != format!("shard_{}", id) {
                        if let Err(e) = std::fs::rename(&old_wal_path, &new_wal_path) {
                            tracing::warn!(
                                "Failed to migrate WAL directory from {:?} to {:?}: {}",
                                old_wal_path, new_wal_path, e
                            );
                        } else {
                            tracing::info!(
                                "Migrated WAL directory: {:?} -> {:?}",
                                old_wal_path, new_wal_path
                            );
                        }
                    }
                    
                    let wal_config = GraphWALConfig::new(&new_wal_path);
                    match GraphShard::with_wal(shard_config.clone(), metrics.clone(), wal_config) {
                        Ok(shard) => Arc::new(shard),
                        Err(e) => {
                            tracing::error!("Failed to create shard {} ({}) with WAL: {}", id, shard_dir_name, e);
                            // Fallback to shard without WAL
                            Arc::new(GraphShard::new(shard_config, metrics.clone()))
                        }
                    }
                } else {
                    Arc::new(GraphShard::new(shard_config, metrics.clone()))
                }
            })
            .collect();
        
        let router = Self {
            shards,
            shard_count,
            strategy,
            round_robin_counter: AtomicUsize::new(0),
            backpressure,
            global_index,
            metrics,
            config,
        };
        
        // Auto-start workers if configured
        if auto_start {
            router.start_workers();
        }
        
        router
    }
    
    /// Recover all shards from WAL and rebuild global index
    /// 
    /// Call this after creating the router to restore state from WAL files.
    /// Uses integrity checking to ensure the global index is complete.
    pub fn recover(&self) -> std::io::Result<Vec<GraphRecoveryResult>> {
        // First, try to load persisted global index with validation
        let (index_loaded, index_valid) = self.global_index.load_with_validation()
            .unwrap_or((false, false));
        
        let mut results = Vec::with_capacity(self.shard_count);
        
        for shard in &self.shards {
            let result = shard.recover()?;
            tracing::info!(
                "Recovered shard {}: {} entries replayed, {} skipped",
                shard.id(),
                result.entries_replayed,
                result.entries_skipped
            );
            results.push(result);
        }
        
        let total_replayed: usize = results.iter().map(|r| r.entries_replayed).sum();
        let total_skipped: usize = results.iter().map(|r| r.entries_skipped).sum();
        
        // Get actual counts from shards
        let actual_entity_count = self.entity_count();
        let actual_relation_count = self.relation_count();
        
        // Verify index integrity against actual shard data
        let integrity_result = self.global_index.verify_integrity(
            actual_entity_count,
            actual_relation_count
        );
        
        // Determine if we need to rebuild the index
        let need_rebuild = if total_replayed > 0 {
            // WAL had new entries, definitely rebuild
            tracing::info!("WAL had {} new entries, rebuilding global index", total_replayed);
            true
        } else if !index_loaded {
            // No index file existed
            tracing::info!("No persisted index found, rebuilding global index");
            true
        } else if !index_valid || !integrity_result.is_valid {
            // Index integrity check failed
            tracing::warn!(
                "Index integrity check failed (loaded_valid={}, integrity_valid={}), rebuilding global index. {}",
                index_valid,
                integrity_result.is_valid,
                integrity_result.summary()
            );
            true
        } else {
            false
        };
        
        if need_rebuild {
            self.rebuild_global_index();
            
            // Verify after rebuild
            let post_rebuild = self.global_index.verify_integrity(
                self.entity_count(),
                self.relation_count()
            );
            if !post_rebuild.is_valid {
                tracing::error!(
                    "Index still invalid after rebuild! {}",
                    post_rebuild.summary()
                );
            } else {
                tracing::info!("Global index rebuilt successfully: {}", post_rebuild.summary());
            }
        } else {
            tracing::info!(
                "Using persisted global index: {} entities, {} relations",
                integrity_result.indexed_entity_count,
                integrity_result.indexed_relation_count
            );
        }
        
        tracing::info!(
            "Recovery complete: {} total entries replayed, {} skipped across {} shards",
            total_replayed,
            total_skipped,
            self.shard_count
        );
        
        Ok(results)
    }
    
    /// Rebuild the global index from current shard data
    pub fn rebuild_global_index(&self) {
        self.global_index.clear();
        
        for (shard_id, shard) in self.shards.iter().enumerate() {
            // Index all entities
            for entity in shard.all_entities() {
                self.global_index.register_entity_with_type(
                    &entity.id,
                    &entity.entity_type,
                    shard_id,
                );
            }
            
            // Index all relations
            for relation in shard.all_relations() {
                let target_shard = self.route_entity(&relation.target);
                self.global_index.register_relation_with_target(
                    &relation.id,
                    &relation.source,
                    &relation.target,
                    shard_id,
                    target_shard,
                );
            }
        }
        
        let stats = self.global_index.stats();
        tracing::info!(
            "Global index rebuilt: {} entities, {} relations, {} cross-shard",
            stats.entity_count,
            stats.relation_count,
            stats.cross_shard_count
        );
    }
    
    /// Check if WAL is enabled
    pub fn is_wal_enabled(&self) -> bool {
        self.config.wal_enabled && self.shards.first().map(|s| s.is_wal_enabled()).unwrap_or(false)
    }
    
    /// Sync all shard WALs to disk
    pub fn sync_all(&self) -> std::io::Result<()> {
        for shard in &self.shards {
            shard.sync_wal()?;
        }
        Ok(())
    }
    
    /// Start all shard workers
    pub fn start_workers(&self) {
        for shard in &self.shards {
            shard.start();
        }
        tracing::info!("Started {} graph shard workers", self.shard_count);
    }
    
    /// Get number of shards
    pub fn shard_count(&self) -> usize {
        self.shard_count
    }
    
    /// Route an entity ID to a shard (for Hash/RoundRobin strategies)
    /// 
    /// For EntityType strategy, this uses the global index to find the shard.
    /// If not found in index, falls back to searching all shards.
    pub fn route_entity(&self, entity_id: &str) -> usize {
        match self.strategy {
            ShardStrategy::Hash => self.hash_route(entity_id),
            ShardStrategy::RoundRobin => self.round_robin_route(),
            ShardStrategy::EntityType => {
                // For EntityType strategy, we need to look up in global index
                // since we don't have the entity_type info here
                if let Some(shard_id) = self.global_index.get_entity_shard(entity_id) {
                    shard_id
                } else {
                    // Fallback: search all shards (this is slower but ensures correctness)
                    for (idx, shard) in self.shards.iter().enumerate() {
                        if shard.contains_entity(entity_id) {
                            return idx;
                        }
                    }
                    // Not found anywhere, use hash as last resort for new entities
                    self.hash_route(entity_id)
                }
            }
        }
    }
    
    /// Route an entity with type information to a shard
    /// 
    /// This is the preferred method for EntityType strategy as it uses
    /// the entity_type for routing directly.
    /// 
    /// For EntityType strategy:
    /// - If type_shard_mapping is configured, uses fixed mapping
    /// - Otherwise, falls back to hash(entity_type)
    pub fn route_entity_with_type(&self, entity_id: &str, entity_type: &str) -> usize {
        match self.strategy {
            ShardStrategy::Hash => self.hash_route(entity_id),
            ShardStrategy::RoundRobin => self.round_robin_route(),
            ShardStrategy::EntityType => {
                // Try type mapping first (Phase 3 feature)
                if let Some(shard_id) = self.config.get_type_shard(entity_type) {
                    return shard_id;
                }
                // Fallback to hash-based routing
                self.hash_route(entity_type)
            }
        }
    }
    
    /// Route an Entity object to a shard
    pub fn route_entity_obj(&self, entity: &Entity) -> usize {
        self.route_entity_with_type(&entity.id, &entity.entity_type)
    }
    
    /// Route a relation to a shard
    /// 
    /// For Hash strategy: follows source entity ID
    /// For EntityType strategy:
    /// - If relation_shard_id is configured, routes all relations to that shard
    /// - Otherwise, uses hash(relation_type) for routing
    pub fn route_relation(&self, relation: &Relation) -> usize {
        match self.strategy {
            ShardStrategy::Hash => self.hash_route(&relation.source),
            ShardStrategy::RoundRobin => self.round_robin_route(),
            ShardStrategy::EntityType => {
                // Check for dedicated relation shard first (Phase 3 feature)
                if let Some(relation_shard) = self.config.relation_shard_id {
                    return relation_shard;
                }
                // Try type mapping for relation types
                if let Some(shard_id) = self.config.get_type_shard(&relation.relation_type) {
                    return shard_id;
                }
                // Fallback: hash-based routing by relation_type
                self.hash_route(&relation.relation_type)
            }
        }
    }
    
    /// Route a relation with source entity type (for cross-shard reference calculation)
    pub fn route_relation_with_source_type(&self, relation: &Relation, _source_type: &str) -> usize {
        // Delegate to route_relation which handles all the logic
        self.route_relation(relation)
    }
    
    /// Route a write operation to a shard
    pub fn route_op(&self, op: &GraphWriteOp) -> usize {
        match op.routing_key() {
            Some(key) => self.route_entity(key),
            None => {
                // For operations without a routing key (like DeleteRelation),
                // we need to look up the relation first or use round-robin
                self.round_robin_route()
            }
        }
    }
    
    /// Hash-based routing
    fn hash_route(&self, key: &str) -> usize {
        let mut hasher = DefaultHasher::new();
        key.hash(&mut hasher);
        (hasher.finish() as usize) % self.shard_count
    }
    
    /// Round-robin routing
    fn round_robin_route(&self) -> usize {
        let counter = self.round_robin_counter.fetch_add(1, Ordering::Relaxed);
        counter % self.shard_count
    }
    
    // ========================================================================
    // Write API
    // ========================================================================
    
    /// Write an entity asynchronously
    pub async fn put_entity(&self, entity: Entity) -> Result<GraphWriteResult, GraphStoreError> {
        self.check_backpressure().await?;
        
        // Use route_entity_obj for proper EntityType strategy support
        let shard_id = self.route_entity_obj(&entity);
        let entity_id = entity.id.clone();
        let entity_type = entity.entity_type.clone();
        let op = GraphWriteOp::PutEntity(entity);
        
        let result = self.shards[shard_id].write(op).await?;
        
        // Update global index
        self.global_index.register_entity_with_type(&entity_id, &entity_type, shard_id);
        
        Ok(result)
    }
    
    /// Write an entity and wait for completion
    pub async fn put_entity_sync(&self, entity: Entity) -> Result<GraphWriteResult, GraphStoreError> {
        self.check_backpressure().await?;
        
        // Use route_entity_obj for proper EntityType strategy support
        let shard_id = self.route_entity_obj(&entity);
        let entity_id = entity.id.clone();
        let entity_type = entity.entity_type.clone();
        let op = GraphWriteOp::PutEntity(entity);
        
        let result = self.shards[shard_id].write_sync(op).await?;
        
        // Update global index
        self.global_index.register_entity_with_type(&entity_id, &entity_type, shard_id);
        
        Ok(result)
    }
    
    /// Delete an entity asynchronously
    pub async fn delete_entity(&self, id: &str) -> Result<GraphWriteResult, GraphStoreError> {
        self.check_backpressure().await?;
        
        let shard_id = self.route_entity(id);
        let op = GraphWriteOp::DeleteEntity(id.to_string());
        
        let result = self.shards[shard_id].write(op).await?;
        
        // Update global index
        self.global_index.remove_entity(id);
        
        Ok(result)
    }
    
    /// Delete an entity and wait for completion
    pub async fn delete_entity_sync(&self, id: &str) -> Result<GraphWriteResult, GraphStoreError> {
        self.check_backpressure().await?;
        
        let shard_id = self.route_entity(id);
        let op = GraphWriteOp::DeleteEntity(id.to_string());
        
        let result = self.shards[shard_id].write_sync(op).await?;
        
        // Update global index
        self.global_index.remove_entity(id);
        
        Ok(result)
    }
    
    /// Write a relation asynchronously
    pub async fn put_relation(&self, relation: Relation) -> Result<GraphWriteResult, GraphStoreError> {
        self.check_backpressure().await?;
        
        let shard_id = self.route_relation(&relation);
        let relation_id = relation.id.clone();
        let source_id = relation.source.clone();
        let target_id = relation.target.clone();
        let target_shard = self.route_entity(&target_id);
        let op = GraphWriteOp::PutRelation(relation);
        
        let result = self.shards[shard_id].write(op).await?;
        
        // Update global index with cross-shard reference if needed
        self.global_index.register_relation_with_target(
            &relation_id,
            &source_id,
            &target_id,
            shard_id,
            target_shard,
        );
        
        Ok(result)
    }
    
    /// Write a relation and wait for completion
    pub async fn put_relation_sync(&self, relation: Relation) -> Result<GraphWriteResult, GraphStoreError> {
        self.check_backpressure().await?;
        
        let shard_id = self.route_relation(&relation);
        let relation_id = relation.id.clone();
        let source_id = relation.source.clone();
        let target_id = relation.target.clone();
        let target_shard = self.route_entity(&target_id);
        let op = GraphWriteOp::PutRelation(relation);
        
        let result = self.shards[shard_id].write_sync(op).await?;
        
        // Update global index with cross-shard reference if needed
        self.global_index.register_relation_with_target(
            &relation_id,
            &source_id,
            &target_id,
            shard_id,
            target_shard,
        );
        
        Ok(result)
    }
    
    /// Delete a relation asynchronously
    /// 
    /// Note: This requires knowing which shard the relation is in.
    /// For efficiency, provide the source entity ID or target entity ID.
    pub async fn delete_relation(&self, id: &str, source_entity_id: Option<&str>, target_entity_id: Option<&str>) -> Result<GraphWriteResult, GraphStoreError> {
        self.check_backpressure().await?;
        
        // Try to use global index first
        let shard_id = if let Some(shard) = self.global_index.get_relation_shard(id) {
            shard
        } else if let Some(source) = source_entity_id {
            self.route_entity(source)
        } else {
            // Without source or global index entry, search all shards
            let mut found_shard = None;
            for (idx, shard) in self.shards.iter().enumerate() {
                if shard.contains_relation(id) {
                    found_shard = Some(idx);
                    break;
                }
            }
            match found_shard {
                Some(idx) => idx,
                None => return Err(GraphStoreError::RelationNotFound(id.to_string())),
            }
        };
        
        let op = GraphWriteOp::DeleteRelation(id.to_string());
        let result = self.shards[shard_id].write(op).await?;
        
        // Update global index
        self.global_index.remove_relation(id, target_entity_id);
        
        Ok(result)
    }
    
    /// Delete a relation and wait for completion
    pub async fn delete_relation_sync(&self, id: &str, source_entity_id: Option<&str>, target_entity_id: Option<&str>) -> Result<GraphWriteResult, GraphStoreError> {
        self.check_backpressure().await?;
        
        // Try to use global index first
        let shard_id = if let Some(shard) = self.global_index.get_relation_shard(id) {
            shard
        } else if let Some(source) = source_entity_id {
            self.route_entity(source)
        } else {
            // Without source or global index entry, search all shards
            let mut found_shard = None;
            for (idx, shard) in self.shards.iter().enumerate() {
                if shard.contains_relation(id) {
                    found_shard = Some(idx);
                    break;
                }
            }
            match found_shard {
                Some(idx) => idx,
                None => return Err(GraphStoreError::RelationNotFound(id.to_string())),
            }
        };
        
        let op = GraphWriteOp::DeleteRelation(id.to_string());
        let result = self.shards[shard_id].write_sync(op).await?;
        
        // Update global index
        self.global_index.remove_relation(id, target_entity_id);
        
        Ok(result)
    }
    
    /// Write a batch of operations
    pub async fn write_batch(&self, ops: Vec<GraphWriteOp>) -> Result<Vec<GraphWriteResult>, GraphStoreError> {
        self.check_backpressure().await?;
        
        let mut results = Vec::with_capacity(ops.len());
        
        for op in ops {
            let shard_id = self.route_op(&op);
            let result = self.shards[shard_id].write(op).await?;
            results.push(result);
        }
        
        Ok(results)
    }
    
    // ========================================================================
    // Read API
    // ========================================================================
    
    /// Get an entity by ID
    /// 
    /// For EntityType strategy, uses global index for O(1) shard lookup.
    /// Falls back to searching all shards if not found in index.
    pub fn get_entity(&self, id: &str) -> Option<Entity> {
        match self.strategy {
            ShardStrategy::Hash | ShardStrategy::RoundRobin => {
                let shard_id = self.route_entity(id);
                self.shards[shard_id].get_entity(id)
            }
            ShardStrategy::EntityType => {
                // Use global index for EntityType strategy
                if let Some(shard_id) = self.global_index.get_entity_shard(id) {
                    return self.shards[shard_id].get_entity(id);
                }
                // Fallback: search all shards
                for shard in &self.shards {
                    if let Some(entity) = shard.get_entity(id) {
                        return Some(entity);
                    }
                }
                None
            }
        }
    }
    
    /// Get an entity by ID with type hint (more efficient for EntityType strategy)
    pub fn get_entity_with_type(&self, id: &str, entity_type: &str) -> Option<Entity> {
        let shard_id = self.route_entity_with_type(id, entity_type);
        self.shards[shard_id].get_entity(id)
    }
    
    /// Check if entity exists
    pub fn contains_entity(&self, id: &str) -> bool {
        match self.strategy {
            ShardStrategy::Hash | ShardStrategy::RoundRobin => {
                let shard_id = self.route_entity(id);
                self.shards[shard_id].contains_entity(id)
            }
            ShardStrategy::EntityType => {
                // Use global index for EntityType strategy
                if let Some(shard_id) = self.global_index.get_entity_shard(id) {
                    return self.shards[shard_id].contains_entity(id);
                }
                // Fallback: search all shards
                for shard in &self.shards {
                    if shard.contains_entity(id) {
                        return true;
                    }
                }
                false
            }
        }
    }
    
    /// Get a relation by ID
    /// 
    /// Uses global index for O(1) shard lookup when available.
    pub fn get_relation(&self, id: &str, source_entity_id: Option<&str>) -> Option<Relation> {
        // Try global index first
        if let Some(shard_id) = self.global_index.get_relation_shard(id) {
            return self.shards[shard_id].get_relation(id);
        }
        
        // Fallback to source-based routing or search
        match source_entity_id {
            Some(source) => {
                let shard_id = self.route_entity(source);
                self.shards[shard_id].get_relation(id)
            }
            None => {
                // Search all shards
                for shard in &self.shards {
                    if let Some(rel) = shard.get_relation(id) {
                        return Some(rel);
                    }
                }
                None
            }
        }
    }
    
    /// Get outgoing relations from an entity
    /// 
    /// For Hash strategy: looks in the entity's shard only
    /// For EntityType strategy: searches all shards (relations are distributed by relation_type)
    pub fn outgoing_from(&self, entity_id: &str) -> Vec<Relation> {
        match self.strategy {
            ShardStrategy::Hash | ShardStrategy::RoundRobin => {
                let shard_id = self.route_entity(entity_id);
                self.shards[shard_id].outgoing_from(entity_id)
            }
            ShardStrategy::EntityType => {
                // For EntityType strategy, relations are distributed by relation_type
                // Need to search all shards
                let mut results = Vec::new();
                for shard in &self.shards {
                    results.extend(shard.outgoing_from(entity_id));
                }
                results
            }
        }
    }
    
    /// Get incoming relations to an entity
    /// 
    /// Uses global index for efficient cross-shard lookups.
    /// For EntityType strategy: searches all shards (relations are distributed by relation_type)
    pub fn incoming_to(&self, entity_id: &str) -> Vec<Relation> {
        match self.strategy {
            ShardStrategy::Hash | ShardStrategy::RoundRobin => {
                let mut results = Vec::new();
                
                // First, get relations from the entity's primary shard (same-shard incoming)
                let primary_shard = self.route_entity(entity_id);
                results.extend(self.shards[primary_shard].incoming_to(entity_id));
                
                // Then, use global index for cross-shard incoming relations
                let cross_refs = self.global_index.get_incoming_refs(entity_id);
                for cross_ref in cross_refs {
                    if cross_ref.shard_id != primary_shard {
                        if let Some(rel) = self.shards[cross_ref.shard_id].get_relation(&cross_ref.relation_id) {
                            results.push(rel);
                        }
                    }
                }
                
                results
            }
            ShardStrategy::EntityType => {
                // For EntityType strategy, relations are distributed by relation_type
                // Need to search all shards
                let mut results = Vec::new();
                for shard in &self.shards {
                    results.extend(shard.incoming_to(entity_id));
                }
                results
            }
        }
    }
    
    /// Get incoming relations to an entity (legacy: searches all shards)
    /// 
    /// Use `incoming_to` instead for better performance with global index.
    pub fn incoming_to_full_scan(&self, entity_id: &str) -> Vec<Relation> {
        let mut results = Vec::new();
        for shard in &self.shards {
            results.extend(shard.incoming_to(entity_id));
        }
        results
    }
    
    /// Find entities by type using global index
    /// 
    /// Uses the global type index for efficient lookups.
    /// Validates results and logs warnings if index appears incomplete.
    pub fn find_entities_by_type(&self, entity_type: &str) -> Vec<Entity> {
        // Use global index to get entity IDs
        let entity_ids = self.global_index.get_entities_by_type(entity_type);
        let indexed_count = entity_ids.len();
        
        if !entity_ids.is_empty() {
            // Fetch entities from their respective shards
            let results: Vec<Entity> = entity_ids
                .into_iter()
                .filter_map(|id| self.get_entity(&id))
                .collect();
            
            // Validate: check if we got all entities we expected
            let fetched_count = results.len();
            if fetched_count != indexed_count {
                tracing::warn!(
                    "Index inconsistency for type '{}': indexed {} entities but only fetched {}. \
                     Index may be stale - consider calling rebuild_global_index()",
                    entity_type,
                    indexed_count,
                    fetched_count
                );
            }
            
            results
        } else {
            // Fallback to shard-by-shard search if type index is empty
            let mut results = Vec::new();
            for shard in &self.shards {
                results.extend(shard.find_entities_by_type(entity_type));
            }
            
            // Log if we found entities via full scan but index was empty
            if !results.is_empty() {
                tracing::warn!(
                    "Index miss for type '{}': global index returned 0 but full scan found {}. \
                     Index may be incomplete - consider calling rebuild_global_index()",
                    entity_type,
                    results.len()
                );
            }
            
            results
        }
    }
    
    /// Find entities by type with pagination (Phase-97)
    ///
    /// Uses the global type index for efficient lookup, fetches only the
    /// requested page of entities from their respective shards.
    ///
    /// Returns `(entities, total_count)` where `entities` is the requested
    /// page and `total_count` is the total number of entities of this type.
    pub fn find_entities_by_type_paginated(
        &self,
        entity_type: &str,
        offset: usize,
        limit: usize,
    ) -> (Vec<Entity>, usize) {
        let (entity_ids, total) = self.global_index
            .get_entities_by_type_paginated(entity_type, offset, limit);
        
        if !entity_ids.is_empty() {
            let results: Vec<Entity> = entity_ids
                .into_iter()
                .filter_map(|id| self.get_entity(&id))
                .collect();
            (results, total)
        } else if total == 0 {
            // type_index was empty; try full scan as fallback and paginate in memory
            let mut all_results = Vec::new();
            for shard in &self.shards {
                all_results.extend(shard.find_entities_by_type(entity_type));
            }
            let total_scan = all_results.len();
            if total_scan > 0 {
                tracing::warn!(
                    "Index miss for type '{}' (paginated): global index returned 0 but full scan found {}. \
                     Consider calling rebuild_global_index()",
                    entity_type,
                    total_scan
                );
            }
            let page: Vec<Entity> = all_results
                .into_iter()
                .skip(offset)
                .take(limit)
                .collect();
            (page, total_scan)
        } else {
            (Vec::new(), total)
        }
    }
    
    /// Count entities of a specific type without loading all data (Phase-97)
    pub fn count_entities_by_type(&self, entity_type: &str) -> usize {
        self.global_index.count_entities_by_type(entity_type)
    }
    
    /// Find entities by type (full scan across all shards)
    /// 
    /// Use `find_entities_by_type` instead for better performance with global index.
    pub fn find_entities_by_type_full_scan(&self, entity_type: &str) -> Vec<Entity> {
        let mut results = Vec::new();
        for shard in &self.shards {
            results.extend(shard.find_entities_by_type(entity_type));
        }
        results
    }
    
    /// Get all entities across all shards
    pub fn all_entities(&self) -> Vec<Entity> {
        let mut results = Vec::new();
        for shard in &self.shards {
            results.extend(shard.all_entities());
        }
        results
    }
    
    /// Get all relations across all shards
    pub fn all_relations(&self) -> Vec<Relation> {
        let mut results = Vec::new();
        for shard in &self.shards {
            results.extend(shard.all_relations());
        }
        results
    }
    
    /// Get total entity count across all shards
    pub fn entity_count(&self) -> usize {
        self.shards.iter().map(|s| s.entity_count()).sum()
    }
    
    /// Get total relation count across all shards
    pub fn relation_count(&self) -> usize {
        self.shards.iter().map(|s| s.relation_count()).sum()
    }
    
    /// Get per-shard statistics for diagnostics
    /// Returns a vector of (shard_id, entity_count, relation_count)
    pub fn per_shard_stats(&self) -> Vec<(usize, usize, usize)> {
        self.shards.iter()
            .map(|s| (s.id(), s.entity_count(), s.relation_count()))
            .collect()
    }
    
    // ========================================================================
    // Metrics & Management
    // ========================================================================
    
    /// Check backpressure and apply if needed
    async fn check_backpressure(&self) -> Result<(), GraphStoreError> {
        let fill_ratio = self.get_total_fill_ratio();
        self.backpressure.apply(fill_ratio).await
            .map_err(|_| GraphStoreError::BackpressureRejected(fill_ratio))
    }
    
    /// Get total queue fill ratio across all shards
    pub fn get_total_fill_ratio(&self) -> f64 {
        let total_len: usize = self.shards.iter().map(|s| s.queue_len()).sum();
        let total_capacity: usize = self.shards.iter().map(|s| s.queue_capacity()).sum();
        
        if total_capacity == 0 {
            0.0
        } else {
            total_len as f64 / total_capacity as f64
        }
    }
    
    /// Get a specific shard
    pub fn get_shard(&self, id: usize) -> Option<Arc<GraphShard>> {
        self.shards.get(id).cloned()
    }
    
    /// Get all shards
    pub fn shards(&self) -> &[Arc<GraphShard>] {
        &self.shards
    }
    
    /// Get metrics snapshot
    pub fn metrics(&self) -> &WriteMetrics {
        &self.metrics
    }
    
    /// Flush all shards
    pub async fn flush(&self) -> Result<(), GraphStoreError> {
        // Currently handled by batch worker timers
        // Could add explicit flush signaling here
        Ok(())
    }
    
    /// Shutdown all shard workers
    pub async fn shutdown(&self) -> Result<(), GraphStoreError> {
        // Persist global index before shutdown
        let _ = self.global_index.persist();
        
        for shard in &self.shards {
            shard.shutdown().await?;
        }
        Ok(())
    }
    
    /// Get configuration
    pub fn config(&self) -> &ConcurrentGraphConfig {
        &self.config
    }
    
    // ========================================================================
    // Global Index API
    // ========================================================================
    
    /// Get the global index
    pub fn global_index(&self) -> &GlobalGraphIndex {
        &self.global_index
    }
    
    /// Get global index statistics
    pub fn global_index_stats(&self) -> GlobalIndexStats {
        self.global_index.stats()
    }
    
    /// Persist the global index to disk
    pub fn persist_global_index(&self) -> std::io::Result<()> {
        self.global_index.persist()
    }
    
    /// Load the global index from disk
    pub fn load_global_index(&self) -> std::io::Result<bool> {
        self.global_index.load()
    }
    
    /// Get cross-shard relation count
    pub fn cross_shard_relation_count(&self) -> usize {
        self.global_index.total_cross_shard_count()
    }
    
    /// Get incoming references for a target entity from other shards
    pub fn get_cross_shard_incoming(&self, target_id: &str) -> Vec<CrossShardRef> {
        self.global_index.get_incoming_refs(target_id)
    }
    
    /// Compact WAL files for all shards.
    ///
    /// Merges multiple .gwal files per shard into a single file each.
    /// Should be called when the graph is not actively receiving writes.
    /// Returns a vector of compaction results, one per shard.
    pub fn compact_wal(&self) -> std::io::Result<Vec<GraphCompactResult>> {
        let mut results = Vec::with_capacity(self.shard_count);
        
        for shard in &self.shards {
            let result = shard.compact_wal()?;
            if result.compacted {
                tracing::info!(
                    "Compacted WAL shard {}: {} → {} files, {} entries",
                    shard.id(), result.files_before, result.files_after, result.entries_total
                );
            }
            results.push(result);
        }
        
        Ok(results)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::graph::{entity_types, relation_types};
    use tempfile::tempdir;
    
    fn create_test_router_no_autostart() -> GraphShardRouter {
        let dir = tempdir().unwrap();
        let config = ConcurrentGraphConfig::new(dir.path())
            .with_shard_count(4)
            .with_auto_start(false);
        GraphShardRouter::new(config)
    }
    
    fn create_test_router_async() -> GraphShardRouter {
        let dir = tempdir().unwrap();
        let config = ConcurrentGraphConfig::new(dir.path())
            .with_shard_count(4)
            .with_auto_start(true);
        GraphShardRouter::new(config)
    }
    
    #[test]
    fn test_router_creation() {
        let router = create_test_router_no_autostart();
        assert_eq!(router.shard_count(), 4);
    }
    
    #[test]
    fn test_hash_routing_consistency() {
        let router = create_test_router_no_autostart();
        
        // Same ID should always route to same shard
        let id = "test-entity-id";
        let shard1 = router.route_entity(id);
        let shard2 = router.route_entity(id);
        assert_eq!(shard1, shard2);
    }
    
    #[test]
    fn test_hash_routing_distribution() {
        let router = create_test_router_no_autostart();
        
        // Different IDs should distribute across shards
        let mut shard_counts = vec![0usize; 4];
        for i in 0..100 {
            let id = format!("entity-{}", i);
            let shard = router.route_entity(&id);
            shard_counts[shard] += 1;
        }
        
        // Each shard should have at least some entities (probabilistic)
        for count in &shard_counts {
            assert!(*count > 0, "Shard distribution is uneven");
        }
    }
    
    #[tokio::test]
    async fn test_router_put_entity() {
        let router = create_test_router_async();
        
        let entity = Entity::new("Alice", entity_types::PERSON);
        let entity_id = entity.id.clone();
        
        let result = router.put_entity_sync(entity).await.unwrap();
        assert!(result.success);
        
        // Verify entity is retrievable
        let retrieved = router.get_entity(&entity_id);
        assert!(retrieved.is_some());
        assert_eq!(retrieved.unwrap().name, "Alice");
    }
    
    #[tokio::test]
    async fn test_router_put_relation() {
        let router = create_test_router_async();
        
        let alice = Entity::new("Alice", entity_types::PERSON);
        let bob = Entity::new("Bob", entity_types::PERSON);
        let alice_id = alice.id.clone();
        let bob_id = bob.id.clone();
        
        router.put_entity_sync(alice).await.unwrap();
        router.put_entity_sync(bob).await.unwrap();
        
        let relation = Relation::new(&alice_id, &bob_id, relation_types::FRIEND);

        
        let result = router.put_relation_sync(relation).await.unwrap();
        assert!(result.success);
        
        // Verify relation is retrievable
        let outgoing = router.outgoing_from(&alice_id);
        assert_eq!(outgoing.len(), 1);
        assert_eq!(outgoing[0].target, bob_id);
    }
    
    #[tokio::test]
    async fn test_router_entity_count() {
        let router = create_test_router_async();
        
        for i in 0..10 {
            let entity = Entity::new(&format!("Person {}", i), entity_types::PERSON);
            router.put_entity_sync(entity).await.unwrap();
        }
        
        assert_eq!(router.entity_count(), 10);
    }
    
    #[tokio::test]
    async fn test_router_find_by_type() {
        let router = create_test_router_async();
        
        // Add some persons
        for i in 0..5 {
            let entity = Entity::new(&format!("Person {}", i), entity_types::PERSON);
            router.put_entity_sync(entity).await.unwrap();
        }
        
        // Add some concepts
        for i in 0..3 {
            let entity = Entity::new(&format!("Concept {}", i), entity_types::CONCEPT);
            router.put_entity_sync(entity).await.unwrap();
        }
        
        let persons = router.find_entities_by_type(entity_types::PERSON);
        assert_eq!(persons.len(), 5);
        
        let concepts = router.find_entities_by_type(entity_types::CONCEPT);
        assert_eq!(concepts.len(), 3);
    }
    
    #[tokio::test]
    async fn test_global_index_stats() {
        let router = create_test_router_async();
        
        // Add entities
        for i in 0..10 {
            let entity = Entity::new(&format!("Person {}", i), entity_types::PERSON);
            router.put_entity_sync(entity).await.unwrap();
        }
        
        // Check global index stats
        let stats = router.global_index_stats();
        assert_eq!(stats.entity_count, 10);
        assert_eq!(stats.type_count, 1); // Only PERSON type
    }
    
    #[tokio::test]
    async fn test_cross_shard_relations() {
        let router = create_test_router_async();
        
        // Create entities that will likely be on different shards
        let mut entity_ids = Vec::new();
        for i in 0..20 {
            let entity = Entity::new(&format!("Person_{}", i), entity_types::PERSON);
            entity_ids.push(entity.id.clone());
            router.put_entity_sync(entity).await.unwrap();
        }
        
        // Create relations between entities
        // Some will be cross-shard (source and target on different shards)
        for i in 0..19 {
            let relation = Relation::new(&entity_ids[i], &entity_ids[i + 1], relation_types::FRIEND);
            router.put_relation_sync(relation).await.unwrap();
        }
        
        // Check that relations are indexed
        let stats = router.global_index_stats();
        assert_eq!(stats.relation_count, 19);
        
        // Cross-shard count should be > 0 (with 4 shards and 20 entities, very likely)
        // But we can't guarantee exact count due to hash distribution
        assert!(stats.entity_count == 20);
    }
    
    #[tokio::test]
    async fn test_incoming_relations_with_global_index() {
        let router = create_test_router_async();
        
        // Create 3 entities
        let alice = Entity::new("Alice", entity_types::PERSON);
        let bob = Entity::new("Bob", entity_types::PERSON);
        let carol = Entity::new("Carol", entity_types::PERSON);
        
        let alice_id = alice.id.clone();
        let bob_id = bob.id.clone();
        let carol_id = carol.id.clone();
        
        router.put_entity_sync(alice).await.unwrap();
        router.put_entity_sync(bob).await.unwrap();
        router.put_entity_sync(carol).await.unwrap();
        
        // Alice -> Bob (friend)
        let rel1 = Relation::new(&alice_id, &bob_id, relation_types::FRIEND);
        // Carol -> Bob (friend)
        let rel2 = Relation::new(&carol_id, &bob_id, relation_types::FRIEND);
        
        router.put_relation_sync(rel1).await.unwrap();
        router.put_relation_sync(rel2).await.unwrap();
        
        // Bob should have 2 incoming relations
        let incoming = router.incoming_to(&bob_id);
        assert_eq!(incoming.len(), 2);
        
        // Verify the sources
        let sources: Vec<_> = incoming.iter().map(|r| r.source.as_str()).collect();
        assert!(sources.contains(&alice_id.as_str()));
        assert!(sources.contains(&carol_id.as_str()));
    }
    
    #[tokio::test]
    async fn test_relation_lookup_with_global_index() {
        let router = create_test_router_async();
        
        let alice = Entity::new("Alice", entity_types::PERSON);
        let bob = Entity::new("Bob", entity_types::PERSON);
        
        let alice_id = alice.id.clone();
        let bob_id = bob.id.clone();
        
        router.put_entity_sync(alice).await.unwrap();
        router.put_entity_sync(bob).await.unwrap();
        
        let relation = Relation::new(&alice_id, &bob_id, relation_types::FRIEND);
        let relation_id = relation.id.clone();
        
        router.put_relation_sync(relation).await.unwrap();
        
        // Should find relation without providing source (using global index)
        let found = router.get_relation(&relation_id, None);
        assert!(found.is_some());
        assert_eq!(found.unwrap().source, alice_id);
    }
    
    // ========================================================================
    // Phase 3: Type Mapping Tests
    // ========================================================================
    
    #[tokio::test]
    async fn test_type_mapping_routing_entities() {
        // Create config with shard_names to trigger auto type mapping
        let temp_dir = tempfile::tempdir().unwrap();
        let config = ConcurrentGraphConfig::new(temp_dir.path().join("test_type_mapping"))
            .with_shard_count(4)
            .with_shard_names(vec!["person", "organization", "relation", "tag"])
            .with_wal_enabled(false);
        
        let router = GraphShardRouter::new(config);
        
        // Test entity routing with type mapping
        // person -> shard 0
        assert_eq!(router.route_entity_with_type("id1", "person"), 0);
        
        // organization types -> shard 1
        assert_eq!(router.route_entity_with_type("id2", "organization"), 1);
        assert_eq!(router.route_entity_with_type("id3", "company"), 1);
        assert_eq!(router.route_entity_with_type("id4", "school"), 1);
        
        // tag types -> shard 3
        assert_eq!(router.route_entity_with_type("id5", "tag"), 3);
        assert_eq!(router.route_entity_with_type("id6", "skill"), 3);
        assert_eq!(router.route_entity_with_type("id7", "interest"), 3);
        
        // unknown type -> _default (shard 3)
        assert_eq!(router.route_entity_with_type("id8", "unknown_type"), 3);
    }
    
    #[tokio::test]
    async fn test_type_mapping_routing_relations() {
        // Create config with shard_names including "relation"
        let temp_dir = tempfile::tempdir().unwrap();
        let config = ConcurrentGraphConfig::new(temp_dir.path().join("test_type_mapping_rel"))
            .with_shard_count(4)
            .with_shard_names(vec!["person", "organization", "relation", "tag"])
            .with_wal_enabled(false);
        
        let router = GraphShardRouter::new(config);
        
        // All relations should go to shard 2 (the "relation" shard)
        let rel1 = Relation::new("alice", "bob", "friend");
        let rel2 = Relation::new("alice", "company1", "work_for");
        let rel3 = Relation::new("bob", "tag1", "has_tag");
        
        assert_eq!(router.route_relation(&rel1), 2);
        assert_eq!(router.route_relation(&rel2), 2);
        assert_eq!(router.route_relation(&rel3), 2);
    }
    
    #[tokio::test]
    async fn test_explicit_type_mapping() {
        use std::collections::HashMap;
        
        let temp_dir = tempfile::tempdir().unwrap();
        
        // Create explicit type mapping
        let mut mapping = HashMap::new();
        mapping.insert("person".to_string(), 0);
        mapping.insert("company".to_string(), 2);  // Different from auto-generated
        mapping.insert("_default".to_string(), 3);
        
        let config = ConcurrentGraphConfig::new(temp_dir.path().join("test_explicit_mapping"))
            .with_shard_count(4)
            .with_type_shard_mapping(mapping)
            .with_wal_enabled(false);
        
        let router = GraphShardRouter::new(config);
        
        // Verify explicit mapping takes precedence
        assert_eq!(router.route_entity_with_type("id1", "person"), 0);
        assert_eq!(router.route_entity_with_type("id2", "company"), 2);
        assert_eq!(router.route_entity_with_type("id3", "unknown"), 3);
    }
    
    #[tokio::test]
    async fn test_fallback_to_hash_without_mapping() {
        // Config without shard_names or type_shard_mapping
        let temp_dir = tempfile::tempdir().unwrap();
        let config = ConcurrentGraphConfig::new(temp_dir.path().join("test_no_mapping"))
            .with_shard_count(4)
            .with_wal_enabled(false);
        
        let router = GraphShardRouter::new(config);
        
        // Should use hash(entity_type) - same type always goes to same shard
        let shard1 = router.route_entity_with_type("id1", "person");
        let shard2 = router.route_entity_with_type("id2", "person");
        let shard3 = router.route_entity_with_type("id3", "person");
        
        // All "person" entities should go to the same shard
        assert_eq!(shard1, shard2);
        assert_eq!(shard2, shard3);
        
        // But we don't know which shard exactly (hash-dependent)
        assert!(shard1 < 4);
    }
    
    #[tokio::test]
    async fn test_type_mapping_with_actual_writes() {
        let temp_dir = tempfile::tempdir().unwrap();
        let path = temp_dir.path().join("type_mapping_writes");
        
        let config = ConcurrentGraphConfig::new(&path)
            .with_shard_count(4)
            .with_shard_names(vec!["person", "organization", "relation", "tag"])
            .with_wal_enabled(false);
        
        let router = GraphShardRouter::new(config);
        router.start_workers();
        
        // Write entities of different types
        let person = Entity::new("Alice", "person");
        let company = Entity::new("Acme Corp", "company");
        let tag = Entity::new("Engineer", "tag");
        
        let person_id = person.id.clone();
        let company_id = company.id.clone();
        let tag_id = tag.id.clone();
        
        router.put_entity_sync(person).await.unwrap();
        router.put_entity_sync(company).await.unwrap();
        router.put_entity_sync(tag).await.unwrap();
        
        // Verify entities can be retrieved
        assert!(router.get_entity(&person_id).is_some());
        assert!(router.get_entity(&company_id).is_some());
        assert!(router.get_entity(&tag_id).is_some());
        
        // Verify type-based lookup works
        let persons = router.find_entities_by_type("person");
        let companies = router.find_entities_by_type("company");
        let tags = router.find_entities_by_type("tag");
        
        assert_eq!(persons.len(), 1);
        assert_eq!(companies.len(), 1);
        assert_eq!(tags.len(), 1);
        
        router.shutdown().await.unwrap();
    }
}
