use std::{env, fs, io, str::FromStr as _, sync::Arc};

use camino::{FromPathBufError, Utf8PathBuf};
use clean_path::Clean as _;
use crossterm::style::Stylize as _;
use duct::cmd;
use inquire::{Select, Text};
use jp_config::{
    PartialAppConfig,
    conversation::tool::RunMode,
    fs::{ConfigLoader, Format, user_global_config_dir},
    model::id::{ModelIdConfig, Name, ProviderId},
};
use jp_printer::Printer;
use jp_storage::backend::FsStorageBackend;
use jp_workspace::Workspace;
use schematic::ConfigEnum as _;

use crate::{DEFAULT_STORAGE_DIR, cmd::Output, ctx::IntoPartialAppConfig};

#[derive(Debug, clap::Args)]
pub(crate) struct Init {
    /// Path to initialize the workspace at.
    /// Defaults to the current directory.
    path: Option<Utf8PathBuf>,
}

impl Init {
    pub(crate) fn run(&self, printer: &Printer) -> Output {
        let cwd: Utf8PathBuf = std::env::current_dir()?
            .try_into()
            .map_err(FromPathBufError::into_io_error)?;

        let mut root: Utf8PathBuf = self
            .path
            .clone()
            .unwrap_or_else(|| Utf8PathBuf::from("."))
            .into_std_path_buf()
            .clean()
            .try_into()
            .map_err(FromPathBufError::into_io_error)?;

        if !root.has_root() {
            root = cwd.join(root);
        }

        fs::create_dir_all(&root)?;

        let storage = root.join(DEFAULT_STORAGE_DIR);
        let id = jp_workspace::Id::new();

        let fs = Arc::new(FsStorageBackend::new(&storage)?);
        let _workspace = Workspace::new_with_id(root.clone(), id.clone()).with_backend(fs);

        id.store(&storage)?;

        // Interactive configuration
        let run_mode = Self::ask_run_mode(&mut printer.out_writer(), true)?;
        let (provider, name) = Self::ask_model(&mut printer.out_writer())?;

        // Ask for the user's display name (skipped if already configured
        // in user-global config). Writes to the user-global config so the
        // setting is shared across all workspaces.
        Self::maybe_ask_and_persist_user_name(&mut printer.out_writer())?;

        // Write workspace config
        fs::write(
            storage.join("config.toml"),
            indoc::formatdoc!(
                r#"
                [assistant.model.id]
                provider = "{provider}"
                name = "{name}"

                [conversation.tools.'*']
                run = "{run_mode}"
            "#
            ),
        )?;

        let loc = if root == cwd {
            "current directory".to_owned()
        } else {
            root.to_string().bold().to_string()
        };

        printer.println(format!("Initialized workspace at {loc}"));
        Ok(())
    }

    /// Ask for and persist `user.name` to user-global config, unless it is
    /// already set there.
    ///
    /// Skipped silently when no user-global config directory can be determined
    /// (e.g.
    /// `$HOME` is unset).
    /// Empty input also skips, leaving transcripts to fall back to the generic
    /// `user` label.
    fn maybe_ask_and_persist_user_name(
        writer: &mut dyn io::Write,
    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
        let home = env::home_dir().and_then(|p| Utf8PathBuf::try_from(p).ok());
        let Some(global_dir) = user_global_config_dir(home.as_deref()) else {
            return Ok(());
        };

        let loader = ConfigLoader {
            file_stem: "config".into(),
            create_if_missing: Some(Format::Toml),
            ..Default::default()
        };
        let mut config_file = loader.load(&global_dir)?;

        // If the user has already set their name in user-global config,
        // don't pester them again.
        if let Ok(existing) = toml::from_str::<PartialAppConfig>(&config_file.content)
            && existing.user.name.as_deref().is_some_and(|s| !s.is_empty())
        {
            return Ok(());
        }

        let Some(name) = Self::ask_user_name(writer)? else {
            return Ok(());
        };

        let mut partial = PartialAppConfig::empty();
        partial.user.name = Some(name);
        config_file.merge_delta(&partial)?;

        if let Some(parent) = config_file.path.parent() {
            fs::create_dir_all(parent)?;
        }
        fs::write(&config_file.path, &config_file.content)?;

        Ok(())
    }

