use std::env;
use std::fs;
use std::path::{Path, PathBuf};
#[cfg(target_os = "windows")]
use std::process::Command;

use crate::error::ForgeError;
use crate::fsutil::atomic_write_file;
use crate::model::InstallConfig;
use crate::paths::managed_bin_dir;
use crate::ui::{self, StatusKind};
use crate::util::{home_dir, resolve_command, set_process_var};

mod cargo_config;
mod paths;
pub(crate) mod typed;

use crate::execution::environment::paths::{display_paths, paths_equal, unix_shell_profile};

pub(crate) fn apply_planned_environment(config: &InstallConfig) -> Result<(), ForgeError> {
    prepare_detection_environment(config);
    refresh_macos_process_environment();
    apply_configured_environment(config)
}

pub(crate) fn prepare_detection_environment(config: &InstallConfig) {
    prepend_process_path([managed_bin_dir()], true);
    typed::apply_process_paths(&config.environment.mutations);
}

pub(crate) fn refresh_install_finish_environment(config: &InstallConfig) -> Result<(), ForgeError> {
    ui::stdout_status(StatusKind::Info, "Refreshing the terminal environment.");
    apply_configured_environment(config)?;
    refresh_rust_process_environment();
    refresh_node_process_environment();
    if cfg!(unix) {
        if let Ok(profile) = unix_shell_profile() {
            ui::stdout_status(
                StatusKind::Success,
                &format!(
                    "Process environment refreshed · profile {}",
                    profile.path.display()
                ),
            );
        } else {
            ui::stdout_status(StatusKind::Success, "Process environment refreshed.");
        }
        ui::stdout_status(
            StatusKind::Hint,
            "Open a new terminal if installed commands are not yet available.",
        );
    } else {
        ui::stdout_status(StatusKind::Success, "Process environment refreshed.");
        ui::stdout_status(
            StatusKind::Hint,
            "Open a new PowerShell or CMD window if installed commands are not yet available.",
        );
    }
    Ok(())
}

fn refresh_macos_process_environment() {
    if !cfg!(target_os = "macos") {
        return;
    }
    let brew = resolve_command("brew");
    let Some(bin) = brew.parent().filter(|_| brew.is_file()) else {
        return;
    };
    let added = prepend_process_path([bin.to_path_buf()], false);
    if !added.is_empty() {
        ui::stdout_status(
            StatusKind::Info,
            &format!("Homebrew added to process PATH · {}", display_paths(&added)),
        );
    }
}

fn apply_configured_environment(config: &InstallConfig) -> Result<(), ForgeError> {
    typed::apply(&config.environment.mutations)
}

#[cfg(target_os = "windows")]
fn persist_windows_user_variable(name: &str, value: &str) -> Result<(), ForgeError> {
    let output = Command::new("reg.exe")
        .args([
            "add",
            r"HKCU\Environment",
            "/v",
            name,
            "/t",
            "REG_SZ",
            "/d",
            value,
            "/f",
        ])
        .output()
        .map_err(|error| {
            ForgeError::Command(format!(
                "failed to write the Windows user environment variable {name}: {error}"
            ))
        })?;
    if output.status.success() {
        Ok(())
    } else {
        Err(ForgeError::Command(format!(
            "failed to write the Windows user environment variable {name}: {}",
            String::from_utf8_lossy(&output.stderr).trim()
        )))
    }
}

#[cfg(not(target_os = "windows"))]
fn persist_windows_user_variable(_: &str, _: &str) -> Result<(), ForgeError> {
    Ok(())
}

pub(crate) fn refresh_rust_process_environment() {
    let cargo_home = env::var_os("CARGO_HOME")
        .filter(|value| !value.is_empty())
        .map(PathBuf::from)
        .unwrap_or_else(|| home_dir().join(".cargo"));
    let rustup_home = env::var_os("RUSTUP_HOME")
        .filter(|value| !value.is_empty())
        .map(PathBuf::from)
        .unwrap_or_else(|| home_dir().join(".rustup"));

    set_process_var("CARGO_HOME", &cargo_home);
    set_process_var("RUSTUP_HOME", &rustup_home);

    let added = prepend_process_path([cargo_home.join("bin")], true);
    if !added.is_empty() {
        ui::stdout_status(
            StatusKind::Info,
            &format!("Rust environment refreshed · {}", display_paths(&added)),
        );
    }
}

