//! `aimx agents remove <agent>`: per-user inverse of `aimx agents setup`.
//!
//! Removes the plugin files under `$HOME` that `agents setup` laid
//! down and prints a per-agent cleanup hint pointing at any external
//! command the operator still needs to run (for example
//! `claude mcp remove aimx`).
//!
//! Refuses to run as root by default. The same `--dangerously-allow-root`
//! escape hatch as `aimx agents setup` exists for single-user
//! root-login VPS setups; on any machine with a regular user, prefer
//! `sudo -u <user> aimx agents remove <agent>`.

use crate::agents_cleanup::{self, plugin_removal_target};
use crate::agents_setup::{
    AgentEnv, AgentSpec, ROOT_REFUSAL_MESSAGE, RealAgentEnv, find_agent, home_dir_for_user,
    resolve_dest,
};
use std::io::{self, Write};
use std::path::PathBuf;

/// CLI options for one `aimx agents remove` invocation.
pub struct RunOpts {
    pub agent: String,
    pub dangerously_allow_root: bool,
}

/// Entry point called from `main.rs`.
pub fn run(opts: RunOpts) -> Result<(), Box<dyn std::error::Error>> {
    let env = RealAgentEnv;
    run_with_env(opts, &env, &mut io::stdout())?;
    Ok(())
}

/// Testable core of [`run`]. Removes plugin files unconditionally
/// (no per-agent file-vs-directory prompt), then prints the per-agent
/// cleanup hint via [`removal_hint`].
pub(crate) fn run_with_env(
    opts: RunOpts,
    env: &dyn AgentEnv,
    out: &mut dyn Write,
) -> Result<agents_cleanup::CleanupOutcome, Box<dyn std::error::Error>> {
    if env.is_root() && !opts.dangerously_allow_root {
        return Err(ROOT_REFUSAL_MESSAGE.into());
    }

    let spec = find_agent(&opts.agent).ok_or_else(|| {
        format!(
            "Unknown agent '{}'. Run `aimx agents list` to see supported agents.",
            opts.agent
        )
    })?;

    // Apply the same `--dangerously-allow-root` home override as
    // `agents_setup::run_with_env_to_writer` so the resolved dest path
    // points at /root's home, not the ambient $HOME.
    let result = if env.is_root() && opts.dangerously_allow_root {
        let root_home = home_dir_for_user("root").unwrap_or_else(|| PathBuf::from("/root"));
        let override_env = crate::agents_setup::OverrideHomeEnv::new(env, root_home);
        do_remove(spec, opts.dangerously_allow_root, &override_env, out)?
    } else {
        do_remove(spec, opts.dangerously_allow_root, env, out)?
    };

    Ok(result)
}

fn do_remove(
    spec: &AgentSpec,
    dangerously_allow_root: bool,
    env: &dyn AgentEnv,
    out: &mut dyn Write,
) -> Result<agents_cleanup::CleanupOutcome, Box<dyn std::error::Error>> {
    // Resolve the removal target up front so we can report whether the
    // wiring actually existed before the cleanup ran.
    let dest_root = resolve_dest(spec.dest_template, env)?;
    let (target_desc, removal_target) = plugin_removal_target(spec, &dest_root);
    let pre_existed = removal_target.exists();

    // Symmetry with `agents setup`: a user who installed under an older
    // aimx (which wrote `~/.claude/plugins/aimx/`) and then runs `agents
    // remove claude-code` on this aimx without first re-running setup
    // would otherwise be left with the orphaned legacy tree, since
    // `spec.dest_template` only points at `~/.claude/skills/aimx/`.
    if spec.name == "claude-code" {
        cleanup_legacy_claude_plugin(env, out)?;
    }

    // Delegate to the existing cleanup core in `--full --yes` mode.
    let cleanup_opts = agents_cleanup::RunOpts {
        agent: spec.name.to_string(),
        full: true,
        yes: true,
    };
    // The underlying cleanup core re-runs the root gate. With
    // `--dangerously-allow-root` set, env.is_root() is still true,
    // which would cause the per-user refusal there. Skip that
    // refusal by passing through a wrapper env that masks is_root.
    let outcome = if dangerously_allow_root {
        let masked = MaskRootEnv { inner: env };
        agents_cleanup::run_with_env(cleanup_opts, &masked, out)?
    } else {
        agents_cleanup::run_with_env(cleanup_opts, env, out)?
    };

    if !pre_existed {
        writeln!(
            out,
            "{} no plugin files were present at {} (already removed?)",
            crate::term::warn_mark(),
            crate::term::highlight(&target_desc),
        )?;
    }

    writeln!(out)?;
    writeln!(out, "{}", removal_hint(spec))?;

    Ok(outcome)
}

