//! Configuration for concurrent graph store
//!
//! Provides configuration structures for the high-concurrency graph store.

use std::collections::HashMap;
use std::path::PathBuf;

use crate::storage::concurrent::{
    WriteQueueConfig, BackpressureConfig,
    batch_aggregator::BatchConfig,
};

use super::wal::GraphWALConfig;

/// Strategy for routing writes to shards
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ShardStrategy {
    /// Route based on entity/relation ID hash (default)
    Hash,
    
    /// Route based on entity type
    EntityType,
    
    /// Round-robin distribution (for testing)
    RoundRobin,
}

impl Default for ShardStrategy {
    fn default() -> Self {
        // Default to EntityType strategy for better data locality
        // Same entity_type data will be routed to the same shard
        ShardStrategy::EntityType
    }
}

/// Configuration for the concurrent graph store
#[derive(Debug, Clone)]
pub struct ConcurrentGraphConfig {
    /// Data directory path
    pub path: PathBuf,
    
    /// Number of shards (0 = auto, typically CPU cores * 2)
    pub shard_count: usize,
    
    /// Sharding strategy
    pub shard_strategy: ShardStrategy,
    
    /// Write queue configuration per shard
    pub queue_config: WriteQueueConfig,
    
    /// Batch aggregation configuration
    pub batch_config: BatchConfig,
    
    /// Backpressure control configuration
    pub backpressure_config: BackpressureConfig,
    
    /// Whether to auto-start worker threads
    pub auto_start: bool,
    
    /// Enable write-ahead logging for durability
    pub wal_enabled: bool,
    
    /// WAL configuration
    pub wal_config: GraphWALConfig,
    
    /// 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"]` produces:
    /// - `shard_0_person/`
    /// - `shard_1_organization/`
    /// - `shard_2_relation/`
    /// - `shard_3_tag/`
    /// 
    /// If not set or fewer names than shards, defaults to `shard_{id}` format.
    pub shard_names: Option<Vec<String>>,
    
    /// Type to shard mapping for fixed routing (Phase 3 feature)
    /// 
    /// When set with EntityType strategy, entity types will be routed to specific shards
    /// based on this mapping instead of hash(entity_type).
    /// 
    /// This is auto-generated from `shard_names` if not explicitly set.
    /// 
    /// # Example
    /// ```ignore
    /// let mut mapping = HashMap::new();
    /// mapping.insert("person".to_string(), 0);
    /// mapping.insert("company".to_string(), 1);
    /// mapping.insert("organization".to_string(), 1);
    /// mapping.insert("tag".to_string(), 3);
    /// mapping.insert("_default".to_string(), 3);  // fallback for unknown types
    /// ```
    pub type_shard_mapping: Option<HashMap<String, usize>>,
    
    /// Dedicated shard ID for all relations (Phase 3 feature)
    /// 
    /// When set with EntityType strategy, all relations will be routed to this shard
    /// regardless of relation_type.
    /// 
    /// This is auto-set when `shard_names` contains "relation".
    pub relation_shard_id: Option<usize>,
}

impl Default for ConcurrentGraphConfig {
    fn default() -> Self {
        let path = PathBuf::from("data/graph");
        Self {
            path: path.clone(),
            shard_count: 0, // Auto-detect
            shard_strategy: ShardStrategy::EntityType,
            queue_config: WriteQueueConfig {
                capacity: 10_000,
                batch_size: 100,
                flush_interval_ms: 50,
                send_timeout_ms: 1000,
            },
            batch_config: BatchConfig {
                max_batch_size: 500,
                max_wait_ms: 10,
                max_batch_bytes: 10 * 1024 * 1024, // 10MB
                flush_on_full: true,
            },
            backpressure_config: BackpressureConfig {
                queue_low_watermark: 0.7,
                queue_high_watermark: 0.9,
                reject_threshold: 0.95,
                base_slowdown_ms: 1,
                max_slowdown_ms: 100,
                exponential_backoff: true,
            },
            auto_start: true,
            wal_enabled: true,
            wal_config: GraphWALConfig::new(path.join("wal")),
            shard_names: None,
            type_shard_mapping: None,
            relation_shard_id: None,
        }
    }
}

impl ConcurrentGraphConfig {
    /// Create a new configuration with the given path
    pub fn new(path: impl Into<PathBuf>) -> Self {
        let path = path.into();
        Self {
            wal_config: GraphWALConfig::new(path.join("wal")),
            path,
            ..Default::default()
        }
    }
    
    /// Set shard count (0 for auto-detection)
    pub fn with_shard_count(mut self, count: usize) -> Self {
        self.shard_count = count;
        self
    }
    