pub(crate) fn refresh_node_process_environment() {
    let mut candidates = Vec::new();
    if cfg!(windows) {
        push_env_path(&mut candidates, "NVM_HOME");
        push_env_path(&mut candidates, "NVM_SYMLINK");
        if let Some(path) = env::var_os("LOCALAPPDATA") {
            candidates.push(PathBuf::from(path).join("nvm"));
        }
        if let Some(path) = env::var_os("APPDATA") {
            candidates.push(PathBuf::from(path).join("nvm"));
        }
        if let Some(path) = env::var_os("ProgramFiles") {
            candidates.push(PathBuf::from(path).join("nodejs"));
        }
        if let Some(path) = env::var_os("ProgramFiles(x86)") {
            candidates.push(PathBuf::from(path).join("nodejs"));
        }
    } else {
        candidates.push(home_dir().join(".local").join("bin"));
        candidates.extend(discover_local_node_bins());
        let nvm_dir = env::var_os("NVM_DIR")
            .filter(|value| !value.is_empty())
            .map(PathBuf::from)
            .unwrap_or_else(|| home_dir().join(".nvm"));
        candidates.push(nvm_dir.join("current").join("bin"));
        candidates.extend(discover_nvm_node_bins(&nvm_dir));
    }

    let added = prepend_process_path(candidates, false);
    if !added.is_empty() {
        ui::stdout_status(
            StatusKind::Info,
            &format!("Node.js environment refreshed · {}", display_paths(&added)),
        );
    }
}

pub(crate) fn refresh_python_environment(environment: &Path) {
    let environment = typed::expand_path(&environment.display().to_string());
    let bin = environment.join(if cfg!(windows) { "Scripts" } else { "bin" });
    let added = prepend_process_path([bin], true);
    if !added.is_empty() {
        ui::stdout_status(
            StatusKind::Info,
            &format!(
                "Python tool environment refreshed · {}",
                display_paths(&added)
            ),
        );
    }
}

fn persist_windows_tool_user_path(bin: Option<PathBuf>) -> Result<(), ForgeError> {
    #[cfg(target_os = "windows")]
    {
        let Some(bin) = bin else {
            return Ok(());
        };
        let system_root = env::var_os("SystemRoot")
            .map(PathBuf::from)
            .unwrap_or_else(|| PathBuf::from(r"C:\Windows"));
        let powershell = system_root
            .join("System32")
            .join("WindowsPowerShell")
            .join("v1.0")
            .join("powershell.exe");
        let bin = bin.display().to_string().replace('\'', "''");
        let script = format!(
            "$bin = '{bin}'; $current = [Environment]::GetEnvironmentVariable('Path', 'User'); if ([string]::IsNullOrWhiteSpace($current)) {{ $next = $bin }} elseif (((';' + $current + ';').IndexOf(';' + $bin + ';', [System.StringComparison]::OrdinalIgnoreCase)) -lt 0) {{ $next = $current.TrimEnd(';') + ';' + $bin }} else {{ $next = $current }}; [Environment]::SetEnvironmentVariable('Path', $next, 'User')"
        );
        let output = Command::new(&powershell)
            .args(["-NoProfile", "-NonInteractive", "-Command", &script])
            .output()
            .map_err(|error| {
                ForgeError::Command(format!("failed to write the Windows user PATH: {error}"))
            })?;
        if output.status.success() {
            ui::stdout_status(
                StatusKind::Success,
                &format!("Windows user PATH updated · {bin}"),
            );
            Ok(())
        } else {
            Err(ForgeError::Command(format!(
                "failed to write the Windows user PATH: {}",
                String::from_utf8_lossy(&output.stderr).trim()
            )))
        }
    }

    #[cfg(not(target_os = "windows"))]
    {
        let _ = bin;
        Ok(())
    }
}

fn push_env_path(target: &mut Vec<PathBuf>, name: &str) {
    if let Some(value) = env::var_os(name).filter(|value| !value.is_empty()) {
        target.push(PathBuf::from(value));
    }
}

