whi 0.8.1

Stupid simple PATH management
Documentation
use std::env;
use std::fs;
use std::io::{BufWriter, StdoutLock};
use std::path::{Path, PathBuf};

use crate::cli::args::{Args, DeleteTarget, PathEdit, PreferTarget};
use crate::commands::support::path_support::{
    emit_line, output_path, should_use_color, write_snapshot_safe,
};
use crate::path::resolve::{looks_like_exact_path, resolve_path};
use crate::path::searcher::PathSearcher;

use super::handle_path_result;
use super::query::search_name;

pub(super) fn handle_clean(
    searcher: &PathSearcher,
    args: &Args,
    out: &mut BufWriter<StdoutLock<'_>>,
) -> i32 {
    let (new_path, _removed_indices) = searcher.clean_duplicates();
    write_snapshot_safe(&new_path, args);
    output_path(out, &new_path)
}

pub(super) fn handle_move_or_swap(
    searcher: &PathSearcher,
    path_edit: &PathEdit,
    args: &Args,
    out: &mut BufWriter<StdoutLock<'_>>,
) -> i32 {
    let result = match path_edit {
        PathEdit::Move { from, to } => searcher.move_entry(*from, *to),
        PathEdit::Swap { first, second } => searcher.swap_entries(*first, *second),
    };
    handle_path_result(result, args, out)
}

pub(super) fn handle_prefer(
    searcher: &PathSearcher,
    target: &PreferTarget,
    args: &Args,
    out: &mut BufWriter<StdoutLock<'_>>,
) -> i32 {
    match target {
        PreferTarget::IndexBased { name, index } => {
            handle_prefer_index(searcher, name, *index, args, out)
        }
        PreferTarget::PathBased { name, path } => {
            handle_prefer_path(searcher, name, path, args, out)
        }
        PreferTarget::PathOnly { path } => handle_prefer_path_only(searcher, path, args, out),
    }
}

fn handle_prefer_index(
    searcher: &PathSearcher,
    name: &str,
    target_idx: usize,
    args: &Args,
    out: &mut BufWriter<StdoutLock<'_>>,
) -> i32 {
    let mut search_args = args.clone();
    search_args.all = true;
    let results = search_name(searcher, name, &search_args);

    if results.is_empty() {
        if !args.silent {
            eprintln!("Error: {name}: not found");
        }
        return 1;
    }

    let winner_idx = results[0].path_index;
    let target_result = results.iter().find(|r| r.path_index == target_idx);
    if target_result.is_none() {
        if !args.silent {
            eprintln!("Error: {name} not found at index {target_idx}");
        }
        return 2;
    }

    let new_position = if target_idx > winner_idx {
        winner_idx
    } else {
        if !args.silent {
            eprintln!(
                "Error: {name} at index {target_idx} is already preferred over index {winner_idx}"
            );
        }
        return 2;
    };

    match searcher.move_entry(target_idx, new_position) {
        Ok(new_path) => {
            write_snapshot_safe(&new_path, args);
            output_path(out, &new_path)
        }
        Err(e) => {
            if !args.silent {
                eprintln!("Error: {e}");
            }
            2
        }
    }
}

fn handle_prefer_path(
    searcher: &PathSearcher,
    name: &str,
    path_str: &str,
    args: &Args,
    out: &mut BufWriter<StdoutLock<'_>>,
) -> i32 {
    let cwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));

    if looks_like_exact_path(path_str) {
        match resolve_path(path_str, &cwd) {
            Ok(resolved_path) => {
                handle_prefer_exact_path(searcher, name, &resolved_path, args, out)
            }
            Err(e) => {
                if !args.silent {
                    eprintln!("Error resolving path: {e}");
                }
                2
            }
        }
    } else {
        handle_prefer_fuzzy(searcher, name, path_str, args, out)
    }
}

fn handle_prefer_exact_path(
    searcher: &PathSearcher,
    name: &str,
    path: &Path,
    args: &Args,
    out: &mut BufWriter<StdoutLock<'_>>,
) -> i32 {
    if !path.exists() {
        if !args.silent {
            eprintln!("Error: Directory does not exist: {}", path.display());
        }
        return 2;
    }

    if let Some(idx) = searcher.find_path_index(path) {
        return handle_prefer_index(searcher, name, idx, args, out);
    }

    if !searcher.has_executable(path, name) {
        if !args.silent {
            eprintln!("Error: {} not found in {}", name, path.display());
        }
        return 2;
    }

    let results = search_name(searcher, name, args);

    let insert_position = if results.is_empty() {
        1
    } else {
        results[0].path_index
    };

    match searcher.add_path_at_position(path, insert_position) {
        Ok(new_path) => {
            if !args.silent {
                eprintln!(
                    "Added {} to PATH at index {}",
                    path.display(),
                    insert_position
                );
            }

            write_snapshot_safe(&new_path, args);
            output_path(out, &new_path)
        }
        Err(e) => {
            if !args.silent {
                eprintln!("Error adding to PATH: {e}");
            }
            2
        }
    }
}