    /// Set sharding strategy
    pub fn with_shard_strategy(mut self, strategy: ShardStrategy) -> Self {
        self.shard_strategy = strategy;
        self
    }
    
    /// Set queue configuration
    pub fn with_queue_config(mut self, config: WriteQueueConfig) -> Self {
        self.queue_config = config;
        self
    }
    
    /// Set batch configuration
    pub fn with_batch_config(mut self, config: BatchConfig) -> Self {
        self.batch_config = config;
        self
    }
    
    /// Set backpressure configuration
    pub fn with_backpressure_config(mut self, config: BackpressureConfig) -> Self {
        self.backpressure_config = config;
        self
    }
    
    /// Enable or disable auto-start
    pub fn with_auto_start(mut self, auto_start: bool) -> Self {
        self.auto_start = auto_start;
        self
    }
    
    /// Enable or disable WAL
    pub fn with_wal_enabled(mut self, enabled: bool) -> Self {
        self.wal_enabled = enabled;
        self
    }
    
    /// Set shard names for descriptive directory naming
    /// 
    /// # Example
    /// ```
    /// use aifastdb_core::social::concurrent::ConcurrentGraphConfig;
    /// 
    /// let config = ConcurrentGraphConfig::new("data/graph")
    ///     .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
    /// ```
    /// use aifastdb_core::social::concurrent::ConcurrentGraphConfig;
    /// 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 = ConcurrentGraphConfig::new("data/graph")
    ///     .with_shard_count(4)
    ///     .with_type_shard_mapping(mapping);
    /// ```
    pub fn with_type_shard_mapping(mut self, mapping: 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
    }
    
    /// Get the directory name for a specific shard
    /// 
    /// Returns `shard_{id}_{name}` if a name is configured for this shard,
    /// otherwise returns `shard_{id}`.
    pub fn shard_dir_name(&self, id: usize) -> String {
        if let Some(ref names) = self.shard_names {
            if let Some(name) = names.get(id) {
                return format!("shard_{}_{}", id, name);
            }
        }
        format!("shard_{}", id)
    }
    
    /// Get the actual shard count (resolves auto-detection)
    pub fn actual_shard_count(&self) -> usize {
        if self.shard_count == 0 {
            // Auto-detect: use CPU cores * 2, min 4, max 64
            let cpus = num_cpus::get();
            (cpus * 2).clamp(4, 64)
        } else {
            self.shard_count
        }
    }
    
    /// Configuration optimized for high throughput
    /// 
    /// Larger batches, longer wait times, more aggressive batching.
    pub fn high_throughput(mut self) -> Self {
        self.batch_config = BatchConfig {
            max_batch_size: 1000,
            max_wait_ms: 20,
            max_batch_bytes: 50 * 1024 * 1024, // 50MB
            flush_on_full: true,
        };
        self.queue_config = WriteQueueConfig {
            capacity: 50_000,
            batch_size: 500,
            flush_interval_ms: 100,
            send_timeout_ms: 5000,
        };
        self
    }
    
    /// Configuration optimized for low latency
    /// 
    /// Smaller batches, shorter wait times, more frequent flushes.
    pub fn low_latency(mut self) -> Self {
        self.batch_config = BatchConfig {
            max_batch_size: 50,
            max_wait_ms: 2,
            max_batch_bytes: 1024 * 1024, // 1MB
            flush_on_full: true,
        };
        self.queue_config = WriteQueueConfig {
            capacity: 5_000,
            batch_size: 50,
            flush_interval_ms: 10,
            send_timeout_ms: 500,
        };
        self
    }
    
    /// Configuration for balanced performance
    pub fn balanced(mut self) -> Self {
        self.batch_config = BatchConfig {
            max_batch_size: 200,
            max_wait_ms: 10,
            max_batch_bytes: 5 * 1024 * 1024, // 5MB
            flush_on_full: true,
        };
        self.queue_config = WriteQueueConfig {
            capacity: 10_000,
            batch_size: 100,
            flush_interval_ms: 50,
            send_timeout_ms: 1000,
        };
        self
    }
    
