gitorii 0.1.1

A human-first Git client with simplified commands, snapshots, multi-platform mirrors and built-in secret scanning
use std::path::{Path, PathBuf};
use std::fs;
use serde::{Deserialize, Serialize};
use crate::error::{Result, ToriiError};

/// Global Torii configuration
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ToriiConfig {
    /// User settings
    pub user: UserConfig,
    
    /// Snapshot settings
    pub snapshot: SnapshotConfig,
    
    /// Mirror settings
    pub mirror: MirrorConfig,
    
    /// Git settings
    pub git: GitConfig,
    
    /// UI settings
    pub ui: UiConfig,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UserConfig {
    /// Default author name for commits
    pub name: Option<String>,
    
    /// Default author email for commits
    pub email: Option<String>,
    
    /// Preferred editor
    pub editor: Option<String>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SnapshotConfig {
    /// Enable auto-snapshots
    pub auto_enabled: bool,
    
    /// Auto-snapshot interval in minutes
    pub auto_interval_minutes: u32,
    
    /// Retention period in days
    pub retention_days: u32,
    
    /// Maximum number of snapshots to keep
    pub max_snapshots: Option<u32>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct MirrorConfig {
    /// Enable auto-fetch from mirrors
    pub autofetch_enabled: bool,
    
    /// Auto-fetch interval in minutes
    pub autofetch_interval_minutes: u32,
    
    /// Default protocol (ssh or https)
    pub default_protocol: String,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct GitConfig {
    /// Default branch name for new repos
    pub default_branch: String,
    
    /// Auto-sign commits with GPG
    pub sign_commits: bool,
    
    /// GPG key ID
    pub gpg_key: Option<String>,
    
    /// Always use rebase instead of merge for pulls
    pub pull_rebase: bool,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UiConfig {
    /// Use colored output
    pub colors: bool,
    
    /// Show emoji in output
    pub emoji: bool,
    
    /// Verbose output
    pub verbose: bool,
    
    /// Preferred date format
    pub date_format: String,
}

impl Default for ToriiConfig {
    fn default() -> Self {
        Self {
            user: UserConfig {
                name: None,
                email: None,
                editor: std::env::var("EDITOR").ok(),
            },
            snapshot: SnapshotConfig {
                auto_enabled: false,
                auto_interval_minutes: 30,
                retention_days: 30,
                max_snapshots: Some(100),
            },
            mirror: MirrorConfig {
                autofetch_enabled: false,
                autofetch_interval_minutes: 30,
                default_protocol: "ssh".to_string(),
            },
            git: GitConfig {
                default_branch: "main".to_string(),
                sign_commits: false,
                gpg_key: None,
                pull_rebase: false,
            },
            ui: UiConfig {
                colors: true,
                emoji: true,
                verbose: false,
                date_format: "%Y-%m-%d %H:%M".to_string(),
            },
        }
    }
}

impl ToriiConfig {
    /// Get the global config file path
    fn global_config_path() -> Result<PathBuf> {
        let home = std::env::var("HOME")
            .map_err(|_| ToriiError::InvalidConfig("HOME environment variable not set".to_string()))?;
        let config_dir = PathBuf::from(home).join(".config").join("torii");
        fs::create_dir_all(&config_dir)?;
        Ok(config_dir.join("config.toml"))
    }
    
    /// Get the local repo config file path
    fn local_config_path<P: AsRef<Path>>(repo_path: P) -> Result<PathBuf> {
        let torii_dir = repo_path.as_ref().join(".torii");
        fs::create_dir_all(&torii_dir)?;
        Ok(torii_dir.join("config.toml"))
    }
    
    /// Load global configuration
    pub fn load_global() -> Result<Self> {
        let config_path = Self::global_config_path()?;
        
        if !config_path.exists() {
            return Ok(Self::default());
        }
        
        let config_str = fs::read_to_string(&config_path)?;
        let config: ToriiConfig = toml::from_str(&config_str)
            .map_err(|e| ToriiError::InvalidConfig(format!("Failed to parse config: {}", e)))?;
        
        Ok(config)
    }
    
    /// Load local repository configuration (merged with global)
    pub fn load_local<P: AsRef<Path>>(repo_path: P) -> Result<Self> {
        let mut config = Self::load_global()?;
        
        let local_path = Self::local_config_path(&repo_path)?;
        if local_path.exists() {
            let local_str = fs::read_to_string(&local_path)?;
            let local_config: ToriiConfig = toml::from_str(&local_str)
                .map_err(|e| ToriiError::InvalidConfig(format!("Failed to parse local config: {}", e)))?;
            
            // Merge local config over global (local takes precedence)
            config = Self::merge(config, local_config);
        }
        
        Ok(config)
    }
    
    /// Save global configuration
    pub fn save_global(&self) -> Result<()> {
        let config_path = Self::global_config_path()?;
        let config_str = toml::to_string_pretty(self)
            .map_err(|e| ToriiError::InvalidConfig(format!("Failed to serialize config: {}", e)))?;
        fs::write(&config_path, config_str)?;
        Ok(())
    }
    
    /// Save local repository configuration
    pub fn save_local<P: AsRef<Path>>(&self, repo_path: P) -> Result<()> {
        let config_path = Self::local_config_path(repo_path)?;
        let config_str = toml::to_string_pretty(self)
            .map_err(|e| ToriiError::InvalidConfig(format!("Failed to serialize config: {}", e)))?;
        fs::write(&config_path, config_str)?;
        Ok(())
    }
    
    /// Merge two configs (second takes precedence for non-None values)
    fn merge(mut base: Self, overlay: Self) -> Self {
        // User config
        if overlay.user.name.is_some() {
            base.user.name = overlay.user.name;
        }
        if overlay.user.email.is_some() {
            base.user.email = overlay.user.email;
        }
        if overlay.user.editor.is_some() {
            base.user.editor = overlay.user.editor;
        }
        
        // Snapshot config
        base.snapshot = overlay.snapshot;
        
        // Mirror config
        base.mirror = overlay.mirror;
        
        // Git config
        base.git = overlay.git;
        
        // UI config
        base.ui = overlay.ui;
        
        base
    }
    
    /// Get a configuration value by key path (e.g., "user.name", "snapshot.auto_enabled")
    pub fn get(&self, key: &str) -> Option<String> {
        let parts: Vec<&str> = key.split('.').collect();
        if parts.len() != 2 {
            return None;
        }
        
        match (parts[0], parts[1]) {
            ("user", "name") => self.user.name.clone(),
            ("user", "email") => self.user.email.clone(),
            ("user", "editor") => self.user.editor.clone(),
            ("snapshot", "auto_enabled") => Some(self.snapshot.auto_enabled.to_string()),
            ("snapshot", "auto_interval_minutes") => Some(self.snapshot.auto_interval_minutes.to_string()),
            ("snapshot", "retention_days") => Some(self.snapshot.retention_days.to_string()),
            ("snapshot", "max_snapshots") => self.snapshot.max_snapshots.map(|v| v.to_string()),
            ("mirror", "autofetch_enabled") => Some(self.mirror.autofetch_enabled.to_string()),
            ("mirror", "autofetch_interval_minutes") => Some(self.mirror.autofetch_interval_minutes.to_string()),
            ("mirror", "default_protocol") => Some(self.mirror.default_protocol.clone()),
            ("git", "default_branch") => Some(self.git.default_branch.clone()),
            ("git", "sign_commits") => Some(self.git.sign_commits.to_string()),
            ("git", "gpg_key") => self.git.gpg_key.clone(),
            ("git", "pull_rebase") => Some(self.git.pull_rebase.to_string()),
            ("ui", "colors") => Some(self.ui.colors.to_string()),
            ("ui", "emoji") => Some(self.ui.emoji.to_string()),
            ("ui", "verbose") => Some(self.ui.verbose.to_string()),
            ("ui", "date_format") => Some(self.ui.date_format.clone()),
            _ => None,
        }
    }
    
    /// Set a configuration value by key path
    pub fn set(&mut self, key: &str, value: &str) -> Result<()> {
        let parts: Vec<&str> = key.split('.').collect();
        if parts.len() != 2 {
            return Err(ToriiError::InvalidConfig(format!("Invalid config key: {}", key)));
        }
        
        match (parts[0], parts[1]) {
            ("user", "name") => self.user.name = Some(value.to_string()),
            ("user", "email") => self.user.email = Some(value.to_string()),
            ("user", "editor") => self.user.editor = Some(value.to_string()),
            ("snapshot", "auto_enabled") => {
                self.snapshot.auto_enabled = value.parse()
                    .map_err(|_| ToriiError::InvalidConfig("Value must be true or false".to_string()))?;
            }
            ("snapshot", "auto_interval_minutes") => {
                self.snapshot.auto_interval_minutes = value.parse()
                    .map_err(|_| ToriiError::InvalidConfig("Value must be a number".to_string()))?;
            }
            ("snapshot", "retention_days") => {
                self.snapshot.retention_days = value.parse()
                    .map_err(|_| ToriiError::InvalidConfig("Value must be a number".to_string()))?;
            }
            ("snapshot", "max_snapshots") => {
                self.snapshot.max_snapshots = Some(value.parse()
                    .map_err(|_| ToriiError::InvalidConfig("Value must be a number".to_string()))?);
            }
            ("mirror", "autofetch_enabled") => {
                self.mirror.autofetch_enabled = value.parse()
                    .map_err(|_| ToriiError::InvalidConfig("Value must be true or false".to_string()))?;
            }
            ("mirror", "autofetch_interval_minutes") => {
                self.mirror.autofetch_interval_minutes = value.parse()
                    .map_err(|_| ToriiError::InvalidConfig("Value must be a number".to_string()))?;
            }
            ("mirror", "default_protocol") => {
                if value != "ssh" && value != "https" {
                    return Err(ToriiError::InvalidConfig("Protocol must be 'ssh' or 'https'".to_string()));
                }
                self.mirror.default_protocol = value.to_string();
            }
            ("git", "default_branch") => self.git.default_branch = value.to_string(),
            ("git", "sign_commits") => {
                self.git.sign_commits = value.parse()
                    .map_err(|_| ToriiError::InvalidConfig("Value must be true or false".to_string()))?;
            }
            ("git", "gpg_key") => self.git.gpg_key = Some(value.to_string()),
            ("git", "pull_rebase") => {
                self.git.pull_rebase = value.parse()
                    .map_err(|_| ToriiError::InvalidConfig("Value must be true or false".to_string()))?;
            }
            ("ui", "colors") => {
                self.ui.colors = value.parse()
                    .map_err(|_| ToriiError::InvalidConfig("Value must be true or false".to_string()))?;
            }
            ("ui", "emoji") => {
                self.ui.emoji = value.parse()
                    .map_err(|_| ToriiError::InvalidConfig("Value must be true or false".to_string()))?;
            }
            ("ui", "verbose") => {
                self.ui.verbose = value.parse()
                    .map_err(|_| ToriiError::InvalidConfig("Value must be true or false".to_string()))?;
            }
            ("ui", "date_format") => self.ui.date_format = value.to_string(),
            _ => return Err(ToriiError::InvalidConfig(format!("Unknown config key: {}", key))),
        }
        
        Ok(())
    }
    
    /// List all configuration values
    pub fn list(&self) -> Vec<(String, String)> {
        let mut items = Vec::new();
        
        // User
        if let Some(name) = &self.user.name {
            items.push(("user.name".to_string(), name.clone()));
        }
        if let Some(email) = &self.user.email {
            items.push(("user.email".to_string(), email.clone()));
        }
        if let Some(editor) = &self.user.editor {
            items.push(("user.editor".to_string(), editor.clone()));
        }
        
        // Snapshot
        items.push(("snapshot.auto_enabled".to_string(), self.snapshot.auto_enabled.to_string()));
        items.push(("snapshot.auto_interval_minutes".to_string(), self.snapshot.auto_interval_minutes.to_string()));
        items.push(("snapshot.retention_days".to_string(), self.snapshot.retention_days.to_string()));
        if let Some(max) = self.snapshot.max_snapshots {
            items.push(("snapshot.max_snapshots".to_string(), max.to_string()));
        }
        
        // Mirror
        items.push(("mirror.autofetch_enabled".to_string(), self.mirror.autofetch_enabled.to_string()));
        items.push(("mirror.autofetch_interval_minutes".to_string(), self.mirror.autofetch_interval_minutes.to_string()));
        items.push(("mirror.default_protocol".to_string(), self.mirror.default_protocol.clone()));
        
        // Git
        items.push(("git.default_branch".to_string(), self.git.default_branch.clone()));
        items.push(("git.sign_commits".to_string(), self.git.sign_commits.to_string()));
        if let Some(key) = &self.git.gpg_key {
            items.push(("git.gpg_key".to_string(), key.clone()));
        }
        items.push(("git.pull_rebase".to_string(), self.git.pull_rebase.to_string()));
        
        // UI
        items.push(("ui.colors".to_string(), self.ui.colors.to_string()));
        items.push(("ui.emoji".to_string(), self.ui.emoji.to_string()));
        items.push(("ui.verbose".to_string(), self.ui.verbose.to_string()));
        items.push(("ui.date_format".to_string(), self.ui.date_format.clone()));
        
        items
    }
}