//! `xtask-unpublished` outputs a table with publish status --- a local version
//! and a version on crates.io for comparisons.
//!
//! This aims to help developers check if there is any crate required a new
//! publish, as well as detect if a version bump is needed in CI pipeline.

use std::collections::HashSet;

use cargo::core::registry::PackageRegistry;
use cargo::core::QueryKind;
use cargo::core::Registry;
use cargo::core::SourceId;
use cargo::ops::Packages;
use cargo::util::command_prelude::*;

pub fn cli() -> clap::Command {
    clap::Command::new("xtask-unpublished")
        .arg_package_spec_simple("Package to inspect the published status")
        .arg(
            opt(
                "verbose",
                "Use verbose output (-vv very verbose/build.rs output)",
            )
            .short('v')
            .action(ArgAction::Count)
            .global(true),
        )
        .arg_quiet()
        .arg(
            opt("color", "Coloring: auto, always, never")
                .value_name("WHEN")
                .global(true),
        )
        .arg(flag("frozen", "Require Cargo.lock and cache are up to date").global(true))
        .arg(flag("locked", "Require Cargo.lock is up to date").global(true))
        .arg(flag("offline", "Run without accessing the network").global(true))
        .arg(multi_opt("config", "KEY=VALUE", "Override a configuration value").global(true))
        .arg(
            Arg::new("unstable-features")
                .help("Unstable (nightly-only) flags to Cargo, see 'cargo -Z help' for details")
                .short('Z')
                .value_name("FLAG")
                .action(ArgAction::Append)
                .global(true),
        )
}

pub fn exec(args: &clap::ArgMatches, config: &mut cargo::util::Config) -> cargo::CliResult {
    config_configure(config, args)?;

    unpublished(args, config)?;

    Ok(())
}

fn config_configure(config: &mut Config, args: &ArgMatches) -> CliResult {
    let verbose = args.verbose();
    // quiet is unusual because it is redefined in some subcommands in order
    // to provide custom help text.
    let quiet = args.flag("quiet");
    let color = args.get_one::<String>("color").map(String::as_str);
    let frozen = args.flag("frozen");
    let locked = args.flag("locked");
    let offline = args.flag("offline");
    let mut unstable_flags = vec![];
    if let Some(values) = args.get_many::<String>("unstable-features") {
        unstable_flags.extend(values.cloned());
    }
    let mut config_args = vec![];
    if let Some(values) = args.get_many::<String>("config") {
        config_args.extend(values.cloned());
    }
    config.configure(
        verbose,
        quiet,
        color,
        frozen,
        locked,
        offline,
        &None,
        &unstable_flags,
        &config_args,
    )?;
    Ok(())
}

fn unpublished(args: &clap::ArgMatches, config: &mut cargo::util::Config) -> cargo::CliResult {
    let ws = args.workspace(config)?;

    let members_to_inspect: HashSet<_> = {
        let pkgs = args.packages_from_flags()?;
        if let Packages::Packages(_) = pkgs {
            HashSet::from_iter(pkgs.get_packages(&ws)?)
        } else {
            HashSet::from_iter(ws.members())
        }
    };

    let mut results = Vec::new();
    {
        let mut registry = PackageRegistry::new(config)?;
        let _lock = config.acquire_package_cache_lock()?;
        registry.lock_patches();
        let source_id = SourceId::crates_io(config)?;

        for member in members_to_inspect {
            let name = member.name();
            let current = member.version();
            if member.publish() == &Some(vec![]) {
                log::trace!("skipping {name}, `publish = false`");
                continue;
            }

            let version_req = format!("<={current}");
            let query =
                cargo::core::dependency::Dependency::parse(name, Some(&version_req), source_id)?;
            let possibilities = loop {
                // Exact to avoid returning all for path/git
                match registry.query_vec(&query, QueryKind::Exact) {
                    std::task::Poll::Ready(res) => {
                        break res?;
                    }
                    std::task::Poll::Pending => registry.block_until_ready()?,
                }
            };
            let (last, published) = possibilities
                .iter()
                .map(|s| s.version())
                .max()
                .map(|last| (last.to_string(), last == current))
                .unwrap_or(("-".to_string(), false));

            results.push(vec![
                name.to_string(),
                last,
                current.to_string(),
                if published { "yes" } else { "no" }.to_string(),
            ]);
        }
    }
    results.sort();

    if results.is_empty() {
        return Ok(());
    }

    results.insert(
        0,
        vec![
            "name".to_owned(),
            "crates.io".to_owned(),
            "local".to_owned(),
            "published?".to_owned(),
        ],
    );

    output_table(results);

    Ok(())
}

/// Outputs a markdown table like this.
///
/// ```text
/// | name             | crates.io | local  | published? |
/// |------------------|-----------|--------|------------|
/// | cargo            | 0.70.1    | 0.72.0 | no         |
/// | cargo-platform   | 0.1.2     | 0.1.2  | yes        |
/// | cargo-util       | -         | 0.2.4  | no         |
/// | crates-io        | 0.36.0    | 0.36.0 | yes        |
/// | home             | -         | 0.5.6  | no         |
/// ```
fn output_table(table: Vec<Vec<String>>) {
    let header = table.first().unwrap();
    let paddings = table.iter().fold(vec![0; header.len()], |mut widths, row| {
        for (width, field) in widths.iter_mut().zip(row) {
            *width = usize::max(*width, field.len());
        }
        widths
    });

    let print = |row: &[_]| {
        for (field, pad) in row.iter().zip(&paddings) {
            print!("| {field:pad$} ");
        }
        println!("|");
    };

    print(header);

    paddings.iter().for_each(|fill| print!("|-{:-<fill$}-", ""));
    println!("|");

    table.iter().skip(1).for_each(|r| print(r));
}

#[test]
fn verify_cli() {
    cli().debug_assert();
}
