vielpork 0.1.3

A high-performance multi-threaded HTTP downloader with extensible reporting and resolution strategies.
Documentation
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use toml::from_str as from_toml_str;
use toml::to_string_pretty;

/// Configuration for vielpork CLI
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
    /// Default download directory
    pub download_dir: String,
    /// Default concurrency level
    pub concurrency: u32,
    /// Default timeout in seconds
    pub timeout: u64,
    /// Default max retries
    pub max_retries: u32,
    /// Default user agent
    pub user_agent: Option<String>,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            download_dir: "downloads".to_string(),
            concurrency: 4,
            timeout: 300, // 5 minutes - long enough for most downloads
            max_retries: 3,
            user_agent: None,
        }
    }
}

pub struct ConfigManager;

impl ConfigManager {
    /// Get config file path
    pub fn config_path() -> PathBuf {
        let config_dir = dirs::config_dir()
            .unwrap_or_else(|| PathBuf::from("."))
            .join("vielpork");

        config_dir.join("config.toml")
    }

    /// Ensure config directory exists
    pub fn ensure_config_dir() -> std::io::Result<()> {
        let config_path = Self::config_path();
        let config_dir = config_path.parent().unwrap();
        if !config_dir.exists() {
            fs::create_dir_all(config_dir)?;
        }
        Ok(())
    }

    /// Load configuration from file, or return default if not found
    pub fn load() -> Result<Config, String> {
        let config_path = Self::config_path();

        if !config_path.exists() {
            return Ok(Config::default());
        }

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

        from_toml_str(&content).map_err(|e| format!("Failed to parse config: {}", e))
    }

    /// Save configuration to file
    pub fn save(config: &Config) -> Result<(), String> {
        Self::ensure_config_dir()
            .map_err(|e| format!("Failed to create config directory: {}", e))?;

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

        fs::write(&config_path, content)
            .map_err(|e| format!("Failed to write config file: {}", e))?;

        Ok(())
    }

    /// Display current configuration
    pub fn display(config: &Config) {
        println!("📋 Current Configuration:");
        println!("  Download Directory: {}", config.download_dir);
        println!("  Concurrency: {}", config.concurrency);
        println!("  Timeout: {}s", config.timeout);
        println!("  Max Retries: {}", config.max_retries);
        if let Some(ua) = &config.user_agent {
            println!("  User Agent: {}", ua);
        } else {
            println!("  User Agent: (default)");
        }
        println!("\n📁 Config file: {}", Self::config_path().display());
    }

    /// Get a specific config value
    pub fn get(config: &Config, key: &str) -> Option<String> {
        match key {
            "download_dir" => Some(config.download_dir.clone()),
            "concurrency" => Some(config.concurrency.to_string()),
            "timeout" => Some(config.timeout.to_string()),
            "max_retries" => Some(config.max_retries.to_string()),
            "user_agent" => config.user_agent.clone(),
            _ => None,
        }
    }

    /// Set a specific config value
    pub fn set(config: &mut Config, key: &str, value: &str) -> Result<(), String> {
        match key {
            "download_dir" => {
                config.download_dir = value.to_string();
            }
            "concurrency" => {
                config.concurrency = value
                    .parse()
                    .map_err(|_| "Invalid concurrency value (must be number)".to_string())?;
            }
            "timeout" => {
                config.timeout = value
                    .parse()
                    .map_err(|_| "Invalid timeout value (must be number)".to_string())?;
            }
            "max_retries" => {
                config.max_retries = value
                    .parse()
                    .map_err(|_| "Invalid max_retries value (must be number)".to_string())?;
            }
            "user_agent" => {
                config.user_agent = Some(value.to_string());
            }
            _ => {
                return Err(format!("Unknown config key: {}", key));
            }
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_default_config() {
        let config = Config::default();
        assert_eq!(config.download_dir, "downloads");
        assert_eq!(config.concurrency, 4);
        assert_eq!(config.timeout, 30);
    }

    #[test]
    fn test_config_serialization() {
        let config = Config::default();
        let toml_str = to_string_pretty(&config).unwrap();
        let parsed: Config = from_toml_str(&toml_str).unwrap();
        assert_eq!(config.download_dir, parsed.download_dir);
    }
}