fn discover_local_node_bins() -> Vec<PathBuf> {
    let opt_dir = home_dir().join(".local").join("opt");
    let Ok(entries) = fs::read_dir(&opt_dir) else {
        return Vec::new();
    };
    let mut bins = entries
        .filter_map(Result::ok)
        .filter_map(|entry| {
            entry
                .file_type()
                .ok()
                .filter(|file_type| file_type.is_dir())?;
            let name = entry.file_name();
            let name = name.to_string_lossy();
            name.starts_with("node-").then(|| entry.path().join("bin"))
        })
        .collect::<Vec<_>>();
    bins.sort();
    bins.reverse();
    bins
}

fn discover_nvm_node_bins(nvm_dir: &Path) -> Vec<PathBuf> {
    let versions_dir = nvm_dir.join("versions").join("node");
    let Ok(entries) = fs::read_dir(&versions_dir) else {
        return Vec::new();
    };
    let mut bins = entries
        .filter_map(Result::ok)
        .filter_map(|entry| {
            entry
                .file_type()
                .ok()
                .filter(|file_type| file_type.is_dir())?;
            Some(entry.path().join("bin"))
        })
        .collect::<Vec<_>>();
    bins.sort();
    bins.reverse();
    bins
}

fn prepend_process_path(
    paths: impl IntoIterator<Item = PathBuf>,
    include_missing: bool,
) -> Vec<PathBuf> {
    let mut added: Vec<PathBuf> = Vec::new();
    let current_path = env::var_os("PATH").unwrap_or_default();
    let mut existing = env::split_paths(&current_path).collect::<Vec<_>>();

    for path in paths {
        if path.as_os_str().is_empty() || (!include_missing && !path.exists()) {
            continue;
        }
        if existing.iter().any(|existing| paths_equal(existing, &path))
            || added.iter().any(|existing| paths_equal(existing, &path))
        {
            continue;
        }
        added.push(path);
    }

    if added.is_empty() {
        return added;
    }

    let mut new_paths = added.clone();
    new_paths.append(&mut existing);
    if let Ok(joined) = env::join_paths(new_paths) {
        set_process_var("PATH", joined);
        added
    } else {
        Vec::new()
    }
}

fn append_lines_if_missing(label: &str, path: &Path, lines: &[String]) -> Result<(), ForgeError> {
    if lines.is_empty() {
        return Ok(());
    }
    if cargo_config::is_cargo_config(path) {
        return cargo_config::merge(label, path, lines);
    }
    let mut existing = fs::read_to_string(path).unwrap_or_default();
    let missing = lines
        .iter()
        .filter(|line| !line.trim().is_empty() && !existing.contains(line.as_str()))
        .cloned()
        .collect::<Vec<_>>();
    if missing.is_empty() {
        ui::stdout_status(
            StatusKind::Info,
            &format!(
                "{label} already contains bot-forge configuration · {}",
                path.display()
            ),
        );
        return Ok(());
    }
    if !existing.is_empty() && !existing.ends_with('\n') {
        existing.push('\n');
    }
    existing.push_str("\n# bot-forge environment\n");
    existing.push_str(&missing.join("\n"));
    existing.push('\n');
    write_text(path, &existing)?;
    ui::stdout_status(
        StatusKind::Success,
        &format!("{label} updated · {}", path.display()),
    );
    Ok(())
}

fn write_text(path: &Path, contents: &str) -> Result<(), ForgeError> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).map_err(|source| ForgeError::Io {
            path: parent.to_path_buf(),
            source,
        })?;
    }
    atomic_write_file(path, contents.as_bytes())
}

#[cfg(test)]
mod tests {
    use crate::execution::TEST_ENV_LOCK;
    use crate::execution::environment::typed::{expand_path, mutation_applies};
    #[cfg(unix)]
    use crate::execution::environment::unix_shell_profile;
    use crate::paths::app_home;
    use std::{env, ffi::OsString, fs};

    use crate::execution::environment::{
        append_lines_if_missing, refresh_node_process_environment, refresh_python_environment,
        refresh_rust_process_environment,
    };
    use crate::util::{remove_process_var, set_process_var};

