sb-vault 0.1.0

S&B Vault // Zero-Trust Desktop Suite & Secret Engine
use std::fs::OpenOptions;
use std::io::Write;
use std::path::PathBuf;
use chrono::Utc;
use serde::{Deserialize, Serialize};
use tauri::State;

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct VaultProfile {
    pub id: String,
    pub name: String,
    pub vault_dir: String,
    pub servers_file: Option<String>,
    pub description: Option<String>,
    pub is_active: bool,
}

#[derive(Debug, Serialize, Deserialize, Default)]
pub struct ProfilesConfig {
    pub active_profile_id: Option<String>,
    pub profiles: Vec<VaultProfile>,
}

pub fn get_profiles_dir() -> PathBuf {
    if let Some(home) = dirs::home_dir() {
        home.join(".config").join("sb-vault")
    } else {
        PathBuf::from(".config/sb-vault")
    }
}

pub fn get_profiles_file_path() -> PathBuf {
    get_profiles_dir().join("profiles.toml")
}

pub fn get_default_forge_vault_dir() -> String {
    if let Some(home) = dirs::home_dir() {
        home.join(".sb-forge").join("vault").to_string_lossy().to_string()
    } else {
        ".sb-forge/vault".to_string()
    }
}

pub fn load_profiles_config() -> ProfilesConfig {
    let path = get_profiles_file_path();
    if !path.exists() {
        let default_profile = VaultProfile {
            id: "default".to_string(),
            name: "S&B Forge Standard".to_string(),
            vault_dir: get_default_forge_vault_dir(),
            servers_file: None,
            description: Some("Standard Workspace für lokale S&B Forge Tresore".to_string()),
            is_active: true,
        };
        let config = ProfilesConfig {
            active_profile_id: Some("default".to_string()),
            profiles: vec![default_profile],
        };
        let _ = save_profiles_config(&config);
        return config;
    }

    match std::fs::read_to_string(&path) {
        Ok(content) => toml::from_str(&content).unwrap_or_default(),
        Err(_) => ProfilesConfig::default(),
    }
}

pub fn save_profiles_config(config: &ProfilesConfig) -> Result<(), String> {
    let dir = get_profiles_dir();
    std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;

    let path = get_profiles_file_path();
    let toml_str = toml::to_string_pretty(config).map_err(|e| e.to_string())?;

    let mut file = OpenOptions::new()
        .write(true)
        .create(true)
        .truncate(true)
        .open(&path)
        .map_err(|e| e.to_string())?;

    file.write_all(toml_str.as_bytes()).map_err(|e| e.to_string())?;
    Ok(())
}

pub fn get_active_vault_dir() -> PathBuf {
    let config = load_profiles_config();
    if let Some(active_id) = config.active_profile_id {
        if let Some(profile) = config.profiles.iter().find(|p| p.id == active_id) {
            return PathBuf::from(&profile.vault_dir);
        }
    }
    PathBuf::from(get_default_forge_vault_dir())
}

#[tauri::command]
pub fn get_profiles() -> Result<Vec<VaultProfile>, String> {
    let config = load_profiles_config();
    Ok(config.profiles)
}

#[tauri::command]
pub fn save_profile(mut profile: VaultProfile) -> Result<Vec<VaultProfile>, String> {
    let mut config = load_profiles_config();

    if profile.id.is_empty() {
        profile.id = format!("prof-{}", Utc::now().timestamp_millis());
        profile.is_active = false;
        config.profiles.push(profile);
    } else if let Some(pos) = config.profiles.iter().position(|p| p.id == profile.id) {
        profile.is_active = config.profiles[pos].is_active;
        config.profiles[pos] = profile;
    } else {
        config.profiles.push(profile);
    }

    save_profiles_config(&config)?;
    Ok(config.profiles)
}

#[tauri::command]
pub fn delete_profile(id: String) -> Result<Vec<VaultProfile>, String> {
    if id == "default" {
        return Err("Das Standard-Profil kann nicht gelöscht werden!".to_string());
    }

    let mut config = load_profiles_config();
    if config.active_profile_id.as_deref() == Some(&id) {
        return Err("Das aktuell aktive Profil kann nicht gelöscht werden. Bitte zuerst wechseln!".to_string());
    }

    config.profiles.retain(|p| p.id != id);
    save_profiles_config(&config)?;
    Ok(config.profiles)
}

#[tauri::command]
pub async fn switch_profile(
    id: String,
    vault_state: State<'_, crate::vault::VaultState>,
    tunnel_state: State<'_, crate::tunnels::TunnelState>,
    log_state: State<'_, crate::logs::LogStreamState>,
) -> Result<VaultProfile, String> {
    let mut config = load_profiles_config();

    let target_profile = config
        .profiles
        .iter()
        .find(|p| p.id == id)
        .cloned()
        .ok_or_else(|| format!("Profil mit ID '{}' nicht gefunden", id))?;

    // ZERO-TRUST SAFETY:
    // 1. Lock vault immediately (clear RAM master key)
    *vault_state.active_session.lock().unwrap() = None;

    // 2. Terminate all active tunnels and log streams
    crate::tunnels::stop_all_active_tunnels(&tunnel_state).await;
    crate::logs::stop_all_active_log_streams(&log_state).await;

    // 3. Switch active dir in vault_state
    let new_vault_dir = PathBuf::from(&target_profile.vault_dir);
    if !new_vault_dir.exists() {
        let _ = std::fs::create_dir_all(&new_vault_dir);
    }
    *vault_state.active_vault_dir.lock().unwrap() = new_vault_dir;

    // 4. Update profiles.toml active state
    config.active_profile_id = Some(id.clone());
    for p in config.profiles.iter_mut() {
        p.is_active = p.id == id;
    }
    save_profiles_config(&config)?;

    let mut switched = target_profile;
    switched.is_active = true;
    Ok(switched)
}

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

    #[test]
    fn test_default_profile_generation() {
        let profile = VaultProfile {
            id: "default".to_string(),
            name: "S&B Forge Standard".to_string(),
            vault_dir: "/home/user/.sb-forge/vault".to_string(),
            servers_file: None,
            description: Some("Test".to_string()),
            is_active: true,
        };

        let json = serde_json::to_string(&profile).unwrap();
        let parsed: VaultProfile = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed.id, "default");
        assert_eq!(parsed.is_active, true);
    }
}