/// Mirrors `agents_setup::cleanup_legacy_claude_plugin`: removes the
/// legacy `~/.claude/plugins/aimx/` tree on the remove-side too, so a
/// user who installed under aimx <= 0.x and then runs `aimx agents
/// remove claude-code` on this aimx isn't left with the orphaned plugin
/// directory. The setup-side cleanup only fires on `agents setup`, so
/// without this the only way to get rid of the legacy tree was to
/// re-run `agents setup` first — which is the opposite of what someone
/// running `agents remove` wants.
fn cleanup_legacy_claude_plugin(env: &dyn AgentEnv, out: &mut dyn Write) -> io::Result<()> {
    let Some(home) = env.home_dir() else {
        return Ok(());
    };
    let legacy = home.join(".claude").join("plugins").join("aimx");
    if !legacy.exists() {
        return Ok(());
    }
    match std::fs::remove_dir_all(&legacy) {
        Ok(()) => {
            writeln!(
                out,
                "{} {} {}",
                crate::term::info("Removed"),
                crate::term::dim("legacy plugin install at"),
                crate::term::highlight(&legacy.to_string_lossy())
            )?;
        }
        Err(e) => {
            writeln!(
                out,
                "{} could not remove legacy plugin install at {}: {}",
                crate::term::warn_mark(),
                legacy.display(),
                e
            )?;
        }
    }
    Ok(())
}

/// Per-agent cleanup hint. Emitted at the end of
/// [`run_with_env`] so the operator knows which (if any) external
/// command they still need to run after `aimx` has wiped its own
/// footprint. Returned as a plain `String` so callers can print it
/// alongside whatever banner / mark they prefer.
pub fn removal_hint(spec: &AgentSpec) -> String {
    match spec.name {
        "claude-code" => "Run `claude mcp remove aimx` to also unregister the MCP server from Claude Code.".to_string(),
        "codex" => "Run `codex mcp remove aimx` to also unregister the MCP server from Codex CLI.".to_string(),
        "opencode" => {
            "Remove the `aimx` block from `~/.config/opencode/opencode.json` to also unregister the MCP server.".to_string()
        }
        "gemini" => {
            "Remove the `aimx` entry from `~/.gemini/settings.json` to also unregister the MCP server.".to_string()
        }
        "goose" => {
            "(Goose discovers recipes by filename; no further action needed.)".to_string()
        }
        "openclaw" => {
            "Run `openclaw mcp remove aimx` if your version supports it; otherwise edit the OpenClaw MCP config by hand.".to_string()
        }
        "hermes" => {
            "Remove the `aimx` block from `~/.hermes/config.yaml` under `mcp_servers:` to also unregister the MCP server.".to_string()
        }
        "nanoclaw" => {
            "Remove the `aimx` entry from `mcpServers` in `<fork>/.mcp.json` to also unregister the MCP server (set $NANOCLAW_HOME if your fork is not at ~/nanoclaw).".to_string()
        }
        // Future-proof: never panic on a registry-name mismatch.
        other => format!("(No agent-specific cleanup hint registered for `{other}`.)"),
    }
}

