//! # Module: commands
//!
//! ## Spec
//! - Enumerates all commands available in the agent-doc ecosystem: agent-doc CLI subcommands,
//!   Claude Code built-in slash commands, installed skill commands, and installed plugin commands.
//! - Each entry carries a `name` (slash-prefixed), `args` (synopsis), and `description`.
//! - `run()` serializes the full list to pretty-printed JSON on stdout; consumed by editor plugins
//!   for autocomplete popups.
//! - Static base list (agent-doc CLI + Claude Code built-ins) is extended with dynamically
//!   discovered plugin commands from `~/.claude/plugins/marketplaces/*/plugins/*/commands/*.md`.
//!
//! ## Plugin Command Discovery
//! - Scans `~/.claude/plugins/marketplaces/<MARKETPLACE>/plugins/<NAMESPACE>/commands/*.md`
//! - Each `.md` file may have YAML frontmatter with `description:` and `argument-hint:` fields
//! - Command name: `/<NAMESPACE>:<stem>` (e.g. `/codex:setup`, `/codex:rescue`)
//! - Files with empty descriptions are skipped
//!
//! ## Agentic Contracts
//! - `run() -> anyhow::Result<()>` — always succeeds unless JSON serialization panics (it won't);
//!   prints to stdout and returns `Ok(())`.
//! - Every `CommandInfo` entry must have a non-empty `name` starting with `/` and a non-empty
//!   `description`; enforced by tests.
//! - Total command count is ≥ 50 (test guard against accidental truncation).
//!
//! ## Evals
//! - nonempty_list: `all_commands()` → at least 50 entries
//! - names_start_with_slash: every entry name starts with `/` and has non-empty description
//! - json_output: serialized JSON contains `"/agent-doc"`, `"/help"`, `"/existence"`

#![allow(clippy::vec_init_then_push)]

use serde::Serialize;

#[derive(Serialize)]
pub struct CommandInfo {
    pub name: String,
    pub args: String,
    pub description: String,
}

/// Return all known commands: agent-doc CLI + Claude Code built-in + installed skills + plugins.
pub fn run() -> anyhow::Result<()> {
    let commands = all_commands();
    println!("{}", serde_json::to_string_pretty(&commands)?);
    Ok(())
}

