use std::fmt;
use std::fs;
use std::path::Path;

use anyhow::{bail, Context, Result};
use inquire::{Confirm, MultiSelect, Select};

use crate::config::Config;
use crate::project;
use crate::registry::{packages, tools};
use crate::state::{MachineState, ProjectIntent};

use super::{exec, yes};

/// `aimgr install [name]`.
///   - no name → interactive picker: MultiSelect over tools not yet installed.
///     If every known tool is already installed, offer a reinstall picker that
///     force-reruns the install method for the chosen tools.
///   - `--all` → install every tool declared in `~/.config/aimgr/config.toml`
///     (prompts for scope in a project).
///   - name resolves as a tool → run installer + record in machine state and intent.
///   - name resolves as a package → bail with a hint to `aimgr use`.
///   - name unknown → scaffold `~/.config/aimgr/custom/tools/<name>.toml` and `$EDITOR`.
pub fn run(name: Option<&str>, all: bool) -> Result<()> {
    let mut machine = MachineState::load()?;
    let mut cfg = Config::load()?;

    match (name, all) {
        (Some(n), false) => install_one(n, &mut machine, &mut cfg, false)?,
        (Some(_), true) => bail!("`--all` can't be combined with a name"),
        (None, true) => install_all(&mut machine, &mut cfg)?,
        (None, false) => pick_flow(&mut machine, &mut cfg)?,
    }

    machine.save()?;
    cfg.save()?;
    Ok(())
}

/// `install --all`: install every tool declared in `cfg.install.tools`. Inside
/// a managed project, ask whether to also record each tool in the project's
/// intent.
fn install_all(machine: &mut MachineState, cfg: &mut Config) -> Result<()> {
    let declared = cfg.install.tools.clone();
    if declared.is_empty() {
        println!(
            "no tools declared in {}",
            crate::config::config_file()?.display()
        );
        return Ok(());
    }

    let cwd = std::env::current_dir().context("getting current directory")?;
    let project_root = project::discover_root(&cwd);

    let also_project = match &project_root {
        Some(_) if yes::enabled() => false,
        Some(_) => matches!(
            Select::new(
                "Managed folder detected. Scope for install?",
                vec!["global only", "also record in this project"],
            )
            .prompt()?,
            "also record in this project"
        ),
        None => false,
    };

    for name in &declared {
        let Some(resolved) = tools::resolve(name)? else {
            eprintln!(
                "  {} `{name}` not in registry — skipping",
                crate::ui::style::warn_mark()
            );
            continue;
        };
        print_step(name);
        match exec::ensure_tool_installed(&resolved.tool, machine, cfg, None, false) {
            Ok(()) => print_ok(name, Status::Installed),
            Err(e) => {
                print_fail(name, &e.to_string());
                return Err(e);
            }
        }
        cfg.add_tool(&resolved.tool.name);
        if also_project {
            if let (Some(root), Some(variant)) = (&project_root, resolved.tool.runtime) {
                add_tool_to_intent(root, variant)?;
            }
        }
    }

    Ok(())
}

fn add_tool_to_intent(root: &Path, tool: crate::tools::Tool) -> Result<()> {
    let mut intent = ProjectIntent::load(root)?;
    if !intent.tools.contains(&tool) {
        intent.tools.push(tool);
        intent.save(root)?;
    }
    Ok(())
}

fn install_one(
    name: &str,
    machine: &mut MachineState,
    cfg: &mut Config,
    force: bool,
) -> Result<()> {
    if let Some(resolved) = tools::resolve(name)? {
        let was_recorded = machine.tools.contains_key(name);
        let already_present = was_recorded || exec::probe_present(&resolved.tool) == Some(true);
        let effective_force = if force {
            true
        } else if already_present {
            if yes::enabled() {
                print_ok(name, Status::AlreadyPresent);
                note_global(name, cfg.add_tool(&resolved.tool.name));
                return Ok(());
            }
            let reinstall = Confirm::new(&format!("`{name}` is already installed. Reinstall?"))
                .with_default(false)
                .prompt()?;
            if !reinstall {
                print_ok(name, Status::AlreadyPresent);
                note_global(name, cfg.add_tool(&resolved.tool.name));
                return Ok(());
            }
            true
        } else {
            false
        };
        print_step(name);
        match exec::ensure_tool_installed(&resolved.tool, machine, cfg, None, effective_force) {
            Ok(()) => {
                let status = describe(was_recorded, effective_force, machine.tools.get(name));
                print_ok(name, status);
            }
            Err(e) => {
                print_fail(name, &e.to_string());
                return Err(e);
            }
        }
        note_global(name, cfg.add_tool(&resolved.tool.name));
        return Ok(());
    }
    if packages::resolve(name)?.is_some() {
        bail!("`{name}` is a package, not a tool — try `aimgr use {name}`");
    }
    scaffold_custom_tool(name)
}

