use eyre::Result;
use std::{collections::BTreeMap, sync::Arc};

use crate::cli::args::ToolArg;
use crate::config::Config;
use crate::env_diff::EnvMap;
use crate::shell::{ShellType, get_shell};
use crate::toolset::{InstallOptions, Toolset, ToolsetBuilder};
use crate::wildcard::wildcard_match;
use indexmap::IndexSet;

/// Exports env vars to activate mise a single time
///
/// Use this if you don't want to permanently install mise. It's not necessary to
/// use this if you have `mise activate` in your shell rc file.
#[derive(Debug, clap::Args)]
#[clap(visible_alias = "e", verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)]
pub struct Env {
    /// Tool(s) to use
    #[clap(value_name = "TOOL@VERSION")]
    tool: Vec<ToolArg>,

    /// Output in dotenv format
    #[clap(long, short = 'D', overrides_with = "shell")]
    dotenv: bool,

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

    /// Shell type to generate environment variables for
    #[clap(long, short, overrides_with = "json")]
    shell: Option<ShellType>,

    /// Output in JSON format with additional information (source, tool)
    #[clap(long, overrides_with = "shell")]
    json_extended: bool,

    /// Only show redacted environment variables
    #[clap(long)]
    redacted: bool,

    /// Only show values of environment variables
    #[clap(long)]
    values: bool,
}

impl Env {
    pub async fn run(self) -> Result<()> {
        let mut config = Config::get().await?;
        let mut ts = ToolsetBuilder::new()
            .with_args(&self.tool)
            .build(&config)
            .await?;
        let (_, missing) = ts
            .install_missing_versions(&mut config, &InstallOptions::default())
            .await?;
        ts.notify_missing_versions(missing);

        // Pre-compute final_env when needed by --redacted or --dotenv to
        // avoid calling it twice. final_env includes the tools-only pass
        // whose redactions are not in config.env_results().
        let final_env = if self.redacted || self.dotenv {
            Some(ts.final_env(&config).await?)
        } else {
            None
        };

        let redacted_keys = if self.redacted {
            let env_results = config.env_results().await?;
            let mut keys = IndexSet::new();
            keys.extend(env_results.redactions.clone());
            if let Some((_, ref tools_env_results)) = final_env {
                keys.extend(tools_env_results.redactions.clone());
            }
            keys.extend(config.redaction_keys());
            Some(keys)
        } else {
            None
        };

        if self.json {
            self.output_json(&config, ts, &redacted_keys).await
        } else if self.json_extended {
            self.output_extended_json(&config, ts, &redacted_keys).await
        } else if self.dotenv {
            self.output_dotenv(final_env.unwrap().0, &redacted_keys)
        } else if self.values {
            self.output_values(&config, ts, &redacted_keys).await
        } else {
            self.output_shell(&config, ts, &redacted_keys).await
        }
    }

    async fn output_json(
        &self,
        config: &Arc<Config>,
        ts: Toolset,
        redacted_keys: &Option<IndexSet<String>>,
    ) -> Result<()> {
        let mut env = ts.env_with_path(config).await?;

        if let Some(keys) = redacted_keys {
            env.retain(|k, _| self.should_include_key(k, keys));
        }

        miseprintln!("{}", serde_json::to_string_pretty(&env)?);
        Ok(())
    }

    async fn output_extended_json(
        &self,
        config: &Arc<Config>,
        ts: Toolset,
        redacted_keys: &Option<IndexSet<String>>,
    ) -> Result<()> {
        let mut res = BTreeMap::new();

        ts.env_with_path(config).await?.iter().for_each(|(k, v)| {
            res.insert(k.to_string(), BTreeMap::from([("value", v.to_string())]));
        });

        config.env_with_sources().await?.iter().for_each(|(k, v)| {
            res.insert(
                k.to_string(),
                BTreeMap::from([
                    ("value", v.0.to_string()),
                    ("source", v.1.to_string_lossy().into_owned()),
                ]),
            );
        });

        let tool_map: BTreeMap<String, String> = ts
            .list_all_versions(config)
            .await?
            .into_iter()
            .map(|(b, tv)| {
                (
                    b.id().into(),
                    tv.request
                        .source()
                        .path()
                        .map(|p| p.to_string_lossy().into_owned())
                        .unwrap_or_else(|| "".to_string()),
                )
            })
            .collect();

        ts.env_from_tools(config)
            .await
            .iter()
            .for_each(|(name, value, tool_id)| {
                res.insert(
                    name.to_string(),
                    BTreeMap::from([
                        ("value", value.to_string()),
                        ("tool", tool_id.to_string()),
                        (
                            "source",
                            tool_map
                                .get(tool_id)
                                .cloned()
                                .unwrap_or_else(|| "unknown_source".to_string()),
                        ),
                    ]),
                );
            });

        if let Some(keys) = redacted_keys {
            res.retain(|k, _| self.should_include_key(k, keys));
        }

        miseprintln!("{}", serde_json::to_string_pretty(&res)?);
        Ok(())
    }