/// Wrapper that hides the underlying env's root status so the
/// `agents_cleanup` core's per-user refusal doesn't trip when we've
/// already accepted `--dangerously-allow-root` here.
struct MaskRootEnv<'a> {
    inner: &'a dyn AgentEnv,
}

impl<'a> AgentEnv for MaskRootEnv<'a> {
    fn home_dir(&self) -> Option<PathBuf> {
        self.inner.home_dir()
    }
    fn xdg_config_home(&self) -> Option<PathBuf> {
        self.inner.xdg_config_home()
    }
    fn is_root(&self) -> bool {
        false
    }
    fn is_stdin_tty(&self) -> bool {
        self.inner.is_stdin_tty()
    }
    fn read_line(&self) -> io::Result<String> {
        self.inner.read_line()
    }
    fn caller_username(&self) -> Option<String> {
        self.inner.caller_username()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::cell::RefCell;
    use tempfile::TempDir;

    struct TestEnv {
        home: PathBuf,
        username: String,
        is_root: bool,
        scripted_input: RefCell<Vec<String>>,
    }

    impl TestEnv {
        fn new(home: PathBuf, username: &str) -> Self {
            Self {
                home,
                username: username.to_string(),
                is_root: false,
                scripted_input: RefCell::new(Vec::new()),
            }
        }
    }

    impl AgentEnv for TestEnv {
        fn home_dir(&self) -> Option<PathBuf> {
            Some(self.home.clone())
        }
        fn xdg_config_home(&self) -> Option<PathBuf> {
            None
        }
        fn nanoclaw_home(&self) -> Option<PathBuf> {
            None
        }
        fn is_root(&self) -> bool {
            self.is_root
        }
        fn is_stdin_tty(&self) -> bool {
            false
        }
        fn read_line(&self) -> io::Result<String> {
            self.scripted_input
                .borrow_mut()
                .pop()
                .ok_or_else(|| io::Error::other("not used in remove tests"))
        }
        fn caller_username(&self) -> Option<String> {
            Some(self.username.clone())
        }
    }

    #[test]
    fn agents_remove_filesystem_cleanup() {
        // Pre-populate the dest dir; after running, dest must not exist.
        let tmp = TempDir::new().unwrap();
        let dest = tmp.path().join(".claude").join("skills").join("aimx");
        std::fs::create_dir_all(&dest).unwrap();
        std::fs::write(dest.join("SKILL.md"), "---\nname: aimx\n---\nbody").unwrap();
        assert!(dest.exists());

        let env = TestEnv::new(tmp.path().to_path_buf(), "alice");
        let opts = RunOpts {
            agent: "claude-code".to_string(),
            dangerously_allow_root: false,
        };
        let mut out = Vec::new();
        let outcome = run_with_env(opts, &env, &mut out).expect("remove must succeed");
        assert!(matches!(outcome, agents_cleanup::CleanupOutcome::Ok));
        assert!(!dest.exists(), "dest must be removed");
    }

    #[test]
    fn agents_remove_prints_agent_specific_hint() {
        let tmp = TempDir::new().unwrap();
        let env = TestEnv::new(tmp.path().to_path_buf(), "alice");
        let opts = RunOpts {
            agent: "claude-code".to_string(),
            dangerously_allow_root: false,
        };
        let mut out = Vec::new();
        run_with_env(opts, &env, &mut out).expect("remove must succeed");
        let stdout = String::from_utf8(out).unwrap();
        assert!(
            stdout.contains("claude mcp remove aimx"),
            "expected claude-code cleanup hint, got: {stdout}"
        );
    }

    #[test]
    fn agents_remove_claude_code_removes_legacy_plugins_dir() {
        // Mirrors `install_claude_code_removes_legacy_plugins_dir` in
        // `agents_setup`: a user who installed under aimx <= 0.x (which
        // wrote `~/.claude/plugins/aimx/`) and then runs `aimx agents
        // remove claude-code` on this aimx must not be left with the
        // orphaned legacy tree. The notice is printed; the directory is
        // gone after remove completes.
        let tmp = TempDir::new().unwrap();
        let env = TestEnv::new(tmp.path().to_path_buf(), "alice");

        let legacy = tmp.path().join(".claude").join("plugins").join("aimx");
        std::fs::create_dir_all(legacy.join("stale")).unwrap();
        std::fs::write(legacy.join("stale").join("x.txt"), "old").unwrap();
        assert!(legacy.exists());

        let opts = RunOpts {
            agent: "claude-code".to_string(),
            dangerously_allow_root: false,
        };
        let mut out = Vec::new();
        run_with_env(opts, &env, &mut out).expect("remove must succeed");

        assert!(!legacy.exists(), "legacy plugin dir must be removed");

        let printed = String::from_utf8(out).unwrap();
        assert!(
            printed.contains("legacy plugin install"),
            "stdout must mention legacy cleanup: {printed}"
        );
    }

    #[test]
    fn agents_remove_non_claude_code_does_not_touch_legacy_plugins_dir() {
        // The legacy cleanup is `claude-code`-specific; running
        // `agents remove codex` against a host that happens to have a
        // dormant `~/.claude/plugins/aimx/` from another tool must not
        // delete it.
        let tmp = TempDir::new().unwrap();
        let env = TestEnv::new(tmp.path().to_path_buf(), "alice");

        let legacy = tmp.path().join(".claude").join("plugins").join("aimx");
        std::fs::create_dir_all(&legacy).unwrap();
        std::fs::write(legacy.join("marker"), "still here").unwrap();

        let opts = RunOpts {
            agent: "codex".to_string(),
            dangerously_allow_root: false,
        };
        let mut out = Vec::new();
        run_with_env(opts, &env, &mut out).expect("remove must succeed");

        assert!(
            legacy.exists(),
            "non-claude-code remove must leave the legacy dir alone"
        );
        assert!(legacy.join("marker").exists());
    }

    #[test]
    fn agents_remove_unknown_agent_errors_clearly() {
        let tmp = TempDir::new().unwrap();
        let env = TestEnv::new(tmp.path().to_path_buf(), "alice");
        let opts = RunOpts {
            agent: "nonesuch".to_string(),
            dangerously_allow_root: false,
        };
        let mut out = Vec::new();
        let err = run_with_env(opts, &env, &mut out).unwrap_err();
        let msg = err.to_string();
        assert!(
            msg.contains("Unknown agent"),
            "expected 'Unknown agent' in error, got: {msg}"
        );
        assert!(
            msg.contains("nonesuch"),
            "expected the bad agent name in the error, got: {msg}"
        );
    }

    #[test]
    fn agents_remove_refuses_root() {
        let tmp = TempDir::new().unwrap();
        let mut env = TestEnv::new(tmp.path().to_path_buf(), "alice");
        env.is_root = true;
        let opts = RunOpts {
            agent: "claude-code".to_string(),
            dangerously_allow_root: false,
        };
        let mut out = Vec::new();
        let err = run_with_env(opts, &env, &mut out).unwrap_err();
        assert!(
            err.to_string().contains("refuses to run as root"),
            "expected root-refusal, got: {err}"
        );
    }

    #[test]
    fn removal_hint_covers_every_registered_agent() {
        // Every registered agent must have an explicit hint (no
        // `(No agent-specific cleanup hint ...)` fallback for known
        // agents). Walk the registry; the fallback path uses
        // parentheses + the literal "No agent-specific" prefix.
        for spec in crate::agents_setup::registry() {
            let hint = removal_hint(spec);
            assert!(
                !hint.starts_with("(No agent-specific"),
                "agent '{}' must have a removal hint",
                spec.name
            );
            assert!(
                !hint.is_empty(),
                "agent '{}' removal hint must not be empty",
                spec.name
            );
        }
    }
}