fn handle_prefer_path_only(
    searcher: &PathSearcher,
    path_str: &str,
    args: &Args,
    out: &mut BufWriter<StdoutLock<'_>>,
) -> i32 {
    let cwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));

    let resolved_path = if looks_like_exact_path(path_str) {
        match resolve_path(path_str, &cwd) {
            Ok(path) => path,
            Err(e) => {
                if !args.silent {
                    eprintln!("Error resolving path: {e}");
                }
                return 2;
            }
        }
    } else {
        cwd.join(path_str)
    };

    if let Some(_idx) = searcher.find_path_index(&resolved_path) {
        if !args.silent {
            eprintln!("{} is already in PATH", resolved_path.display());
        }
        return emit_line(out, &searcher.to_path_string());
    }

    match searcher.add_path(&resolved_path) {
        Ok((new_path, idx)) => {
            if !args.silent {
                eprintln!("Added {} to PATH at index {}", resolved_path.display(), idx);
            }

            write_snapshot_safe(&new_path, args);
            output_path(out, &new_path)
        }
        Err(e) => {
            if !args.silent {
                eprintln!("Error adding to PATH: {e}");
            }
            2
        }
    }
}

fn handle_prefer_fuzzy(
    searcher: &PathSearcher,
    name: &str,
    pattern: &str,
    args: &Args,
    out: &mut BufWriter<StdoutLock<'_>>,
) -> i32 {
    let matches = searcher.find_fuzzy_indices(pattern, Some(name));

    if matches.is_empty() {
        if !args.silent {
            eprintln!("Error: No PATH entries match pattern '{pattern}' containing '{name}'");
        }
        return 1;
    }

    if matches.len() > 1 {
        if !args.silent {
            eprintln!("Error: Multiple PATH entries match pattern '{pattern}':");
            for (idx, path) in &matches {
                eprintln!("  [{}] {}", idx, path.display());
            }
            eprintln!("Please be more specific or use an index directly.");
        }
        return 2;
    }

    let (index, _) = matches[0];
    handle_prefer_index(searcher, name, index, args, out)
}

pub(super) fn handle_delete(
    searcher: &PathSearcher,
    targets: &[DeleteTarget],
    args: &Args,
    out: &mut BufWriter<StdoutLock<'_>>,
) -> i32 {
    let cwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
    let mut indices_to_delete = Vec::new();

    for target in targets {
        match target {
            DeleteTarget::Index(idx) => {
                indices_to_delete.push(*idx);
            }
            DeleteTarget::Path(path_str) => {
                if looks_like_exact_path(path_str) {
                    match resolve_path(path_str, &cwd) {
                        Ok(resolved) => {
                            if let Some(idx) = searcher.find_path_index(&resolved) {
                                indices_to_delete.push(idx);
                            } else {
                                if !args.silent {
                                    eprintln!(
                                        "Error: Path not found in PATH: {}",
                                        resolved.display()
                                    );
                                }
                                return 1;
                            }
                        }
                        Err(e) => {
                            if !args.silent {
                                eprintln!("Error resolving path: {e}");
                            }
                            return 2;
                        }
                    }
                } else {
                    let matches = searcher.find_fuzzy_indices(path_str, None);

                    if matches.is_empty() {
                        if !args.silent {
                            eprintln!("Error: No PATH entries match pattern '{path_str}'");
                        }
                        return 1;
                    }

                    for (idx, _) in &matches {
                        indices_to_delete.push(*idx);
                    }
                }
            }
        }
    }

    let dirs = searcher.dirs();

    if let Some(exe_dir) = super::get_current_exe_dir() {
        let canonical_exe_dir = fs::canonicalize(&exe_dir).unwrap_or_else(|_| exe_dir.clone());

        indices_to_delete.retain(|&idx| {
            if idx > 0 && idx <= dirs.len() {
                let path = &dirs[idx - 1];
                let canonical_path = fs::canonicalize(path).unwrap_or_else(|_| path.clone());

                path != &exe_dir
                    && path != &canonical_exe_dir
                    && canonical_path != exe_dir
                    && canonical_path != canonical_exe_dir
            } else {
                true
            }
        });
    }

    indices_to_delete.sort_unstable();
    indices_to_delete.dedup();

    if !args.silent && !indices_to_delete.is_empty() {
        let use_color = should_use_color(args, super::atty::is(super::atty::Stream::Stdout));
        let (red, reset) = if use_color {
            ("\x1b[31m", "\x1b[0m")
        } else {
            ("", "")
        };

        for &idx in &indices_to_delete {
            if idx > 0 && idx <= dirs.len() {
                eprintln!("{red}- {}{reset}", dirs[idx - 1].display());
            }
        }
    }

    let result = if indices_to_delete.len() == 1 {
        searcher.delete_entry(indices_to_delete[0])
    } else {
        searcher.delete_entries(&indices_to_delete)
    };

    match result {
        Ok(new_path) => {
            write_snapshot_safe(&new_path, args);
            output_path(out, &new_path)
        }
        Err(e) => {
            if !args.silent {
                eprintln!("Error: {e}");
            }
            2
        }
    }
}