    /// Auto-generate type_shard_mapping from shard_names
    /// 
    /// This method generates a mapping based on the configured shard_names.
    /// Each shard name is expanded to include related types:
    /// 
    /// - "person" -> person
    /// - "organization" -> organization, company, school, city
    /// - "relation" -> sets relation_shard_id
    /// - "tag" -> tag, skill, interest, attribute, hobby, zodiac, industry, age_group, _default
    /// 
    /// If type_shard_mapping is already set, this method does nothing.
    pub fn auto_generate_type_mapping(&mut self) {
        // Don't override existing mapping
        if self.type_shard_mapping.is_some() {
            return;
        }
        
        // Only generate if shard_names is configured
        let names = match &self.shard_names {
            Some(names) => names.clone(),
            None => return,
        };
        
        let mut mapping = HashMap::new();
        
        for (idx, name) in names.iter().enumerate() {
            match name.as_str() {
                "person" => {
                    mapping.insert("person".to_string(), idx);
                }
                "organization" => {
                    mapping.insert("organization".to_string(), idx);
                    mapping.insert("company".to_string(), idx);
                    mapping.insert("school".to_string(), idx);
                    mapping.insert("city".to_string(), idx);
                }
                "relation" => {
                    mapping.insert("relation".to_string(), idx);
                    // Set dedicated relation shard
                    self.relation_shard_id = Some(idx);
                }
                "tag" => {
                    mapping.insert("tag".to_string(), idx);
                    mapping.insert("skill".to_string(), idx);
                    mapping.insert("interest".to_string(), idx);
                    mapping.insert("attribute".to_string(), idx);
                    mapping.insert("hobby".to_string(), idx);
                    mapping.insert("zodiac".to_string(), idx);
                    mapping.insert("industry".to_string(), idx);
                    mapping.insert("age_group".to_string(), idx);
                    // Set as default fallback
                    mapping.insert("_default".to_string(), idx);
                }
                other => {
                    // For any other name, map it directly
                    mapping.insert(other.to_string(), idx);
                }
            }
        }
        
        if !mapping.is_empty() {
            self.type_shard_mapping = Some(mapping);
        }
    }
    
    /// Get the shard ID for a given entity type using type mapping
    /// 
    /// Returns None if no mapping is configured or type is not found.
    pub fn get_type_shard(&self, entity_type: &str) -> Option<usize> {
        if let Some(ref mapping) = self.type_shard_mapping {
            // Try exact match first
            if let Some(&shard_id) = mapping.get(entity_type) {
                return Some(shard_id);
            }
            // Try default fallback
            if let Some(&shard_id) = mapping.get("_default") {
                return Some(shard_id);
            }
        }
        None
    }
}

/// Configuration for a single graph shard
#[derive(Debug, Clone)]
pub struct GraphShardConfig {
    /// Shard ID
    pub id: usize,
    
    /// Write queue configuration
    pub queue_config: WriteQueueConfig,
    
    /// Batch configuration
    pub batch_config: BatchConfig,
    
    /// Path for shard data
    pub path: PathBuf,
}

impl GraphShardConfig {
    /// Create a new shard configuration
    pub fn new(id: usize, base_path: impl Into<PathBuf>) -> Self {
        let base_path = base_path.into();
        Self {
            id,
            queue_config: WriteQueueConfig::default(),
            batch_config: BatchConfig::default(),
            path: base_path.join(format!("shard_{}", id)),
        }
    }
    
    /// Create from parent concurrent config
    pub fn from_concurrent_config(id: usize, config: &ConcurrentGraphConfig) -> Self {
        Self {
            id,
            queue_config: config.queue_config.clone(),
            batch_config: config.batch_config.clone(),
            path: config.path.join(format!("shard_{}", id)),
        }
    }
    
    /// Set queue configuration
    pub fn with_queue_config(mut self, config: WriteQueueConfig) -> Self {
        self.queue_config = config;
        self
    }
    