fn all_commands() -> Vec<CommandInfo> {
    let mut cmds = Vec::new();

    // --- agent-doc CLI commands (exposed as /agent-doc <subcommand>) ---
    cmds.push(cmd(
        "/agent-doc",
        "<FILE>",
        "Submit document session (diff, respond, write back)",
    ));
    cmds.push(cmd(
        "/agent-doc claim",
        "<FILE>",
        "Claim file for current tmux pane",
    ));
    cmds.push(cmd(
        "/agent-doc run",
        "<FILE>",
        "Run session workflow with agent backend",
    ));
    cmds.push(cmd(
        "/agent-doc init",
        "<FILE>",
        "Scaffold a new session document",
    ));
    cmds.push(cmd(
        "/agent-doc diff",
        "<FILE>",
        "Preview the diff that would be sent",
    ));
    cmds.push(cmd(
        "/agent-doc reset",
        "<FILE>",
        "Clear session ID and delete snapshot",
    ));
    cmds.push(cmd(
        "/agent-doc clean",
        "<FILE>",
        "Squash session git history",
    ));
    cmds.push(cmd(
        "/agent-doc start",
        "<FILE>",
        "Start Claude in a tmux pane",
    ));
    cmds.push(cmd(
        "/agent-doc route",
        "[--dispatch-only] <FILE>",
        "Route command to the correct tmux pane",
    ));
    cmds.push(cmd(
        "/agent-doc commit",
        "<FILE>",
        "Git add + commit with timestamp",
    ));
    cmds.push(cmd(
        "/agent-doc focus",
        "<FILE>",
        "Focus tmux pane for a session document",
    ));
    cmds.push(cmd(
        "/agent-doc sync",
        "--col <FILES>",
        "Sync tmux panes to columnar layout",
    ));
    cmds.push(cmd(
        "/agent-doc layout",
        "<FILES>",
        "Arrange tmux panes to mirror editor",
    ));
    cmds.push(cmd(
        "/agent-doc patch",
        "<FILE> <COMPONENT> [--mode replace|append|prepend]",
        "Replace content in a named component",
    ));
    cmds.push(cmd(
        "/agent-doc watch",
        "",
        "Watch files for changes and auto-run",
    ));
    cmds.push(cmd(
        "/agent-doc outline",
        "<FILE>",
        "Display markdown outline with token counts",
    ));
    cmds.push(cmd(
        "/agent-doc resync",
        "[FILE] [--fix]",
        "Validate the durable session registry globally or for one document; `--fix` aliases `fix`",
    ));
    cmds.push(cmd(
        "/agent-doc fix",
        "[FILE]",
        "Fix stale routing/session issues globally or for one document",
    ));
    cmds.push(cmd(
        "/agent-doc compact",
        "<FILE>",
        "Archive old exchanges to reduce document size",
    ));
    cmds.push(cmd(
        "/agent-doc compact exchange",
        "<FILE>",
        "Compact the exchange component of a document",
    ));
    cmds.push(cmd(
        "/agent-doc convert",
        "<FILE>",
        "Convert append-mode document to template mode",
    ));
    cmds.push(cmd(
        "/agent-doc mode",
        "<FILE>",
        "Get or set the document mode",
    ));
    cmds.push(cmd(
        "/agent-doc write",
        "<FILE>",
        "Append assistant response (reads from stdin)",
    ));
    cmds.push(cmd(
        "/agent-doc response-checkpoint",
        "<FILE>",
        "Persist a cumulative complete response section without sealing the cycle",
    ));
    cmds.push(cmd(
        "/agent-doc respond",
        "<FILE> [write flags...]",
        "Persist the complete response and let the binary seal and commit the turn (`finalize` alias kept)",
    ));
    cmds.push(cmd(
        "/agent-doc repair",
        "<FILE>",
        "Repair orphaned response / stale document cycle and auto-close git-backed repairs (`recover` alias kept)",
    ));
    cmds.push(cmd(
        "/agent-doc doctor",
        "<FILE> [--preflight-json PATH] [--session-check-json PATH] [--limit N] [--json]",
        "Diagnose workflow invariant status and print exact repair/operator actions",
    ));
    cmds.push(cmd(
        "/agent-doc autofix",
        "<FILE> [--preflight-json PATH] [--session-check-json PATH] [--limit N] [--apply] [--dry-run] [--json]",
        "Plan invariant-driven repairs, record de-duplication proof markers, and optionally execute whitelisted safe repairs",
    ));
    cmds.push(cmd(
        "/agent-doc template-info",
        "<FILE>",
        "Show template structure (components, modes)",
    ));
    cmds.push(cmd(
        "/agent-doc prompt",
        "<FILE>",
        "Detect permission prompts from Claude Code or OpenCode",
    ));
    cmds.push(cmd(
        "/agent-doc audit-docs",
        "",
        "Audit instruction files against codebase",
    ));
    cmds.push(cmd(
        "/agent-doc ops summary",
        "[--project-root DIR] [--limit N] [--json]",
        "Summarize high-signal ops.log events by document/session",
    ));
    cmds.push(cmd(
        "/agent-doc ops diagnose",
        "[--cycle-id ID] [--patch-id ID] [--session-id ID] [--file FILE] [--limit N] [--json]",
        "Gather cycle/patch diagnostics from logs, captures, hooks, and actor state",
    ));
    cmds.push(cmd(
        "/agent-doc verify-op-capture",
        "<FILE> [--expect-cafe-demo]",
        "Verify editor op-capture producer and merge-consumer evidence in ops.log",
    ));
    cmds.push(cmd(
        "/agent-doc memory index",
        "<FILE> [--db PATH] [--json]",
        "Index session tracked work and exchange history into .tsift/memory.db",
    ));
    cmds.push(cmd(
        "/agent-doc memory search",
        "<FILE> --query TEXT [--db PATH] [--limit N] [--json] [--rebuild]",
        "Search indexed session memory plus current tracked work",
    ));
    cmds.push(cmd(
        "/agent-doc skill install",
        "",
        "Install Claude Code skill definition",
    ));
    cmds.push(cmd(
        "/agent-doc skill check",
        "",
        "Check if installed skill matches binary",
    ));
    cmds.push(cmd(
        "/agent-doc plugin install",
        "<EDITOR>",
        "Install editor plugin",
    ));
    cmds.push(cmd(
        "/agent-doc plugin update",
        "<EDITOR>",
        "Update editor plugin",
    ));
    cmds.push(cmd(
        "/agent-doc plugin list",
        "",
        "List installed editor plugins",
    ));
    cmds.push(cmd(
        "/agent-doc upgrade",
        "",
        "Check for updates and upgrade",
    ));
    cmds.push(cmd(
        "/agent-doc self-install",
        "[--source-root DIR] [--target-dir DIR] [--keep-worktree]",
        "Install committed checkout from an isolated sibling git worktree",
    ));
    cmds.push(cmd(
        "/agent-doc autoclaim",
        "",
        "Re-establish claims after context compaction",
    ));
    cmds.push(cmd(
        "/agent-doc stream",
        "<FILE>",
        "Stream agent output to document in real-time (CRDT)",
    ));
    cmds.push(cmd(
        "/agent-doc cleanup",
        "<FILE>",
        "Clean up: callback orchestration, compaction",
    ));
    cmds.push(cmd(
        "/agent-doc jobs create",
        "<FILE> [--operation-doc] [--audit]",
        "Generate lower-agent job packets from the current planning record",
    ));
    cmds.push(cmd(
        "/agent-doc orchestrate --mode dag --from-queue",
        "<FILE> [--resume-schedule ID]",
        "Build or resume a persisted auto-DAG schedule from active queue prompts",
    ));
    cmds.push(cmd(
        "/agent-doc jobs list",
        "<FILE> [--json]",
        "List lower-agent job packets for a document",
    ));
    cmds.push(cmd(
        "/agent-doc jobs status",
        "<FILE> [--json]",
        "Show lower-agent job packet result status",
    ));
    cmds.push(cmd(
        "/agent-doc jobs collect",
        "<FILE> [--cycle ID] [--json]",
        "Collect structured worker result envelopes",
    ));
    cmds.push(cmd(
        "/agent-doc backlog add",
        "<FILE> <ITEM>",
        "Add item to the backlog component",
    ));
    cmds.push(cmd(
        "/agent-doc backlog remove",
        "<FILE> <TARGET>",
        "Remove item from the backlog component",
    ));
    cmds.push(cmd(
        "/agent-doc backlog reopen",
        "<FILE> <ID> [--queue]",
        "Move completed work back to the backlog and optionally reactivate its queue directive",
    ));
    cmds.push(cmd(
        "/agent-doc backlog prune",
        "<FILE>",
        "Remove completed items from the backlog",
    ));
    cmds.push(cmd(
        "/agent-doc backlog list",
        "<FILE>",
        "List backlog items",
    ));
    cmds.push(cmd(
        "/agent-doc callback request",
        "<FILE> [<OPS>]",
        "Create IPC callback request",
    ));
    cmds.push(cmd(
        "/agent-doc migrate",
        "[FILES...] [--all] [--dry-run]",
        "Migrate deprecated component names and attributes to canonical forms",
    ));

    // --- Claude Code built-in commands ---
    cmds.push(cmd("/help", "", "Show help and available commands"));
    cmds.push(cmd(
        "/model",
        "<MODEL>",
        "Switch to a different Claude model",
    ));
    cmds.push(cmd("/clear", "", "Clear the conversation history"));
    cmds.push(cmd(
        "/compact",
        "[INSTRUCTIONS]",
        "Compact context to free up space",
    ));
    cmds.push(cmd(
        "/cost",
        "",
        "Show token usage and cost for this session",
    ));
    cmds.push(cmd("/login", "", "Switch Anthropic account"));
    cmds.push(cmd("/logout", "", "Sign out of current account"));
    cmds.push(cmd("/status", "", "Show account and session status"));
    cmds.push(cmd("/config", "", "Show or modify project configuration"));
    cmds.push(cmd("/memory", "", "Edit CLAUDE.md memory files"));
    cmds.push(cmd(
        "/review",
        "",
        "Review and give feedback on Claude Code",
    ));
    cmds.push(cmd("/bug", "", "Report a bug"));
    cmds.push(cmd(
        "/fast",
        "",
        "Toggle fast mode (same model, faster output)",
    ));
    cmds.push(cmd("/slow", "", "Toggle slow mode (thorough processing)"));
    cmds.push(cmd("/permissions", "", "Show or modify tool permissions"));
    cmds.push(cmd(
        "/terminal-setup",
        "",
        "Install shell integration (Shift+Enter)",
    ));
    cmds.push(cmd("/doctor", "", "Check system health and configuration"));
    cmds.push(cmd(
        "/init",
        "",
        "Initialize a new CLAUDE.md for the project",
    ));
    cmds.push(cmd("/pr-comments", "", "View PR comments from GitHub"));
    cmds.push(cmd("/vim", "", "Toggle vim keybinding mode"));
    cmds.push(cmd(
        "/diff",
        "",
        "Show a diff of all file changes since start",
    ));
    cmds.push(cmd("/undo", "", "Undo the last file change"));
    cmds.push(cmd("/resume", "", "Resume a previous conversation"));
    cmds.push(cmd(
        "/listen",
        "",
        "Toggle listen mode (transcribe audio input)",
    ));
    cmds.push(cmd("/mcp", "", "Show MCP server status and tools"));
    cmds.push(cmd("/approved-tools", "", "Show list of approved tools"));
    cmds.push(cmd("/add-dir", "<PATH>", "Add a directory to the context"));
    cmds.push(cmd(
        "/release-notes",
        "",
        "Show release notes for current version",
    ));
    cmds.push(cmd("/hooks", "", "Show configured hooks"));
    cmds.push(cmd(
        "/btw",
        "<MESSAGE>",
        "Quick side question without interrupting",
    ));

    // --- Static project skills (fallback for well-known skills) ---
    cmds.push(cmd(
        "/existence",
        "lookup|search|new|lint|scope [ARGS]",
        "Query and manage ontology terms",
    ));
    cmds.push(cmd(
        "/existence lookup",
        "<TERM>",
        "Read a term's full definition",
    ));
    cmds.push(cmd(
        "/existence search",
        "<QUERY>",
        "Search terms by relevance",
    ));
    cmds.push(cmd(
        "/existence new",
        "<TERM>",
        "Author a new term (interactive)",
    ));
    cmds.push(cmd("/existence lint", "", "Validate ontology nodes"));
    cmds.push(cmd(
        "/existence scope",
        "[RING]",
        "List terms by ring level",
    ));

    cmds.push(cmd(
        "/tagpath",
        "parse|alias|prose|search|lint|extract|graph|init [ARGS]",
        "Parse and analyze tag-based identifiers",
    ));
    cmds.push(cmd(
        "/tagpath parse",
        "<NAME>",
        "Decompose identifier into canonical tags",
    ));
    cmds.push(cmd(
        "/tagpath alias",
        "<NAME>",
        "Generate all convention variants",
    ));
    cmds.push(cmd(
        "/tagpath search",
        "<QUERY> <PATH>",
        "Semantic search across conventions",
    ));
    cmds.push(cmd(
        "/tagpath lint",
        "[PATH]",
        "Validate naming against .naming.toml rules",
    ));
    cmds.push(cmd(
        "/tagpath extract",
        "<PATH>",
        "Extract identifiers from files",
    ));

    // --- Dynamic: plugin commands from ~/.claude/plugins/marketplaces/ ---
    let static_names: std::collections::HashSet<String> =
        cmds.iter().map(|c| c.name.clone()).collect();
    let mut plugin_cmds = discover_plugin_commands();
    plugin_cmds.retain(|c| !static_names.contains(&c.name));
    cmds.extend(plugin_cmds);

    // --- Dynamic: user-invocable skills from .claude/skills/ (relative to CWD) ---
    let all_names: std::collections::HashSet<String> =
        cmds.iter().map(|c| c.name.clone()).collect();
    let mut skill_cmds = discover_skill_commands();
    skill_cmds.retain(|c| !all_names.contains(&c.name));
    cmds.extend(skill_cmds);

    cmds
}

