use std::collections::HashSet;

use crate::cli::args::ToolArg;
use crate::config::Config;
use crate::toolset::{OutdatedInfo, ToolsetBuilder};
use crate::ui::table;
use eyre::Result;
use indexmap::IndexMap;

/// Shows outdated tool versions
#[derive(Debug, clap::Args)]
#[clap(verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)]
pub struct Outdated {
    /// Tool(s) to show outdated versions for
    /// e.g.: node@20 python@3.10
    /// If not specified, all tools in global and local configs will be shown
    #[clap(value_name = "TOOL@VERSION", verbatim_doc_comment)]
    pub tool: Vec<ToolArg>,

    /// Compares against the latest versions available, not what matches the current config
    ///
    /// For example, if you have `node = "20"` in your config by default `mise outdated` will only
    /// show other 20.x versions, not 21.x or 22.x versions.
    ///
    /// Using this flag, if there are 21.x or newer versions it will display those instead of 20.x.
    #[clap(long, short = 'l', verbatim_doc_comment)]
    pub bump: bool,

    /// Output in JSON format
    #[clap(short = 'J', long, verbatim_doc_comment)]
    pub json: bool,

    /// Don't show table header
    #[clap(long)]
    pub no_header: bool,
}

impl Outdated {
    pub fn run(self) -> Result<()> {
        let config = Config::try_get()?;
        let mut ts = ToolsetBuilder::new().with_args(&self.tool).build(&config)?;
        let tool_set = self
            .tool
            .iter()
            .map(|t| t.backend.clone())
            .collect::<HashSet<_>>();
        ts.versions
            .retain(|_, tvl| tool_set.is_empty() || tool_set.contains(&tvl.backend));
        let outdated = ts.list_outdated_versions(self.bump);
        if outdated.is_empty() {
            info!("All tools are up to date");
        } else if self.json {
            self.display_json(outdated)?;
        } else {
            self.display(outdated)?;
        }

        Ok(())
    }

    fn display(&self, outdated: Vec<OutdatedInfo>) -> Result<()> {
        let mut table = tabled::Table::new(outdated);
        table::default_style(&mut table, self.no_header);
        miseprintln!("{table}");
        Ok(())
    }

    fn display_json(&self, outdated: Vec<OutdatedInfo>) -> Result<()> {
        let mut map = IndexMap::new();
        for o in outdated {
            map.insert(o.name.to_string(), o);
        }
        miseprintln!("{}", serde_json::to_string_pretty(&map)?);
        Ok(())
    }
}

static AFTER_LONG_HELP: &str = color_print::cstr!(
    r#"<bold><underline>Examples:</underline></bold>

    $ <bold>mise outdated</bold>
    Plugin  Requested  Current  Latest
    python  3.11       3.11.0   3.11.1
    node    20         20.0.0   20.1.0

    $ <bold>mise outdated node</bold>
    Plugin  Requested  Current  Latest
    node    20         20.0.0   20.1.0

    $ <bold>mise outdated --json</bold>
    {"python": {"requested": "3.11", "current": "3.11.0", "latest": "3.11.1"}, ...}
"#
);

#[cfg(test)]
mod tests {
    use crate::test::{change_installed_version, reset};

    #[test]
    fn test_outdated() {
        reset();
        assert_cli!("prune");
        assert_cli!("install");
        assert_cli_snapshot!("outdated");
    }

    #[test]
    fn test_outdated_with_runtimes() {
        reset();
        assert_cli_snapshot!("outdated", "tiny");
    }

    #[test]
    fn test_outdated_json() {
        reset();
        change_installed_version("tiny", "3.1.0", "3.0.0");
        assert_cli_snapshot!("outdated", "tiny", "--json");
        change_installed_version("tiny", "3.0.0", "3.1.0");
    }

    #[test]
    fn test_outdated_json_bump() {
        reset();
        assert_cli!("use", "tiny@2");
        assert_cli_snapshot!("outdated", "tiny", "--json", "--bump");
    }
}
