use cargo::core::dependency::DepKind;
use cargo::core::PackageIdSpec;
use cargo::core::Workspace;
use cargo::ops::cargo_remove::remove;
use cargo::ops::cargo_remove::RemoveOptions;
use cargo::ops::resolve_ws;
use cargo::util::command_prelude::*;
use cargo::util::print_available_packages;
use cargo::util::toml_mut::dependency::Dependency;
use cargo::util::toml_mut::dependency::MaybeWorkspace;
use cargo::util::toml_mut::dependency::Source;
use cargo::util::toml_mut::manifest::DepTable;
use cargo::util::toml_mut::manifest::LocalManifest;
use cargo::CargoResult;

pub fn cli() -> clap::Command {
    clap::Command::new("remove")
        // Subcommand aliases are handled in `aliased_command()`.
        // .alias("rm")
        .about("Remove dependencies from a Cargo.toml manifest file")
        .args([clap::Arg::new("dependencies")
            .action(clap::ArgAction::Append)
            .required(true)
            .num_args(1..)
            .value_name("DEP_ID")
            .help("Dependencies to be removed")])
        .arg_package("Package to remove from")
        .arg_manifest_path()
        .arg_quiet()
        .arg_dry_run("Don't actually write the manifest")
        .next_help_heading("Section")
        .args([
            clap::Arg::new("dev")
                .long("dev")
                .conflicts_with("build")
                .action(clap::ArgAction::SetTrue)
                .group("section")
                .help("Remove as development dependency"),
            clap::Arg::new("build")
                .long("build")
                .conflicts_with("dev")
                .action(clap::ArgAction::SetTrue)
                .group("section")
                .help("Remove as build dependency"),
            clap::Arg::new("target")
                .long("target")
                .num_args(1)
                .value_name("TARGET")
                .value_parser(clap::builder::NonEmptyStringValueParser::new())
                .help("Remove as dependency from the given target platform"),
        ])
}

pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult {
    let dry_run = args.dry_run();

    let workspace = args.workspace(config)?;

    if args.is_present_with_zero_values("package") {
        print_available_packages(&workspace)?;
    }

    let packages = args.packages_from_flags()?;
    let packages = packages.get_packages(&workspace)?;
    let spec = match packages.len() {
        0 => {
            return Err(CliError::new(
                anyhow::format_err!(
                    "no packages selected to modify.  Please specify one with `-p <PKGID>`"
                ),
                101,
            ));
        }
        1 => packages[0],
        _ => {
            let names = packages.iter().map(|p| p.name()).collect::<Vec<_>>();
            return Err(CliError::new(
                anyhow::format_err!(
                    "`cargo remove` could not determine which package to modify. \
                    Use the `--package` option to specify a package. \n\
                    available packages: {}",
                    names.join(", ")
                ),
                101,
            ));
        }
    };

    let dependencies = args
        .get_many::<String>("dependencies")
        .expect("required(true)")
        .cloned()
        .collect::<Vec<_>>();

    let section = parse_section(args);

    let options = RemoveOptions {
        config,
        spec,
        dependencies,
        section,
        dry_run,
    };
    remove(&options)?;

    if !dry_run {
        // Clean up the workspace
        gc_workspace(&workspace)?;

        // Reload the workspace since we've changed dependencies
        let ws = args.workspace(config)?;
        resolve_ws(&ws)?;
    }

    Ok(())
}

fn parse_section(args: &ArgMatches) -> DepTable {
    let dev = args.flag("dev");
    let build = args.flag("build");

    let kind = if dev {
        DepKind::Development
    } else if build {
        DepKind::Build
    } else {
        DepKind::Normal
    };

    let mut table = DepTable::new().set_kind(kind);

    if let Some(target) = args.get_one::<String>("target") {
        assert!(!target.is_empty(), "Target specification may not be empty");
        table = table.set_target(target);
    }

    table
}