    /// Set batch configuration
    pub fn with_batch_config(mut self, config: BatchConfig) -> Self {
        self.batch_config = config;
        self
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_default_config() {
        let config = ConcurrentGraphConfig::default();
        assert_eq!(config.shard_count, 0);
        assert_eq!(config.shard_strategy, ShardStrategy::EntityType);
        assert!(config.auto_start);
        assert!(config.wal_enabled);
    }
    
    #[test]
    fn test_auto_shard_count() {
        let config = ConcurrentGraphConfig::default();
        let actual = config.actual_shard_count();
        assert!(actual >= 4);
        assert!(actual <= 64);
    }
    
    #[test]
    fn test_explicit_shard_count() {
        let config = ConcurrentGraphConfig::default().with_shard_count(16);
        assert_eq!(config.actual_shard_count(), 16);
    }
    
    #[test]
    fn test_high_throughput_config() {
        let config = ConcurrentGraphConfig::default().high_throughput();
        assert_eq!(config.batch_config.max_batch_size, 1000);
        assert_eq!(config.queue_config.capacity, 50_000);
    }
    
    #[test]
    fn test_low_latency_config() {
        let config = ConcurrentGraphConfig::default().low_latency();
        assert_eq!(config.batch_config.max_batch_size, 50);
        assert_eq!(config.batch_config.max_wait_ms, 2);
    }
    
    #[test]
    fn test_shard_config_from_concurrent() {
        let config = ConcurrentGraphConfig::new("test/path").with_shard_count(8);
        let shard_config = GraphShardConfig::from_concurrent_config(3, &config);
        
        assert_eq!(shard_config.id, 3);
        assert_eq!(shard_config.path, PathBuf::from("test/path/shard_3"));
    }
    
    #[test]
    fn test_shard_dir_name_default() {
        let config = ConcurrentGraphConfig::new("test/path").with_shard_count(4);
        
        // Without shard_names, should use default format
        assert_eq!(config.shard_dir_name(0), "shard_0");
        assert_eq!(config.shard_dir_name(1), "shard_1");
        assert_eq!(config.shard_dir_name(2), "shard_2");
        assert_eq!(config.shard_dir_name(3), "shard_3");
    }
    
    #[test]
    fn test_shard_dir_name_with_names() {
        let config = ConcurrentGraphConfig::new("test/path")
            .with_shard_count(4)
            .with_shard_names(vec!["person", "organization", "relation", "tag"]);
        
        // With shard_names, should use descriptive format
        assert_eq!(config.shard_dir_name(0), "shard_0_person");
        assert_eq!(config.shard_dir_name(1), "shard_1_organization");
        assert_eq!(config.shard_dir_name(2), "shard_2_relation");
        assert_eq!(config.shard_dir_name(3), "shard_3_tag");
    }
    
    #[test]
    fn test_shard_dir_name_partial_names() {
        let config = ConcurrentGraphConfig::new("test/path")
            .with_shard_count(4)
            .with_shard_names(vec!["person", "organization"]); // Only 2 names for 4 shards
        
        // Named shards use descriptive format
        assert_eq!(config.shard_dir_name(0), "shard_0_person");
        assert_eq!(config.shard_dir_name(1), "shard_1_organization");
        // Unnamed shards fall back to default format
        assert_eq!(config.shard_dir_name(2), "shard_2");
        assert_eq!(config.shard_dir_name(3), "shard_3");
    }
    
    #[test]
    fn test_auto_generate_type_mapping() {
        let mut config = ConcurrentGraphConfig::new("test/path")
            .with_shard_count(4)
            .with_shard_names(vec!["person", "organization", "relation", "tag"]);
        
        config.auto_generate_type_mapping();
        
        // Verify mapping was generated
        assert!(config.type_shard_mapping.is_some());
        let mapping = config.type_shard_mapping.as_ref().unwrap();
        
        // person shard (0)
        assert_eq!(mapping.get("person"), Some(&0));
        
        // organization shard (1)
        assert_eq!(mapping.get("organization"), Some(&1));
        assert_eq!(mapping.get("company"), Some(&1));
        assert_eq!(mapping.get("school"), Some(&1));
        
        // relation shard (2)
        assert_eq!(mapping.get("relation"), Some(&2));
        assert_eq!(config.relation_shard_id, Some(2));
        
        // tag shard (3)
        assert_eq!(mapping.get("tag"), Some(&3));
        assert_eq!(mapping.get("skill"), Some(&3));
        assert_eq!(mapping.get("_default"), Some(&3));
    }
    
    #[test]
    fn test_get_type_shard() {
        let mut config = ConcurrentGraphConfig::new("test/path")
            .with_shard_count(4)
            .with_shard_names(vec!["person", "organization", "relation", "tag"]);
        
        config.auto_generate_type_mapping();
        
        // Known types
        assert_eq!(config.get_type_shard("person"), Some(0));
        assert_eq!(config.get_type_shard("company"), Some(1));
        assert_eq!(config.get_type_shard("tag"), Some(3));
        
        // Unknown type falls back to _default
        assert_eq!(config.get_type_shard("unknown_type"), Some(3));
    }
    
    #[test]
    fn test_type_mapping_not_overridden() {
        let mut manual_mapping = HashMap::new();
        manual_mapping.insert("person".to_string(), 2); // Different from auto
        
        let mut config = ConcurrentGraphConfig::new("test/path")
            .with_shard_count(4)
            .with_shard_names(vec!["person", "organization", "relation", "tag"])
            .with_type_shard_mapping(manual_mapping);
        
        config.auto_generate_type_mapping();
        
        // Manual mapping should not be overridden
        let mapping = config.type_shard_mapping.as_ref().unwrap();
        assert_eq!(mapping.get("person"), Some(&2));
    }
    
    #[test]
    fn test_no_mapping_without_shard_names() {
        let mut config = ConcurrentGraphConfig::new("test/path")
            .with_shard_count(4);
        
        config.auto_generate_type_mapping();
        
        // No mapping should be generated without shard_names
        assert!(config.type_shard_mapping.is_none());
    }
}
