use std::collections::btree_map::Entry;
use std::{env, fs};

use crate::core::PackageId;
use crate::core::{PackageIdSpec, SourceId};
use crate::ops::common_for_install_and_uninstall::*;
use crate::util::errors::CargoResult;
use crate::util::paths;
use crate::util::Config;
use crate::util::{FileLock, Filesystem};

pub fn uninstall(
    root: Option<&str>,
    specs: Vec<&str>,
    bins: &[String],
    config: &Config,
) -> CargoResult<()> {
    if specs.len() > 1 && !bins.is_empty() {
        failure::bail!("A binary can only be associated with a single installed package, specifying multiple specs with --bin is redundant.");
    }

    let root = resolve_root(root, config)?;
    let scheduled_error = if specs.len() == 1 {
        uninstall_one(&root, specs[0], bins, config)?;
        false
    } else if specs.is_empty() {
        uninstall_cwd(&root, bins, config)?;
        false
    } else {
        let mut succeeded = vec![];
        let mut failed = vec![];
        for spec in specs {
            let root = root.clone();
            match uninstall_one(&root, spec, bins, config) {
                Ok(()) => succeeded.push(spec),
                Err(e) => {
                    crate::handle_error(&e, &mut config.shell());
                    failed.push(spec)
                }
            }
        }

        let mut summary = vec![];
        if !succeeded.is_empty() {
            summary.push(format!(
                "Successfully uninstalled {}!",
                succeeded.join(", ")
            ));
        }
        if !failed.is_empty() {
            summary.push(format!(
                "Failed to uninstall {} (see error(s) above).",
                failed.join(", ")
            ));
        }

        if !succeeded.is_empty() || !failed.is_empty() {
            config.shell().status("Summary", summary.join(" "))?;
        }

        !failed.is_empty()
    };

    if scheduled_error {
        failure::bail!("some packages failed to uninstall");
    }

    Ok(())
}

pub fn uninstall_one(
    root: &Filesystem,
    spec: &str,
    bins: &[String],
    config: &Config,
) -> CargoResult<()> {
    let crate_metadata = metadata(config, root)?;
    let metadata = read_crate_list(&crate_metadata)?;
    let pkgid = PackageIdSpec::query_str(spec, metadata.v1().keys().cloned())?;
    uninstall_pkgid(&crate_metadata, metadata, pkgid, bins, config)
}

fn uninstall_cwd(root: &Filesystem, bins: &[String], config: &Config) -> CargoResult<()> {
    let crate_metadata = metadata(config, root)?;
    let metadata = read_crate_list(&crate_metadata)?;
    let source_id = SourceId::for_path(config.cwd())?;
    let src = path_source(source_id, config)?;
    let (pkg, _source) = select_pkg(src, None, None, config, true, &mut |path| {
        path.read_packages()
    })?;
    let pkgid = pkg.package_id();
    uninstall_pkgid(&crate_metadata, metadata, pkgid, bins, config)
}

fn uninstall_pkgid(
    crate_metadata: &FileLock,
    mut metadata: CrateListingV1,
    pkgid: PackageId,
    bins: &[String],
    config: &Config,
) -> CargoResult<()> {
    let mut to_remove = Vec::new();
    {
        let mut installed = match metadata.v1_mut().entry(pkgid) {
            Entry::Occupied(e) => e,
            Entry::Vacant(..) => failure::bail!("package `{}` is not installed", pkgid),
        };

        let dst = crate_metadata.parent().join("bin");
        for bin in installed.get() {
            let bin = dst.join(bin);
            if fs::metadata(&bin).is_err() {
                failure::bail!(
                    "corrupt metadata, `{}` does not exist when it should",
                    bin.display()
                )
            }
        }

        let bins = bins
            .iter()
            .map(|s| {
                if s.ends_with(env::consts::EXE_SUFFIX) {
                    s.to_string()
                } else {
                    format!("{}{}", s, env::consts::EXE_SUFFIX)
                }
            })
            .collect::<Vec<_>>();

        for bin in bins.iter() {
            if !installed.get().contains(bin) {
                failure::bail!("binary `{}` not installed as part of `{}`", bin, pkgid)
            }
        }

        if bins.is_empty() {
            to_remove.extend(installed.get().iter().map(|b| dst.join(b)));
            installed.get_mut().clear();
        } else {
            for bin in bins.iter() {
                to_remove.push(dst.join(bin));
                installed.get_mut().remove(bin);
            }
        }
        if installed.get().is_empty() {
            installed.remove();
        }
    }
    write_crate_list(&crate_metadata, metadata)?;
    for bin in to_remove {
        config.shell().status("Removing", bin.display())?;
        paths::remove_file(bin)?;
    }

    Ok(())
}
