whi 0.8.1

Stupid simple PATH management
Documentation
use std::env;
use std::fs;
use std::io::Write;
use std::path::PathBuf;

use crate::io::atomic_file::AtomicFile;

fn default_protected_paths() -> Vec<PathBuf> {
    #[cfg(target_os = "macos")]
    {
        vec![
            PathBuf::from("/usr/local/bin"),
            PathBuf::from("/usr/local/sbin"),
            PathBuf::from("/usr/bin"),
            PathBuf::from("/bin"),
            PathBuf::from("/usr/sbin"),
            PathBuf::from("/sbin"),
        ]
    }

    #[cfg(target_os = "linux")]
    {
        vec![
            PathBuf::from("/usr/local/sbin"),
            PathBuf::from("/usr/local/bin"),
            PathBuf::from("/usr/sbin"),
            PathBuf::from("/usr/bin"),
            PathBuf::from("/sbin"),
            PathBuf::from("/bin"),
        ]
    }

    #[cfg(not(any(target_os = "linux", target_os = "macos")))]
    {
        vec![PathBuf::from("/usr/bin"), PathBuf::from("/bin")]
    }
}

pub fn get_protected_paths_path() -> Result<PathBuf, String> {
    let home = env::var("HOME").map_err(|_| "HOME environment variable not set")?;
    Ok(PathBuf::from(home).join(".whi").join("protected_paths"))
}

fn get_migration_marker_path() -> Result<PathBuf, String> {
    let home = env::var("HOME").map_err(|_| "HOME environment variable not set")?;
    Ok(PathBuf::from(home).join(".whi").join(".migrated"))
}

fn parse_protected_paths(content: &str) -> Result<Vec<PathBuf>, String> {
    use crate::io::line_utils::strip_inline_comment;

    let mut paths = Vec::new();
    let mut found_header = false;

    for line in content.lines() {
        let trimmed = line.trim();
        if trimmed.is_empty() || trimmed.starts_with('#') {
            continue;
        }

        let without_comment = strip_inline_comment(trimmed);
        if without_comment.is_empty() {
            continue;
        }

        if without_comment == "!protected.paths" {
            found_header = true;
            continue;
        }

        if found_header {
            paths.push(PathBuf::from(without_comment));
        }
    }

    if !found_header {
        return Err("Missing !protected.paths header".to_string());
    }

    Ok(paths)
}

fn format_protected_paths(paths: &[PathBuf]) -> String {
    let mut result = String::from("!protected.paths\n");
    for path in paths {
        result.push_str(&path.to_string_lossy());
        result.push('\n');
    }
    result
}

pub fn load_protected_paths() -> Result<Vec<PathBuf>, String> {
    let path = get_protected_paths_path()?;

    if !path.exists() {
        ensure_protected_paths_exists()?;
        return Ok(default_protected_paths());
    }

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

pub fn ensure_protected_paths_exists() -> Result<(), String> {
    let path = get_protected_paths_path()?;
    if path.exists() {
        return Ok(());
    }

    save_protected_paths(&default_protected_paths())
}

pub fn save_protected_paths(paths: &[PathBuf]) -> Result<(), String> {
    let path = get_protected_paths_path()?;

    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).map_err(|e| format!("Failed to create .whi directory: {e}"))?;
    }

    let content = format_protected_paths(paths);
    let mut atomic_file = AtomicFile::new(&path)
        .map_err(|e| format!("Failed to create {} file: {e}", path.display()))?;

    atomic_file
        .write_all(content.as_bytes())
        .map_err(|e| format!("Failed to write {}: {e}", path.display()))?;

    atomic_file
        .commit()
        .map_err(|e| format!("Failed to commit {} file: {e}", path.display()))?;

    Ok(())
}

pub fn migrate_from_config_toml() -> Result<bool, String> {
    use std::io::Write;

    let marker_path = get_migration_marker_path()?;
    if marker_path.exists() {
        return Ok(false);
    }

    let home = env::var("HOME").map_err(|_| "HOME environment variable not set")?;
    let config_path = PathBuf::from(&home).join(".whi").join("config.toml");
    let protected_paths_file = get_protected_paths_path()?;

    if protected_paths_file.exists() {
        write_migration_marker(&marker_path)?;
        return Ok(false);
    }

    if !config_path.exists() {
        write_migration_marker(&marker_path)?;
        return Ok(false);
    }

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

    if !content.contains("[protected]") {
        write_migration_marker(&marker_path)?;
        return Ok(false);
    }

    let migrated_paths = parse_protected_paths_from_toml(&content);
    if !migrated_paths.is_empty() {
        save_protected_paths(&migrated_paths)?;
    }

    let rewritten = remove_protected_section(&content);
    let mut atomic_file =
        AtomicFile::new(&config_path).map_err(|e| format!("Failed to create config file: {e}"))?;
    atomic_file
        .write_all(rewritten.as_bytes())
        .map_err(|e| format!("Failed to write config.toml: {e}"))?;
    atomic_file
        .commit()
        .map_err(|e| format!("Failed to commit config.toml: {e}"))?;

    write_migration_marker(&marker_path)?;
    Ok(!migrated_paths.is_empty())
}

fn parse_protected_paths_from_toml(content: &str) -> Vec<PathBuf> {
    let mut in_protected = false;
    let mut paths = Vec::new();

    for line in content.lines() {
        let trimmed = line.trim();

        if trimmed.starts_with('[') && trimmed.ends_with(']') {
            in_protected = trimmed == "[protected]";
            continue;
        }

        if !in_protected || !trimmed.starts_with("paths") {
            continue;
        }

        if let Some((_, values)) = trimmed.split_once('=') {
            for entry in values.split('"').skip(1).step_by(2) {
                if !entry.is_empty() {
                    paths.push(PathBuf::from(entry));
                }
            }
        }
    }

    paths
}

fn remove_protected_section(content: &str) -> String {
    let mut result = Vec::new();
    let mut in_protected = false;

    for line in content.lines() {
        let trimmed = line.trim();
        if trimmed.starts_with('[') && trimmed.ends_with(']') {
            in_protected = trimmed == "[protected]";
            if in_protected {
                continue;
            }
        }

        if !in_protected {
            result.push(line);
        }
    }

    let mut rewritten = result.join("\n");
    if !rewritten.is_empty() {
        rewritten.push('\n');
    }
    rewritten.push_str(
        "# MIGRATION NOTE: The [protected] section has been migrated to ~/.whi/protected_paths\n",
    );
    rewritten
}

fn write_migration_marker(marker_path: &PathBuf) -> Result<(), String> {
    if let Some(parent) = marker_path.parent() {
        fs::create_dir_all(parent).map_err(|e| format!("Failed to create .whi directory: {e}"))?;
    }

    let mut atomic_file = AtomicFile::new(marker_path)
        .map_err(|e| format!("Failed to create migration marker: {e}"))?;
    atomic_file
        .write_all(b"# protected_paths migration completed\n")
        .map_err(|e| format!("Failed to write migration marker: {e}"))?;
    atomic_file
        .commit()
        .map_err(|e| format!("Failed to commit migration marker: {e}"))?;
    Ok(())
}