    /// Prompt the user for their display name, pre-filled with the best
    /// available default (see [`detect_default_user_name`]).
    /// Returns `None` when the user submits an empty value.
    ///
    /// The default is set as an *initial value* (editable), not as an `inquire`
    /// default (substituted on empty submission), so the user can clear the
    /// field and submit empty to skip attribution.
    fn ask_user_name(
        writer: &mut dyn io::Write,
    ) -> Result<Option<String>, Box<dyn std::error::Error + Send + Sync>> {
        let prompt = Text::new("Your name for conversations:").with_help_message(
            "Stamped onto each message you send. Press Enter to accept the pre-filled value, or \
             clear the field for the generic 'user' label.",
        );

        let initial = detect_default_user_name();
        let answer = match initial.as_deref() {
            Some(v) => prompt.with_initial_value(v).prompt_with_writer(writer)?,
            None => prompt.prompt_with_writer(writer)?,
        };

        let trimmed = answer.trim();
        Ok((!trimmed.is_empty()).then(|| trimmed.to_owned()))
    }

    fn ask_run_mode(
        writer: &mut dyn io::Write,
        help: bool,
    ) -> Result<RunMode, Box<dyn std::error::Error + Send + Sync>> {
        let mut options = vec![
            format!("Yes {}", "(safest option)".green()),
            format!("No  {}", "(potentially dangerous)".red()),
        ];

        if help {
            options.push("Help…".to_owned());
        }

        let answer = Select::new("Confirm before running tools?", options)
            .with_help_message(
                "You can always configure individual tools you deem safe to run without \
                 confirmation.",
            )
            .with_starting_cursor(0)
            .prompt_with_writer(writer)?;

        if answer == "Help…" {
            let _err = indoc::writedoc!(
                writer,
                r"

                    # Recommended Configuration

                    Yes (confirm before running tools)

                    # Summary

                    The assistant runs tools on your local machine, these
                    can perform destructive actions and should therefore
                    be run with a human-in-the-loop confirmation.

                    # Details

                    When using JP, the assistant needs to run tools on
                    your local machine to perform certain tasks such as
                    modifying files, running CLI tools, etc.

                    Most of these tools are safe to run, but some can
                    be potentially dangerous, depending on the
                    arguments provided to them.

                    While all of JP's built-in tools are confined to the
                    workspace root, externally supplied tools cannot be
                    restricted in the same way, and can potentially run
                    any command on your system.

                    For example, a potentially external tool `rm` could
                    take an argument `file`, which could be an absolute
                    path to a file outside of your workspace root,
                    deleting files from your system that you don't want
                    to delete.

                    To avoid this, you should configure the assistant to
                    run these tools with a human-in-the-loop confirmation.
                    This will ensure that the assistant only runs tools
                    that you explicitly allow it to run.

                    You can also configure the assistant to run tools
                    automatically, which means it will run tools without
                    asking you first.

                    The answer to this question will be used as the default
                    for all tools that are run by the assistant, but each
                    tool can also be configured to run with a different
                    mode, by editing your config file after the workspace
                    is initialized.

                "
            );
            writer.flush()?;

            return Self::ask_run_mode(writer, false);
        }

        Ok(if answer.starts_with("Yes") {
            RunMode::Unattended
        } else {
            RunMode::Ask
        })
    }