    async fn output_shell(
        &self,
        config: &Arc<Config>,
        ts: Toolset,
        redacted_keys: &Option<IndexSet<String>>,
    ) -> Result<()> {
        let default_shell = get_shell(Some(fallback_shell())).unwrap();
        let shell = get_shell(self.shell).unwrap_or(default_shell);
        let mut env = ts.env_with_path(config).await?;

        if let Some(keys) = redacted_keys {
            env.retain(|k, _| self.should_include_key(k, keys));
        }

        for (k, v) in env {
            let k = k.to_string();
            let v = v.to_string();
            miseprint!("{}", shell.set_env(&k, &v))?;
        }
        Ok(())
    }

    fn output_dotenv(
        &self,
        mut env: EnvMap,
        redacted_keys: &Option<IndexSet<String>>,
    ) -> Result<()> {
        if let Some(keys) = redacted_keys {
            env.retain(|k, _| self.should_include_key(k, keys));
        }

        for (k, v) in env {
            let k = k.to_string();
            let v = v.to_string();
            miseprint!("{}={}\n", k, v)?;
        }
        Ok(())
    }

    async fn output_values(
        &self,
        config: &Arc<Config>,
        ts: Toolset,
        redacted_keys: &Option<IndexSet<String>>,
    ) -> Result<()> {
        let mut env = ts.env_with_path(config).await?;

        if let Some(keys) = redacted_keys {
            env.retain(|k, _| self.should_include_key(k, keys));
        }

        for (_, v) in env {
            miseprintln!("{}", v);
        }
        Ok(())
    }

    fn should_include_key(&self, key: &str, redacted_keys: &IndexSet<String>) -> bool {
        redacted_keys
            .iter()
            .any(|pattern| wildcard_match(key, pattern))
    }
}

/// The shell to emit for when there is nothing to detect from.
///
/// Detection is `MISE_SHELL`, then `SHELL`. On unix that always resolves — an unset `SHELL` falls
/// back to `sh` — so this is reached only on Windows, where PowerShell and cmd set neither and the
/// value mise would otherwise read, `COMSPEC`, names cmd.exe. mise has no cmd implementation, so
/// bash was being printed into a PowerShell session:
///
/// ```console
/// PS> mise env
/// export MY_PATH='C:\Users\me'
/// ```
///
/// pwsh is the only shell mise can emit for that a Windows user is likely to be in. It is not
/// right for a cmd user, but neither was bash, and there is no third answer available.
fn fallback_shell() -> ShellType {
    match cfg!(windows) {
        true => ShellType::Pwsh,
        false => ShellType::Bash,
    }
}

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

    $ <bold>eval "$(mise env -s bash)"</bold>
    $ <bold>eval "$(mise env -s zsh)"</bold>
    $ <bold>mise env -s fish | source</bold>
    $ <bold>execx($(mise env -s xonsh))</bold>
"#
);

#[cfg(test)]
mod tests {
    use super::*;

    /// Asserted on the output rather than on the enum, which would only restate the `cfg!` above.
    /// What matters is that the syntax is one the platform's shell can run.
    #[test]
    fn the_fallback_emits_syntax_this_platform_can_run() {
        let line = fallback_shell().as_shell().set_env("K", "V");

        if cfg!(windows) {
            // `export K=V` is what a PowerShell session used to be handed.
            assert!(!line.starts_with("export "), "{line}");
            assert!(line.contains("${Env:K}"), "{line}");
        } else {
            assert!(line.starts_with("export "), "{line}");
        }
    }
}
