sb-vault 0.1.0

S&B Vault // Zero-Trust Desktop Suite & Secret Engine
use std::path::{Path, PathBuf};
use std::collections::HashSet;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ForgeProjectSummary {
    pub name: String,
    pub version: String,
    pub path: String,
    pub file_path: String,
    pub is_root: bool,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct RecipeInfo {
    pub name: String,
    pub description: Option<String>,
    pub depends_on: Option<Vec<String>>,
    pub server: Option<String>,
    pub action: Option<String>,
    pub commands: Vec<String>,
    pub health_check: Option<String>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct StageInfo {
    pub name: String,
    pub recipes: Vec<String>,
    pub parallel: bool,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PipelineInfo {
    pub name: String,
    pub description: Option<String>,
    pub stages: Vec<StageInfo>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct ForgeProjectDetails {
    pub name: String,
    pub version: String,
    pub path: String,
    pub recipes: Vec<RecipeInfo>,
    pub pipelines: Vec<PipelineInfo>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct RunResult {
    pub success: bool,
    pub exit_code: Option<i32>,
    pub stdout: String,
    pub stderr: String,
    pub duration_ms: u64,
}

/// Rekursives und parent-basiertes Entdecken von allen Forge.toml Dateien
pub fn discover_all_forge_projects() -> Vec<ForgeProjectSummary> {
    let mut discovered = Vec::new();
    let mut seen_paths = HashSet::new();
    let mut candidate_roots = Vec::new();

    if let Ok(cur) = std::env::current_dir() {
        candidate_roots.push(cur);
    }
    if let Ok(exe) = std::env::current_exe() {
        if let Some(parent) = exe.parent() {
            candidate_roots.push(parent.to_path_buf());
        }
    }

    // Von den Startpunkten aus bis zu 5 Ebenen nach oben wandern
    for start in candidate_roots {
        let mut curr = Some(start);
        let mut depth = 0;
        while let Some(dir) = curr {
            if depth > 5 {
                break;
            }

            // 1. Ordner selbst prüfen
            check_and_add_forge_toml(&dir, &mut discovered, &mut seen_paths);

            // 2. Nachbar- und Unterordner prüfen (z.B. website, tools, etc.)
            if let Ok(entries) = std::fs::read_dir(&dir) {
                for entry in entries.flatten() {
                    let path = entry.path();
                    if path.is_dir() {
                        check_and_add_forge_toml(&path, &mut discovered, &mut seen_paths);

                        // Wenn es tools/ oder website/ etc. ist, auch deren direkte Unterordner scannen
                        let dir_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
                        if dir_name == "tools" || dir_name == "packages" || dir_name == "apps" {
                            if let Ok(sub_entries) = std::fs::read_dir(&path) {
                                for sub_entry in sub_entries.flatten() {
                                    let sub_path = sub_entry.path();
                                    if sub_path.is_dir() {
                                        check_and_add_forge_toml(&sub_path, &mut discovered, &mut seen_paths);
                                    }
                                }
                            }
                        }
                    }
                }
            }

            curr = dir.parent().map(|p| p.to_path_buf());
            depth += 1;
        }
    }

    // Sortierung: Root-Projekte zuerst, dann alphabetisch
    discovered.sort_by(|a, b| {
        b.is_root.cmp(&a.is_root).then_with(|| a.name.cmp(&b.name))
    });

    discovered
}

fn check_and_add_forge_toml(
    dir: &Path,
    discovered: &mut Vec<ForgeProjectSummary>,
    seen_paths: &mut HashSet<PathBuf>,
) {
    let forge_file = dir.join("Forge.toml");
    if forge_file.exists() && forge_file.is_file() {
        let canonical = forge_file.canonicalize().unwrap_or_else(|_| forge_file.clone());
        if seen_paths.insert(canonical.clone()) {
            if let Ok(content) = std::fs::read_to_string(&forge_file) {
                if let Ok(parsed) = toml::from_str::<toml::Value>(&content) {
                    let name = parsed.get("forge")
                        .and_then(|f| f.get("name"))
                        .and_then(|n| n.as_str())
                        .unwrap_or_else(|| dir.file_name().and_then(|n| n.to_str()).unwrap_or("Unbenannt"))
                        .to_string();
                    let version = parsed.get("forge")
                        .and_then(|f| f.get("version"))
                        .and_then(|v| v.as_str())
                        .unwrap_or("1.0.0")
                        .to_string();

                    // Root wenn 'workspace' im Namen oder Rezepte cd in Unterordner aufrufen
                    let is_root = name.contains("workspace") 
                        || parsed.get("recipe").and_then(|r| r.as_table()).map(|t| {
                            t.values().any(|v| {
                                v.get("run").map(|r| r.to_string().contains("cd ")).unwrap_or(false)
                            })
                        }).unwrap_or(false);

                    discovered.push(ForgeProjectSummary {
                        name,
                        version,
                        path: dir.to_string_lossy().to_string(),
                        file_path: canonical.to_string_lossy().to_string(),
                        is_root,
                    });
                }
            }
        }
    }
}

#[tauri::command]
pub fn list_forge_projects() -> Result<Vec<ForgeProjectSummary>, String> {
    Ok(discover_all_forge_projects())
}

#[tauri::command]
pub fn load_forge_project(project_path: Option<String>) -> Result<ForgeProjectDetails, String> {
    let target_dir = if let Some(p) = project_path {
        let p_buf = PathBuf::from(&p);
        if p_buf.join("Forge.toml").exists() {
            p_buf
        } else if p_buf.file_name().and_then(|n| n.to_str()) == Some("Forge.toml") && p_buf.exists() {
            p_buf.parent().unwrap_or(&p_buf).to_path_buf()
        } else {
            let projects = discover_all_forge_projects();
            if let Some(found) = projects.iter().find(|pr| pr.path == p || pr.file_path == p) {
                PathBuf::from(&found.path)
            } else {
                return Err(format!("Keine Forge.toml unter '{}' gefunden", p));
            }
        }
    } else {
        let projects = discover_all_forge_projects();
        if let Some(first) = projects.first() {
            PathBuf::from(&first.path)
        } else {
            return Err("Keine Forge.toml im aktuellen Verzeichnis, Parent oder Nachbar-Ordnern gefunden.".to_string());
        }
    };

    let file_path = target_dir.join("Forge.toml");
    let content = std::fs::read_to_string(&file_path)
        .map_err(|e| format!("Forge.toml in '{}' konnte nicht gelesen werden: {}", target_dir.display(), e))?;
    let parsed: toml::Value = toml::from_str(&content)
        .map_err(|e| format!("Forge.toml Syntax-Fehler: {}", e))?;

    let name = parsed.get("forge")
        .and_then(|f| f.get("name"))
        .and_then(|n| n.as_str())
        .unwrap_or("Unbenannt")
        .to_string();

    let version = parsed.get("forge")
        .and_then(|f| f.get("version"))
        .and_then(|v| v.as_str())
        .unwrap_or("0.1.0")
        .to_string();

    let mut recipes = Vec::new();
    if let Some(r_table) = parsed.get("recipe").and_then(|r| r.as_table()) {
        for (r_name, r_val) in r_table {
            let description = r_val.get("description").and_then(|d| d.as_str()).map(String::from);
            let server = r_val.get("server").and_then(|s| s.as_str()).map(String::from);
            let action = r_val.get("action").and_then(|a| a.as_str()).map(String::from);
            let health_check = r_val.get("health_check").and_then(|h| h.as_str()).map(String::from);

            let depends_on = r_val.get("depends_on")
                .and_then(|d| d.as_array())
                .map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect());

            let mut commands = Vec::new();
            if let Some(run_val) = r_val.get("run") {
                if let Some(single) = run_val.as_str() {
                    commands.push(single.to_string());
                } else if let Some(arr) = run_val.as_array() {
                    for v in arr {
                        if let Some(s) = v.as_str() {
                            commands.push(s.to_string());
                        }
                    }
                }
            }

            recipes.push(RecipeInfo {
                name: r_name.clone(),
                description,
                depends_on,
                server,
                action,
                commands,
                health_check,
            });
        }
    }
    recipes.sort_by(|a, b| a.name.cmp(&b.name));

    let mut pipelines = Vec::new();
    if let Some(p_table) = parsed.get("pipeline").and_then(|p| p.as_table()) {
        for (p_name, p_val) in p_table {
            let description = p_val.get("description").and_then(|d| d.as_str()).map(String::from);
            let mut stages = Vec::new();

            if let Some(stages_arr) = p_val.get("stages").and_then(|s| s.as_array()) {
                for stage_val in stages_arr {
                    let s_name = stage_val.get("name").and_then(|n| n.as_str()).unwrap_or("Stage").to_string();
                    let parallel = stage_val.get("parallel").and_then(|p| p.as_bool()).unwrap_or(false);
                    let mut s_recipes = Vec::new();

                    if let Some(arr) = stage_val.get("recipes").and_then(|r| r.as_array()) {
                        for item in arr {
                            if let Some(s) = item.as_str() {
                                s_recipes.push(s.to_string());
                            }
                        }
                    }

                    stages.push(StageInfo {
                        name: s_name,
                        recipes: s_recipes,
                        parallel,
                    });
                }
            }

            pipelines.push(PipelineInfo {
                name: p_name.clone(),
                description,
                stages,
            });
        }
    }
    pipelines.sort_by(|a, b| a.name.cmp(&b.name));

    Ok(ForgeProjectDetails {
        name,
        version,
        path: target_dir.to_string_lossy().to_string(),
        recipes,
        pipelines,
    })
}

#[tauri::command]
pub async fn execute_forge_target(
    project_path: String,
    target: String,
    env: Option<String>,
    state: tauri::State<'_, crate::vault::VaultState>,
) -> Result<RunResult, String> {
    crate::vault::ensure_unlocked(&state)?;

    let sb_forge = which::which("sb-forge")
        .or_else(|_| {
            dirs::home_dir().map(|h| h.join(".cargo").join("bin").join(if cfg!(windows) { "sb-forge.exe" } else { "sb-forge" }))
                .filter(|p| p.exists())
                .ok_or("Nicht gefunden")
        })
        .map_err(|_| "sb-forge Binary nicht gefunden".to_string())?;

    let mut cmd = tokio::process::Command::new(&sb_forge);
    cmd.current_dir(&project_path);
    cmd.args(["run", &target]);

    if let Some(e) = env {
        cmd.args(["--env", &e]);
    }

    let start = std::time::Instant::now();
    let output = cmd.output().await
        .map_err(|e| format!("Fehler beim Ausführen von sb-forge: {}", e))?;
    let duration_ms = start.elapsed().as_millis() as u64;

    Ok(RunResult {
        success: output.status.success(),
        exit_code: output.status.code(),
        stdout: String::from_utf8_lossy(&output.stdout).to_string(),
        stderr: String::from_utf8_lossy(&output.stderr).to_string(),
        duration_ms,
    })
}

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

    #[test]
    fn test_discover_forge_projects() {
        let projects = discover_all_forge_projects();
        assert!(!projects.is_empty(), "Should discover at least one Forge.toml");
        println!("Discovered {} Forge projects:", projects.len());
        for p in &projects {
            println!(" - {} (v{}) [root={}] at {}", p.name, p.version, p.is_root, p.path);
        }

        // Test loading without path
        let loaded = load_forge_project(None);
        assert!(loaded.is_ok(), "Auto-discovery should successfully load a Forge project: {:?}", loaded.err());
        let details = loaded.unwrap();
        assert!(!details.name.is_empty());
    }
}