mod crates_io;
mod metadata;

use std::path::Path;

use super::{FetchError, OutdatedDependency, PackageManager};

pub struct Cargo;

impl PackageManager for Cargo {
    /// Ask `cargo metadata` whether this directory is the workspace root.
    /// Returns true for both standalone projects and workspace roots,
    /// false for workspace members (whose root is an ancestor).
    fn is_project_root(&self, target: &Path) -> bool {
        super::run_and_check_root(
            "cargo",
            &["metadata", "--no-deps", "--format-version", "1"],
            target,
            |v| v["workspace_root"].as_str().map(String::from),
        )
    }

    fn fetch_outdated(&self, target: &Path) -> Result<Vec<OutdatedDependency>, FetchError> {
        let direct_deps = metadata::collect_direct_deps(target)?;

        let latest_versions = fetch_latest_versions(&direct_deps);

        let mut outdated = Vec::new();
        let mut errors = Vec::new();

        for (dependency, result) in &latest_versions {
            match result {
                Ok(Some(latest)) if latest > &dependency.version => {
                    outdated.push(OutdatedDependency {
                        name: dependency.name.clone(),
                        current: dependency.version.clone(),
                        latest: latest.clone(),
                        location: dependency.location_relative_to(target),
                    });
                }
                Ok(_) => {}
                Err(error) => errors.push(format!("{}: {error}", dependency.name)),
            }
        }

        if outdated.is_empty() && errors.len() == direct_deps.len() && !direct_deps.is_empty() {
            return Err(FetchError::Failed(format!(
                "all registry lookups failed: {}",
                errors.join(", ")
            )));
        }

        Ok(outdated)
    }
}

fn fetch_latest_versions(
    dependencies: &[metadata::DirectDep],
) -> Vec<(
    &metadata::DirectDep,
    Result<Option<semver::Version>, FetchError>,
)> {
    std::thread::scope(|scope| {
        let handles: Vec<_> = dependencies
            .iter()
            .map(|dependency| {
                (
                    scope.spawn(move || crates_io::fetch_latest_version(&dependency.name)),
                    dependency,
                )
            })
            .collect();

        handles
            .into_iter()
            .map(|(handle, dependency)| match handle.join() {
                Ok(result) => (dependency, result),
                Err(_) => (
                    dependency,
                    Err(FetchError::Failed(format!(
                        "registry lookup failed for {}",
                        dependency.name
                    ))),
                ),
            })
            .collect()
    })
}