/// Scan `~/.claude/plugins/marketplaces/<MARKETPLACE>/plugins/<NAMESPACE>/commands/*.md`
/// and return one `CommandInfo` per `.md` file with a non-empty `description` frontmatter field.
/// Command names follow the pattern `/<NAMESPACE>:<stem>`.
fn discover_plugin_commands() -> Vec<CommandInfo> {
    use std::fs;
    use std::path::PathBuf;

    let mut cmds = Vec::new();

    let home = std::env::var("HOME").unwrap_or_default();
    if home.is_empty() {
        return cmds;
    }
    let marketplaces_dir = PathBuf::from(&home).join(".claude/plugins/marketplaces");
    if !marketplaces_dir.is_dir() {
        return cmds;
    }

    let Ok(marketplace_entries) = fs::read_dir(&marketplaces_dir) else {
        return cmds;
    };

    for marketplace_entry in marketplace_entries.flatten() {
        let marketplace_path = marketplace_entry.path();
        if !marketplace_path.is_dir() {
            continue;
        }
        let plugins_dir = marketplace_path.join("plugins");
        if !plugins_dir.is_dir() {
            continue;
        }
        let Ok(plugin_entries) = fs::read_dir(&plugins_dir) else {
            continue;
        };
        for plugin_entry in plugin_entries.flatten() {
            let plugin_path = plugin_entry.path();
            if !plugin_path.is_dir() {
                continue;
            }
            let namespace = plugin_path
                .file_name()
                .map(|n| n.to_string_lossy().to_string())
                .unwrap_or_default();
            if namespace.is_empty() || namespace.starts_with('.') {
                continue;
            }
            let commands_dir = plugin_path.join("commands");
            if !commands_dir.is_dir() {
                continue;
            }
            let Ok(cmd_entries) = fs::read_dir(&commands_dir) else {
                continue;
            };
            let mut plugin_cmds: Vec<CommandInfo> = cmd_entries
                .flatten()
                .filter_map(|entry| {
                    let path = entry.path();
                    if path.extension().and_then(|e| e.to_str()) != Some("md") {
                        return None;
                    }
                    let stem = path.file_stem()?.to_string_lossy().to_string();
                    if stem.is_empty() || stem.starts_with('.') {
                        return None;
                    }
                    let content = fs::read_to_string(&path).unwrap_or_default();
                    let (description, args) = parse_command_frontmatter(&content);
                    if description.is_empty() {
                        return None;
                    }
                    Some(CommandInfo {
                        name: format!("/{namespace}:{stem}"),
                        args,
                        description,
                    })
                })
                .collect();
            plugin_cmds.sort_by(|a, b| a.name.cmp(&b.name));
            cmds.extend(plugin_cmds);
        }
    }

    cmds
}