fn pick_flow(machine: &mut MachineState, cfg: &mut Config) -> Result<()> {
    let all: Vec<tools::Resolved> = tools::list()?;
    if all.is_empty() {
        println!("no tools in the registry");
        return Ok(());
    }

    let remaining: Vec<ToolChoice> = all
        .iter()
        .filter(|r| !machine.tools.contains_key(&r.tool.name))
        .cloned()
        .map(ToolChoice)
        .collect();

    if remaining.is_empty() {
        println!("Every known tool is already installed.");
        let reinstall = Confirm::new("Reinstall one?")
            .with_default(false)
            .prompt()?;
        if !reinstall {
            return Ok(());
        }
        let choices: Vec<ToolChoice> = all.into_iter().map(ToolChoice).collect();
        let picked = MultiSelect::new("Pick tools to reinstall:", choices)
            .with_vim_mode(true)
            .prompt()?;
        for ToolChoice(r) in picked {
            install_one(&r.tool.name, machine, cfg, true)?;
        }
        return Ok(());
    }

    let picked = MultiSelect::new("Pick tools to install:", remaining)
        .with_vim_mode(true)
        .prompt()?;
    for ToolChoice(r) in picked {
        install_one(&r.tool.name, machine, cfg, false)?;
    }
    Ok(())
}

#[derive(Clone)]
struct ToolChoice(tools::Resolved);

impl fmt::Display for ToolChoice {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let desc = self.0.tool.description.as_deref().unwrap_or("");
        if desc.is_empty() {
            f.write_str(&self.0.tool.name)
        } else {
            write!(f, "{}  {}", self.0.tool.name, desc)
        }
    }
}

#[derive(Copy, Clone, Eq, PartialEq)]
enum Status {
    Installed,
    Reinstalled,
    AlreadyPresent,
    UpToDate,
}

impl Status {
    fn label(self) -> &'static str {
        match self {
            Status::Installed => "installed",
            Status::Reinstalled => "reinstalled",
            Status::AlreadyPresent => "already present",
            Status::UpToDate => "up to date",
        }
    }
}

fn describe(
    was_recorded: bool,
    force: bool,
    after: Option<&crate::state::MachineInstall>,
) -> Status {
    if force {
        return Status::Reinstalled;
    }
    if was_recorded {
        return Status::UpToDate;
    }
    match after.map(|m| m.method.as_str()) {
        Some("preexisting") => Status::AlreadyPresent,
        _ => Status::Installed,
    }
}

fn print_step(name: &str) {
    println!("  {} {name}", crate::ui::style::step_mark());
}

fn print_ok(name: &str, status: Status) {
    println!(
        "    {} {name} {}",
        crate::ui::style::ok_mark(),
        status.label()
    );
}

fn print_fail(name: &str, err: &str) {
    eprintln!("    {} {name} failed: {err}", crate::ui::style::fail_mark());
}

fn note_global(name: &str, newly_added: bool) {
    let mark = crate::ui::style::ok_mark();
    if newly_added {
        println!("    {mark} {name} added to global config");
    } else {
        println!("    {mark} {name} already in global config");
    }
}

fn scaffold_custom_tool(name: &str) -> Result<()> {
    let path = tools::custom::path_for(name)?;
    if path.exists() {
        bail!("custom tool already exists at {}", path.display());
    }
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).with_context(|| format!("creating {}", parent.display()))?;
    }
    let tmpl = format!(
        r#"name        = "{name}"
description = ""
# runtime   = "claude"   # bind to a known Tool enum variant when applicable

[[artifacts]]
kind = "cli"
name = "{name}"

[[install]]
id      = "manual"
label   = "fill in"
command = "echo TODO: replace with real install command"
# interactive = true   # set if the installer needs a TTY (wizard, prompts)

# update    = "{name} update"
# uninstall = ""
"#
    );
    fs::write(&path, tmpl).with_context(|| format!("writing {}", path.display()))?;
    println!("wrote {}", path.display());
    launch_editor(&path)
}

pub fn launch_editor(path: &Path) -> Result<()> {
    let editor = std::env::var("EDITOR")
        .or_else(|_| std::env::var("VISUAL"))
        .unwrap_or_else(|_| "vi".into());
    let status = std::process::Command::new(&editor)
        .arg(path)
        .status()
        .with_context(|| format!("spawning {editor}"))?;
    if !status.success() {
        bail!("{editor} exited with {status}");
    }
    Ok(())
}
