#![forbid(unsafe_code)]

use std::{
    env, fs, io,
    path::{Path, PathBuf},
};

const COMMANDS: [&str; 2] = ["ironstorage", "ironstorage-tui"];

pub(crate) fn install_packaged_launchers() -> io::Result<[PathBuf; 2]> {
    let executable = env::current_exe()?;
    let bundle_directory = executable.parent().ok_or_else(|| {
        io::Error::new(
            io::ErrorKind::InvalidData,
            "the desktop executable has no parent directory",
        )
    })?;
    let home = env::var_os("HOME")
        .or_else(|| env::var_os("USERPROFILE"))
        .map(PathBuf::from)
        .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "home directory not found"))?;
    install_launchers(bundle_directory, &home)
}

fn install_launchers(bundle_directory: &Path, home: &Path) -> io::Result<[PathBuf; 2]> {
    let sources = COMMANDS
        .map(|command| bundle_directory.join(format!("{command}{}", env::consts::EXE_SUFFIX)));
    for source in &sources {
        if !source.is_file() {
            return Err(io::Error::new(
                io::ErrorKind::NotFound,
                format!("packaged command not found: {}", source.display()),
            ));
        }
    }
    let sources = [sources[0].canonicalize()?, sources[1].canonicalize()?];

    let directory = home.join(".local/bin");
    let targets = COMMANDS.map(|command| directory.join(command));
    for target in &targets {
        match fs::symlink_metadata(target) {
            Ok(metadata) if metadata.is_dir() => {
                return Err(io::Error::new(
                    io::ErrorKind::IsADirectory,
                    format!("command link target is a directory: {}", target.display()),
                ));
            }
            Ok(_) => {}
            Err(error) if error.kind() == io::ErrorKind::NotFound => {}
            Err(error) => return Err(error),
        }
    }

    fs::create_dir_all(&directory)?;
    for (source, target) in sources.iter().zip(&targets) {
        if fs::symlink_metadata(target).is_ok() {
            fs::remove_file(target)?;
        }
        symlink(source, target)?;
    }
    Ok(targets)
}

#[cfg(unix)]
fn symlink(source: &Path, target: &Path) -> io::Result<()> {
    std::os::unix::fs::symlink(source, target)
}

#[cfg(windows)]
fn symlink(source: &Path, target: &Path) -> io::Result<()> {
    std::os::windows::fs::symlink_file(source, target)
}

#[cfg(not(any(unix, windows)))]
fn symlink(_source: &Path, _target: &Path) -> io::Result<()> {
    Err(io::Error::new(
        io::ErrorKind::Unsupported,
        "command links are not supported on this platform",
    ))
}

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

    fn packaged_commands(directory: &Path) -> [PathBuf; 2] {
        let sources =
            COMMANDS.map(|command| directory.join(format!("{command}{}", env::consts::EXE_SUFFIX)));
        for source in &sources {
            fs::write(source, command_contents(source)).expect("write packaged command");
        }
        sources
    }

    fn command_contents(path: &Path) -> String {
        path.file_name()
            .expect("command name")
            .to_string_lossy()
            .into_owned()
    }

    #[test]
    fn installs_both_packaged_commands_and_replaces_existing_files() {
        let bundle = tempfile::tempdir().expect("bundle directory");
        let home = tempfile::tempdir().expect("home directory");
        let sources = packaged_commands(bundle.path());
        let bin = home.path().join(".local/bin");
        fs::create_dir_all(&bin).expect("bin directory");
        fs::write(bin.join("ironstorage"), "old command").expect("old command");

        let targets = install_launchers(bundle.path(), home.path()).expect("install launchers");
        install_launchers(bundle.path(), home.path()).expect("reinstall launchers");

        for ((source, target), expected_target) in sources
            .iter()
            .zip(&targets)
            .zip([bin.join("ironstorage"), bin.join("ironstorage-tui")])
        {
            assert_eq!(target, &expected_target);
            assert_eq!(
                fs::read_link(target).expect("read link"),
                source.canonicalize().expect("canonical source")
            );
        }
    }

    #[test]
    fn validates_all_sources_before_changing_existing_commands() {
        let bundle = tempfile::tempdir().expect("bundle directory");
        let home = tempfile::tempdir().expect("home directory");
        let first = bundle
            .path()
            .join(format!("ironstorage{}", env::consts::EXE_SUFFIX));
        fs::write(&first, "new command").expect("packaged command");
        let bin = home.path().join(".local/bin");
        fs::create_dir_all(&bin).expect("bin directory");
        let existing = bin.join("ironstorage");
        fs::write(&existing, "old command").expect("old command");

        let error = install_launchers(bundle.path(), home.path()).expect_err("missing TUI");

        assert_eq!(error.kind(), io::ErrorKind::NotFound);
        assert_eq!(
            fs::read_to_string(existing).expect("existing command"),
            "old command"
        );
    }

    #[test]
    fn rejects_directory_targets_before_changing_other_commands() {
        let bundle = tempfile::tempdir().expect("bundle directory");
        let home = tempfile::tempdir().expect("home directory");
        packaged_commands(bundle.path());
        let bin = home.path().join(".local/bin");
        fs::create_dir_all(bin.join("ironstorage-tui")).expect("directory target");
        let existing = bin.join("ironstorage");
        fs::write(&existing, "old command").expect("old command");

        let error = install_launchers(bundle.path(), home.path()).expect_err("directory target");

        assert_eq!(error.kind(), io::ErrorKind::IsADirectory);
        assert_eq!(
            fs::read_to_string(existing).expect("existing command"),
            "old command"
        );
    }
}