/// Parse `description:` and `argument-hint:` from a YAML frontmatter block.
/// Returns `(description, args)`.
fn parse_command_frontmatter(content: &str) -> (String, String) {
    let mut description = String::new();
    let mut args = String::new();

    let Some(rest) = content.strip_prefix("---") else {
        return (description, args);
    };
    let end = rest.find("\n---").unwrap_or(rest.len());
    let frontmatter = &rest[..end];

    for line in frontmatter.lines() {
        if let Some(val) = line.strip_prefix("description:") {
            description = val.trim().trim_matches('"').trim_matches('\'').to_string();
        } else if let Some(val) = line.strip_prefix("argument-hint:") {
            args = val.trim().trim_matches('"').trim_matches('\'').to_string();
        }
    }

    (description, args)
}

/// Scan `.claude/skills/<name>/SKILL.md` in the current directory and return one
/// `CommandInfo` per skill with `user-invocable: true` in YAML frontmatter.
/// Command name: `/<skill-name>` (directory name).
fn discover_skill_commands() -> Vec<CommandInfo> {
    use std::fs;
    use std::path::PathBuf;

    let mut cmds = Vec::new();

    let skills_dir = PathBuf::from(".claude/skills");
    if !skills_dir.is_dir() {
        return cmds;
    }

    let Ok(entries) = fs::read_dir(&skills_dir) else {
        return cmds;
    };

    let mut discovered: Vec<CommandInfo> = entries
        .flatten()
        .filter_map(|entry| {
            let path = entry.path();
            if !path.is_dir() {
                return None;
            }
            let name = path.file_name()?.to_string_lossy().to_string();
            if name.is_empty() || name.starts_with('.') {
                return None;
            }
            let skill_md = path.join("SKILL.md");
            if !skill_md.is_file() {
                return None;
            }
            let content = fs::read_to_string(&skill_md).unwrap_or_default();
            let (description, args, user_invocable) = parse_skill_frontmatter(&content);
            if !user_invocable || description.is_empty() {
                return None;
            }
            Some(CommandInfo {
                name: format!("/{name}"),
                args,
                description,
            })
        })
        .collect();

    discovered.sort_by(|a, b| a.name.cmp(&b.name));
    cmds.extend(discovered);
    cmds
}

