cstats-core 0.1.1

Core library for cstats - statistical analysis and metrics collection
Documentation
//! Configuration management for cstats

use std::path::{Path, PathBuf};

use serde::{Deserialize, Serialize};

use crate::{api::types::AnthropicConfig, Error, Result};

/// Main configuration structure for cstats
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Config {
    /// Database configuration
    pub database: DatabaseConfig,

    /// API configuration
    pub api: ApiConfig,

    /// Cache configuration
    pub cache: CacheConfig,

    /// Statistics configuration
    pub stats: StatsConfig,
}

/// Database configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DatabaseConfig {
    /// Database URL or path
    pub url: String,

    /// Maximum number of connections
    pub max_connections: u32,

    /// Connection timeout in seconds
    pub timeout_seconds: u64,
}

/// API configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiConfig {
    /// Base URL for API endpoints
    pub base_url: Option<String>,

    /// API timeout in seconds
    pub timeout_seconds: u64,

    /// Request retry attempts
    pub retry_attempts: u32,

    /// Anthropic API configuration
    pub anthropic: Option<AnthropicConfig>,
}

/// Cache configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheConfig {
    /// Cache directory path
    pub cache_dir: PathBuf,

    /// Maximum cache size in bytes
    pub max_size_bytes: u64,

    /// Cache TTL in seconds
    pub ttl_seconds: u64,
}

/// Statistics configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StatsConfig {
    /// Default statistics to collect
    pub default_metrics: Vec<String>,

    /// Sampling rate for statistics
    pub sampling_rate: f64,

    /// Aggregation window in seconds
    pub aggregation_window_seconds: u64,
}

impl Default for DatabaseConfig {
    fn default() -> Self {
        Self {
            url: "sqlite:./cstats.db".to_string(),
            max_connections: 10,
            timeout_seconds: 30,
        }
    }
}

impl Default for ApiConfig {
    fn default() -> Self {
        Self {
            base_url: None,
            timeout_seconds: 30,
            retry_attempts: 3,
            anthropic: None,
        }
    }
}

impl Default for CacheConfig {
    fn default() -> Self {
        Self {
            cache_dir: dirs::cache_dir()
                .unwrap_or_else(std::env::temp_dir)
                .join("cstats"),
            max_size_bytes: 100 * 1024 * 1024, // 100MB
            ttl_seconds: 3600,                 // 1 hour
        }
    }
}

impl Default for StatsConfig {
    fn default() -> Self {
        Self {
            default_metrics: vec![
                "execution_time".to_string(),
                "memory_usage".to_string(),
                "cpu_usage".to_string(),
            ],
            sampling_rate: 1.0,
            aggregation_window_seconds: 300, // 5 minutes
        }
    }
}

#[cfg(test)]
mod tests;

impl Config {
    /// Get the default configuration directory path
    pub fn default_config_dir() -> Result<PathBuf> {
        dirs::home_dir()
            .map(|home| home.join(".cstats"))
            .ok_or_else(|| Error::config("Unable to determine home directory"))
    }

    /// Get the default configuration file path
    pub fn default_config_path() -> Result<PathBuf> {
        Ok(Self::default_config_dir()?.join("config.json"))
    }

    /// Load configuration from file
    pub async fn load_from_file(path: &Path) -> Result<Self> {
        if !path.exists() {
            return Err(Error::config(format!(
                "Configuration file does not exist: {}",
                path.display()
            )));
        }

        let content = tokio::fs::read_to_string(path)
            .await
            .map_err(|e| Error::config(format!("Failed to read config file: {}", e)))?;

        let config: Config = serde_json::from_str(&content)
            .map_err(|e| Error::config(format!("Failed to parse config file: {}", e)))?;

        Ok(config)
    }

    /// Save configuration to file
    pub async fn save_to_file(&self, path: &Path) -> Result<()> {
        // Create parent directory if it doesn't exist
        if let Some(parent) = path.parent() {
            tokio::fs::create_dir_all(parent)
                .await
                .map_err(|e| Error::config(format!("Failed to create config directory: {}", e)))?;
        }

        let content = serde_json::to_string_pretty(self)
            .map_err(|e| Error::config(format!("Failed to serialize config: {}", e)))?;

        tokio::fs::write(path, content)
            .await
            .map_err(|e| Error::config(format!("Failed to write config file: {}", e)))?;

        Ok(())
    }