    #[test]
    fn typed_mutation_platform_and_value_expansion_are_explicit() {
        let _guard = TEST_ENV_LOCK.lock().unwrap();
        assert!(mutation_applies(&["*".into()]));
        assert!(mutation_applies(&[format!("{}-*", std::env::consts::OS)]));
        assert!(!mutation_applies(&["unsupported-os-*".into()]));
        assert_eq!(expand_path("$BOT_FORGE_HOME/bin"), app_home().join("bin"));
    }
    #[cfg(unix)]
    #[test]
    fn selects_profiles_for_supported_macos_shells_and_rejects_unknown_shells() {
        let _guard = TEST_ENV_LOCK.lock().unwrap();
        let shell = env::var_os("SHELL");
        let home = env::var_os("HOME");
        set_process_var("HOME", "/user-home");

        set_process_var("SHELL", "/bin/zsh");
        let profile = unix_shell_profile().unwrap();
        assert_eq!(profile.path, std::path::Path::new("/user-home/.zprofile"));

        set_process_var("SHELL", "/bin/bash");
        let profile = unix_shell_profile().unwrap();
        assert_eq!(
            profile.path,
            std::path::Path::new(if cfg!(target_os = "macos") {
                "/user-home/.bash_profile"
            } else {
                "/user-home/.bashrc"
            })
        );

        set_process_var("SHELL", "/opt/homebrew/bin/fish");
        let profile = unix_shell_profile().unwrap();
        assert_eq!(
            profile.path,
            std::path::Path::new("/user-home/.config/fish/config.fish")
        );

        set_process_var("SHELL", "/opt/homebrew/bin/nu");
        assert!(unix_shell_profile().is_err());
        restore_env("SHELL", shell);
        restore_env("HOME", home);
    }

    fn restore_env(name: &str, value: Option<OsString>) {
        match value {
            Some(value) => set_process_var(name, value),
            None => remove_process_var(name),
        }
    }

    #[test]
    fn appends_missing_bashrc_lines() {
        let temp =
            std::env::temp_dir().join(format!("bot-forge-envfile-bashrc-{}", std::process::id()));
        let path = temp.join(".bashrc");
        fs::create_dir_all(&temp).unwrap();
        fs::write(&path, "export DEMO=1\n").unwrap();

        append_lines_if_missing("bashrc", &path, &[". \"$HOME/.cargo/env\"".to_string()]).unwrap();
        let contents = fs::read_to_string(&path).unwrap();
        assert!(contents.contains("export DEMO=1"));
        assert!(contents.contains(". \"$HOME/.cargo/env\""));

        append_lines_if_missing("bashrc", &path, &[". \"$HOME/.cargo/env\"".to_string()]).unwrap();
        let contents = fs::read_to_string(&path).unwrap();
        assert_eq!(contents.matches(". \"$HOME/.cargo/env\"").count(), 1);

        fs::remove_dir_all(temp).unwrap();
    }

    #[test]
    fn appends_missing_npmrc_lines() {
        let temp =
            std::env::temp_dir().join(format!("bot-forge-envfile-npmrc-{}", std::process::id()));
        let path = temp.join(".npmrc");
        fs::create_dir_all(&temp).unwrap();
        fs::write(&path, "strict-ssl=false\n").unwrap();

        append_lines_if_missing(
            "npmrc",
            &path,
            &["registry=https://mirror.com/npm/".to_string()],
        )
        .unwrap();
        let contents = fs::read_to_string(&path).unwrap();
        assert!(contents.contains("strict-ssl=false"));
        assert!(contents.contains("registry=https://mirror.com/npm/"));

        append_lines_if_missing(
            "npmrc",
            &path,
            &["registry=https://mirror.com/npm/".to_string()],
        )
        .unwrap();
        let contents = fs::read_to_string(&path).unwrap();
        assert_eq!(
            contents.matches("registry=https://mirror.com/npm/").count(),
            1
        );

        fs::remove_dir_all(temp).unwrap();
    }