/// Clean up the workspace.dependencies, profile, patch, and replace sections of the root manifest
/// by removing dependencies which no longer have a reference to them.
fn gc_workspace(workspace: &Workspace<'_>) -> CargoResult<()> {
    let mut manifest: toml_edit::Document =
        cargo_util::paths::read(workspace.root_manifest())?.parse()?;
    let mut is_modified = true;

    let members = workspace
        .members()
        .map(|p| LocalManifest::try_new(p.manifest_path()))
        .collect::<CargoResult<Vec<_>>>()?;

    let mut dependencies = members
        .iter()
        .flat_map(|manifest| {
            manifest.get_sections().into_iter().flat_map(|(_, table)| {
                table
                    .as_table_like()
                    .unwrap()
                    .iter()
                    .map(|(key, item)| Dependency::from_toml(&manifest.path, key, item))
                    .collect::<Vec<_>>()
            })
        })
        .collect::<CargoResult<Vec<_>>>()?;

    // Clean up the workspace.dependencies section and replace instances of
    // workspace dependencies with their definitions
    if let Some(toml_edit::Item::Table(deps_table)) = manifest
        .get_mut("workspace")
        .and_then(|t| t.get_mut("dependencies"))
    {
        deps_table.set_implicit(true);
        for (key, item) in deps_table.iter_mut() {
            let ws_dep = Dependency::from_toml(&workspace.root(), key.get(), item)?;

            // search for uses of this workspace dependency
            let mut is_used = false;
            for dep in dependencies.iter_mut().filter(|d| {
                d.toml_key() == key.get() && matches!(d.source(), Some(Source::Workspace(_)))
            }) {
                // HACK: Replace workspace references in `dependencies` to simplify later GC steps:
                // 1. Avoid having to look it up again to determine the dependency source / spec
                // 2. The entry might get deleted, preventing us from looking it up again
                //
                // This does lose extra information, like features enabled, but that shouldn't be a
                // problem for GC
                *dep = ws_dep.clone();

                is_used = true;
            }

            if !is_used {
                *item = toml_edit::Item::None;
                is_modified = true;
            }
        }
    }

    // Clean up the profile section
    //
    // Example tables:
    // - profile.dev.package.foo
    // - profile.release.package."*"
    // - profile.release.package."foo:2.1.0"
    if let Some(toml_edit::Item::Table(profile_section_table)) = manifest.get_mut("profile") {
        profile_section_table.set_implicit(true);

        for (_, item) in profile_section_table.iter_mut() {
            if let toml_edit::Item::Table(profile_table) = item {
                profile_table.set_implicit(true);

                if let Some(toml_edit::Item::Table(package_table)) =
                    profile_table.get_mut("package")
                {
                    package_table.set_implicit(true);

                    for (key, item) in package_table.iter_mut() {
                        if !spec_has_match(
                            &PackageIdSpec::parse(key.get())?,
                            &dependencies,
                            workspace.config(),
                        )? {
                            *item = toml_edit::Item::None;
                            is_modified = true;
                        }
                    }
                }
            }
        }
    }

    // Clean up the patch section
    if let Some(toml_edit::Item::Table(patch_section_table)) = manifest.get_mut("patch") {
        patch_section_table.set_implicit(true);

        // The key in each of the subtables is a source (either a registry or a URL)
        for (source, item) in patch_section_table.iter_mut() {
            if let toml_edit::Item::Table(patch_table) = item {
                patch_table.set_implicit(true);

                for (key, item) in patch_table.iter_mut() {
                    let package_name =
                        Dependency::from_toml(&workspace.root_manifest(), key.get(), item)?.name;
                    if !source_has_match(
                        &package_name,
                        source.get(),
                        &dependencies,
                        workspace.config(),
                    )? {
                        *item = toml_edit::Item::None;
                    }
                }
            }
        }
    }

    // Clean up the replace section
    if let Some(toml_edit::Item::Table(table)) = manifest.get_mut("replace") {
        table.set_implicit(true);

        for (key, item) in table.iter_mut() {
            if !spec_has_match(
                &PackageIdSpec::parse(key.get())?,
                &dependencies,
                workspace.config(),
            )? {
                *item = toml_edit::Item::None;
                is_modified = true;
            }
        }
    }

    if is_modified {
        cargo_util::paths::write(workspace.root_manifest(), manifest.to_string().as_bytes())?;
    }

    Ok(())
}

/// Check whether or not a package ID spec matches any non-workspace dependencies.
fn spec_has_match(
    spec: &PackageIdSpec,
    dependencies: &[Dependency],
    config: &Config,
) -> CargoResult<bool> {
    for dep in dependencies {
        if spec.name().as_str() != &dep.name {
            continue;
        }

        let version_matches = match (spec.version(), dep.version()) {
            (Some(v), Some(vq)) => semver::VersionReq::parse(vq)?.matches(v),
            (Some(_), None) => false,
            (None, None | Some(_)) => true,
        };
        if !version_matches {
            continue;
        }

        match dep.source_id(config)? {
            MaybeWorkspace::Other(source_id) => {
                if spec.url().map(|u| u == source_id.url()).unwrap_or(true) {
                    return Ok(true);
                }
            }
            MaybeWorkspace::Workspace(_) => {}
        }
    }

    Ok(false)
}

/// Check whether or not a source (URL or registry name) matches any non-workspace dependencies.
fn source_has_match(
    name: &str,
    source: &str,
    dependencies: &[Dependency],
    config: &Config,
) -> CargoResult<bool> {
    for dep in dependencies {
        if &dep.name != name {
            continue;
        }

        match dep.source_id(config)? {
            MaybeWorkspace::Other(source_id) => {
                if source_id.is_registry() {
                    if source_id.display_registry_name() == source
                        || source_id.url().as_str() == source
                    {
                        return Ok(true);
                    }
                } else if source_id.is_git() {
                    if source_id.url().as_str() == source {
                        return Ok(true);
                    }
                }
            }
            MaybeWorkspace::Workspace(_) => {}
        }
    }

    Ok(false)
}