/// Parse `description:`, `argument-hint:`, and `user-invocable:` from YAML frontmatter.
/// Returns `(description, args, user_invocable)`.
fn parse_skill_frontmatter(content: &str) -> (String, String, bool) {
    let mut description = String::new();
    let mut args = String::new();
    let mut user_invocable = false;

    let Some(rest) = content.strip_prefix("---") else {
        return (description, args, user_invocable);
    };
    let end = rest.find("\n---").unwrap_or(rest.len());
    let frontmatter = &rest[..end];

    for line in frontmatter.lines() {
        if let Some(val) = line.strip_prefix("description:") {
            description = val.trim().trim_matches('"').trim_matches('\'').to_string();
        } else if let Some(val) = line.strip_prefix("argument-hint:") {
            args = val.trim().trim_matches('"').trim_matches('\'').to_string();
        } else if let Some(val) = line.strip_prefix("user-invocable:") {
            let v = val.trim();
            user_invocable = v == "true";
        }
    }

    (description, args, user_invocable)
}

fn cmd(name: &str, args: &str, description: &str) -> CommandInfo {
    CommandInfo {
        name: name.to_string(),
        args: args.to_string(),
        description: description.to_string(),
    }
}

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

    #[test]
    fn all_commands_is_nonempty() {
        let cmds = all_commands();
        assert!(cmds.len() > 50, "expected 50+ commands, got {}", cmds.len());
    }

    #[test]
    fn all_commands_have_name_and_description() {
        for cmd in all_commands() {
            assert!(!cmd.name.is_empty(), "command name must not be empty");
            assert!(
                !cmd.description.is_empty(),
                "command '{}' must have a description",
                cmd.name
            );
            assert!(
                cmd.name.starts_with('/'),
                "command '{}' must start with /",
                cmd.name
            );
        }
    }

    #[test]
    fn serializes_to_json() {
        let cmds = all_commands();
        let json = serde_json::to_string(&cmds).unwrap();
        assert!(json.contains("/agent-doc"));
        assert!(json.contains("/agent-doc self-install"));
        assert!(json.contains("/help"));
        assert!(json.contains("/existence"));
    }

    #[test]
    fn parse_command_frontmatter_extracts_description_and_args() {
        let content = r#"---
description: Check whether the local Codex CLI is ready
argument-hint: '[--enable-review-gate|--disable-review-gate]'
allowed-tools: Bash(node:*)
---

Body text here.
"#;
        let (desc, args) = parse_command_frontmatter(content);
        assert_eq!(desc, "Check whether the local Codex CLI is ready");
        assert_eq!(args, "[--enable-review-gate|--disable-review-gate]");
    }

    #[test]
    fn parse_command_frontmatter_handles_missing_fields() {
        let (desc, args) = parse_command_frontmatter("no frontmatter here");
        assert!(desc.is_empty());
        assert!(args.is_empty());
    }
}