    #[test]
    fn refreshes_rust_environment_for_current_process() {
        let _guard = TEST_ENV_LOCK.lock().unwrap();
        let temp =
            std::env::temp_dir().join(format!("bot-forge-envfile-rust-{}", std::process::id()));
        let cargo_home = temp.join(".cargo");
        let old_userprofile = env::var_os("USERPROFILE");
        let old_home = env::var_os("HOME");
        let old_cargo_home = env::var_os("CARGO_HOME");
        let old_rustup_home = env::var_os("RUSTUP_HOME");
        let old_path = env::var_os("PATH");

        set_process_var("USERPROFILE", &temp);
        set_process_var("HOME", &temp);
        remove_process_var("CARGO_HOME");
        remove_process_var("RUSTUP_HOME");
        set_process_var("PATH", "");

        refresh_rust_process_environment();

        assert_eq!(env::var_os("CARGO_HOME").unwrap(), cargo_home.as_os_str());
        assert_eq!(
            env::var_os("RUSTUP_HOME").unwrap(),
            temp.join(".rustup").as_os_str()
        );
        let paths = env::split_paths(&env::var_os("PATH").unwrap()).collect::<Vec<_>>();
        assert!(paths.iter().any(|path| path == &cargo_home.join("bin")));

        restore_env("USERPROFILE", old_userprofile);
        restore_env("HOME", old_home);
        restore_env("CARGO_HOME", old_cargo_home);
        restore_env("RUSTUP_HOME", old_rustup_home);
        restore_env("PATH", old_path);
        let _ = fs::remove_dir_all(temp);
    }

    #[test]
    fn refreshes_python_tool_environment_for_current_process() {
        let _guard = TEST_ENV_LOCK.lock().unwrap();
        let environment = std::env::temp_dir().join(format!(
            "bot-forge-python-environment-{}",
            std::process::id()
        ));
        let expected = environment.join(if cfg!(windows) { "Scripts" } else { "bin" });
        let old_path = env::var_os("PATH");
        set_process_var("PATH", "");

        refresh_python_environment(&environment);

        let paths = env::split_paths(&env::var_os("PATH").unwrap()).collect::<Vec<_>>();
        assert_eq!(paths.first(), Some(&expected));
        restore_env("PATH", old_path);
    }

    #[test]
    fn refreshes_node_environment_for_current_process() {
        let _guard = TEST_ENV_LOCK.lock().unwrap();
        let temp =
            std::env::temp_dir().join(format!("bot-forge-envfile-node-{}", std::process::id()));
        let expected_path = if cfg!(windows) {
            temp.join("nvm")
        } else {
            temp.join(".nvm")
                .join("versions")
                .join("node")
                .join("v20.17.0")
                .join("bin")
        };
        fs::create_dir_all(&expected_path).unwrap();
        let local_bin = temp.join(".local").join("bin");
        let local_node_bin = temp
            .join(".local")
            .join("opt")
            .join("node-v20.17.0-linux-x64")
            .join("bin");
        if !cfg!(windows) {
            fs::create_dir_all(&local_bin).unwrap();
            fs::create_dir_all(&local_node_bin).unwrap();
        }
        let old_userprofile = env::var_os("USERPROFILE");
        let old_home = env::var_os("HOME");
        let old_nvm_dir = env::var_os("NVM_DIR");
        let old_nvm_home = env::var_os("NVM_HOME");
        let old_path = env::var_os("PATH");

        set_process_var("USERPROFILE", &temp);
        set_process_var("HOME", &temp);
        if cfg!(windows) {
            set_process_var("NVM_HOME", &expected_path);
        } else {
            remove_process_var("NVM_DIR");
        }
        set_process_var("PATH", "");

        refresh_node_process_environment();

        let paths = env::split_paths(&env::var_os("PATH").unwrap()).collect::<Vec<_>>();
        assert!(paths.iter().any(|path| path == &expected_path));
        if !cfg!(windows) {
            assert!(paths.iter().any(|path| path == &local_bin));
            assert!(paths.iter().any(|path| path == &local_node_bin));
        }

        restore_env("USERPROFILE", old_userprofile);
        restore_env("HOME", old_home);
        restore_env("NVM_DIR", old_nvm_dir);
        restore_env("NVM_HOME", old_nvm_home);
        restore_env("PATH", old_path);
        let _ = fs::remove_dir_all(temp);
    }
}