    /// Load configuration with priority: env vars > config file > defaults
    pub async fn load() -> Result<Self> {
        // Start with defaults
        let mut config = Self::default();

        // Try to load from default config file
        if let Ok(config_path) = Self::default_config_path() {
            if config_path.exists() {
                match Self::load_from_file(&config_path).await {
                    Ok(file_config) => {
                        config = file_config;
                    }
                    Err(e) => {
                        tracing::warn!(
                            "Failed to load config from {}: {}",
                            config_path.display(),
                            e
                        );
                    }
                }
            }
        }

        // Override with environment variables
        config.apply_env_overrides();

        Ok(config)
    }

    /// Load configuration from a specific file path with env overrides
    pub async fn load_from_path(path: &Path) -> Result<Self> {
        let mut config = if path.exists() {
            Self::load_from_file(path).await?
        } else {
            Self::default()
        };

        // Apply environment variable overrides
        config.apply_env_overrides();

        Ok(config)
    }

    /// Apply environment variable overrides to the configuration
    fn apply_env_overrides(&mut self) {
        // Override Anthropic API key from environment
        if let Ok(api_key) = std::env::var("ANTHROPIC_API_KEY") {
            if !api_key.is_empty() {
                let mut anthropic_config = self.api.anthropic.clone().unwrap_or_default();
                anthropic_config.api_key = api_key;
                self.api.anthropic = Some(anthropic_config);
            }
        }

        // Override database URL if set
        if let Ok(db_url) = std::env::var("CSTATS_DATABASE_URL") {
            self.database.url = db_url;
        }

        // Override base URL if set
        if let Ok(base_url) = std::env::var("CSTATS_API_BASE_URL") {
            self.api.base_url = Some(base_url);
        }
    }

    /// Get the Anthropic API key from config (use effective_anthropic_api_key for env priority)
    pub fn get_anthropic_api_key(&self) -> Option<&str> {
        // Return config file API key (use effective_anthropic_api_key to include env vars)
        self.api
            .anthropic
            .as_ref()
            .map(|config| config.api_key.as_str())
    }

    /// Check if the configuration has a valid Anthropic API key
    pub fn has_anthropic_api_key(&self) -> bool {
        // Check environment variable first
        if let Ok(api_key) = std::env::var("ANTHROPIC_API_KEY") {
            if !api_key.is_empty() {
                return true;
            }
        }

        // Then check config
        self.api
            .anthropic
            .as_ref()
            .map(|config| !config.api_key.is_empty())
            .unwrap_or(false)
    }

    /// Get the effective Anthropic API key (env var takes precedence)
    pub fn effective_anthropic_api_key(&self) -> Option<String> {
        // Environment variable takes precedence
        if let Ok(api_key) = std::env::var("ANTHROPIC_API_KEY") {
            if !api_key.is_empty() {
                return Some(api_key);
            }
        }

        // Fall back to config file
        self.api.anthropic.as_ref().and_then(|config| {
            if config.api_key.is_empty() {
                None
            } else {
                Some(config.api_key.clone())
            }
        })
    }

    /// Validate the configuration
    pub fn validate(&self) -> Result<()> {
        let mut errors = Vec::new();

        // Validate database config
        if self.database.url.is_empty() {
            errors.push("Database URL cannot be empty".to_string());
        }

        if self.database.max_connections == 0 {
            errors.push("Database max_connections must be greater than 0".to_string());
        }

        if self.database.timeout_seconds == 0 {
            errors.push("Database timeout_seconds must be greater than 0".to_string());
        }

        // Validate cache config
        if self.cache.max_size_bytes == 0 {
            errors.push("Cache max_size_bytes must be greater than 0".to_string());
        }

        if self.cache.ttl_seconds == 0 {
            errors.push("Cache ttl_seconds must be greater than 0".to_string());
        }

        // Validate stats config
        if !(0.0..=1.0).contains(&self.stats.sampling_rate) {
            errors.push("Stats sampling_rate must be between 0.0 and 1.0".to_string());
        }

        if self.stats.aggregation_window_seconds == 0 {
            errors.push("Stats aggregation_window_seconds must be greater than 0".to_string());
        }

        // Validate API config
        if let Some(ref anthropic) = self.api.anthropic {
            if anthropic.timeout_seconds == 0 {
                errors.push("Anthropic timeout_seconds must be greater than 0".to_string());
            }

            if anthropic.max_retry_delay_ms < anthropic.initial_retry_delay_ms {
                errors.push(
                    "Anthropic max_retry_delay_ms must be >= initial_retry_delay_ms".to_string(),
                );
            }
        }

        if !errors.is_empty() {
            return Err(Error::config(format!(
                "Configuration validation failed:\n  - {}",
                errors.join("\n  - ")
            )));
        }

        Ok(())
    }

    /// Load configuration from environment variables and defaults
    pub fn from_env() -> Result<Self> {
        let mut config = Self::default();
        config.apply_env_overrides();
        Ok(config)
    }
}