//
// command.rs
//
// Copyright (C) 2024 Posit Software, PBC. All rights reserved.
//
//

use std::io;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::process::Output;

use anyhow::anyhow;

use crate::sys::command::COMMAND_R_NAMES;

/// Execute a `Command` for R, trying multiple names for the R executable
///
/// - For unix, this look at `R`
/// - For Windows, this looks at `R.exe` and `R.bat` (for rig compatibility)
///
/// The executable name is joined to the path in `r_home`. Use `r_home_setup()` to
/// retrieve this.
///
/// Returns the `Ok()` value of the first success, or the `Err()` value of the
/// last failure if all locations fail.
pub fn r_command<F>(r_home: &Path, build: F) -> io::Result<Output>
where
    F: Fn(&mut Command),
{
    assert!(!COMMAND_R_NAMES.is_empty());

    let locations: Vec<PathBuf> = COMMAND_R_NAMES
        .map(|loc| r_home.join("bin").join(loc))
        .into();

    r_command_from_locs(locations, build)
}

/// Ensures that `R_HOME` is set consistently (for `R_HomeDir()`) and returns the active
/// `r_home` path
pub fn r_home_setup() -> anyhow::Result<PathBuf> {
    // Determine candidate path and the string form to set (we will overwrite
    // R_HOME in the environment after validation, even if it was already set).
    let home = match std::env::var("R_HOME") {
        Ok(home) => {
            // Get `R_HOME` from env var, typically set by Positron / CI / kernel specification
            home.clone()
        },

        Err(_) => {
            // Get `R_HOME` from `PATH`, via `R`
            let result = r_command_from_path(|command| {
                command.arg("RHOME");
            })
            .map_err(|err| anyhow!("Can't find R or `R_HOME`: {err}"))?;

            let home = String::from_utf8(result.stdout)
                .map_err(|err| anyhow!("Invalid UTF-8 from R RHOME output: {err}"))?;
            home.trim().to_string()
        },
    };

    let path = PathBuf::from(home.clone());
    match path.try_exists() {
        Ok(true) => {
            // Ensure `R_HOME` is set in the environment after validation, mostly
            // for `R_HomeDir()` to ensure we are in sync with R
            unsafe { std::env::set_var("R_HOME", &home) };

            // Check that `R` can be called
            r_command(&path, |command| {
                command.arg("RHOME");
            })
            .map_err(|err| anyhow!("Can't run R: {err}"))?;

            Ok(path)
        },
        Ok(false) => Err(anyhow!(
            "The `R_HOME` path '{}' does not exist.",
            path.display()
        )),
        Err(err) => Err(anyhow!("Can't check if `R_HOME` path exists: {err}")),
    }
}

/// Locate an R executable, given an `R_HOME` path
///
/// - For unix, this look for `{R_HOME}/bin/R`
/// - For Windows, this looks for `{R_HOME}/bin/R.exe` and `{R_HOME}/bin/R.bat` (for rig compatibility)
pub fn r_executable(r_home: &Path) -> Option<PathBuf> {
    let r_bin = r_home.join("bin");

    for command in COMMAND_R_NAMES {
        let candidate = r_bin.join(command);

        if candidate.exists() {
            return Some(candidate);
        }
    }

    None
}

/// Execute a `Command` for R found on the `PATH`
///
/// This is like `r_command()` but the R executable is executed as a bare name and the
/// shell executes it from `PATH`.
pub fn r_command_from_path<F>(build: F) -> io::Result<Output>
where
    F: Fn(&mut Command),
{
    assert!(!COMMAND_R_NAMES.is_empty());

    // Use the bare command names so they are found from the `PATH`
    let locations: Vec<PathBuf> = COMMAND_R_NAMES
        .map(|loc| std::path::Path::new(loc).to_path_buf())
        .into();

    r_command_from_locs(locations, build)
}

fn r_command_from_locs<F>(locations: Vec<PathBuf>, build: F) -> io::Result<Output>
where
    F: Fn(&mut Command),
{
    let mut out = None;

    for program in locations.into_iter() {
        // Build the `Command` from the user's function
        let mut command = Command::new(program);
        build(&mut command);

        // Run it, waiting on it to finish.
        // Store it as `out` no matter what. If all locations fail
        // we end up returning the last failure.
        let result = command.output();
        let ok = result.is_ok();
        out = Some(result);

        if ok {
            // We had a successful command, don't try any more
            break;
        }
    }

    // Unwrap: The `assert!` above ensures at least 1 program location is provided
    out.unwrap()
}