    fn ask_model(
        writer: &mut dyn io::Write,
    ) -> Result<(ProviderId, Name), Box<dyn std::error::Error + Send + Sync>> {
        let models = Self::detect_models();

        let mut options: Vec<String> = models.iter().map(ToString::to_string).collect();
        options.push("Other (enter manually)".to_string());

        let ans = Select::new("Select an AI model to use:", options.clone())
            .with_help_message("We detected these models based on your environment.")
            .prompt_with_writer(writer)?;

        if ans == "Other (enter manually)" {
            let providers = ProviderId::variants();
            let provider_strs: Vec<String> = providers.iter().map(ToString::to_string).collect();

            let provider_str =
                Select::new("Select a provider:", provider_strs).prompt_with_writer(writer)?;

            let provider =
                ProviderId::from_str(&provider_str).map_err(|e| io::Error::other(e.to_string()))?;

            let name = Text::new("Enter the model name:")
                .with_placeholder("e.g. gpt-4o")
                .prompt_with_writer(writer)?;

            Ok((provider, Name(name)))
        } else {
            let m = models.iter().find(|m| m.to_string() == ans).unwrap();
            Ok((m.provider, m.name.clone()))
        }
    }

    fn detect_models() -> Vec<ModelIdConfig> {
        let mut models = Vec::new();

        if has_anthropic()
            && let Some(m) = default_model_id_for(ProviderId::Anthropic)
        {
            models.push(m);
        }
        if has_openai()
            && let Some(m) = default_model_id_for(ProviderId::Openai)
        {
            models.push(m);
        }
        if has_google()
            && let Some(m) = default_model_id_for(ProviderId::Google)
        {
            models.push(m);
        }

        if let Ok(output) = cmd!("ollama", "list").read() {
            for line in output.lines().skip(1) {
                let Some(name) = line.split_whitespace().next() else {
                    continue;
                };

                if name.is_empty() {
                    continue;
                }

                let name = name.split(':').next().unwrap_or(name);
                models.push(ModelIdConfig {
                    provider: ProviderId::Ollama,
                    name: Name(name.to_owned()),
                });
            }
        }

        models.sort();
        models.dedup();
        models
    }
}

/// Best-effort detection of a sensible default display name.
///
/// Cascades through:
///
/// 1. `git config --global --get user.name` — the user's stable global
///    identity.
///    The picked-up value is persisted to user-global JP config and inherited
///    by every future workspace, so we deliberately avoid the unscoped `git
///    config --get user.name`: it would resolve against whichever repo happens
///    to be in cwd (or in the init root), leaking a repo-local identity the
///    user explicitly chose not to make global into every future JP workspace.
/// 2. `$USER` (Unix) or `$USERNAME` (Windows) — the system login name,
///    last-resort fallback.
///
/// Returns `None` when nothing is available so the prompt renders without a
/// pre-filled value.
fn detect_default_user_name() -> Option<String> {
    if let Ok(out) = cmd!("git", "config", "--global", "--get", "user.name")
        .stderr_null()
        .read()
    {
        let trimmed = out.trim();
        if !trimmed.is_empty() {
            return Some(trimmed.to_owned());
        }
    }

    // System login name ($USER on Unix, $USERNAME on Windows).
    env::var("USER")
        .or_else(|_| env::var("USERNAME"))
        .ok()
        .map(|s| s.trim().to_owned())
        .filter(|s| !s.is_empty())
}

fn has_anthropic() -> bool {
    env::var("ANTHROPIC_API_KEY").is_ok()
}

fn has_openai() -> bool {
    env::var("OPENAI_API_KEY").is_ok()
}

fn has_google() -> bool {
    env::var("GOOGLE_API_KEY").is_ok()
}

fn default_model_id_for(provider: ProviderId) -> Option<ModelIdConfig> {
    let name = match provider {
        ProviderId::Anthropic => Name("claude-sonnet-4-6".into()),
        ProviderId::Google => Name("gemini-3.1-pro-preview".into()),
        ProviderId::Openai => Name("gpt-5.2".into()),
        _ => return None,
    };

    Some(ModelIdConfig { provider, name })
}

impl IntoPartialAppConfig for Init {
    fn apply_cli_config(
        &self,
        _workspace: Option<&Workspace>,
        partial: PartialAppConfig,
        _: Option<&PartialAppConfig>,
    ) -> std::result::Result<PartialAppConfig, Box<dyn std::error::Error + Send + Sync>> {
        Ok(partial)
    }
}

#[cfg(test)]
#[path = "init_tests.rs"]
mod tests;
