//! Command registration: single source of truth for every top-level CLI command.
//!
//! Issue #1097 (audit finding EX-V1.29-1) collapses the 5+ exhaustive matches
//! that previously needed coordinated edits per new command:
//!
//!   1. `Commands` enum variant declaration       — still owned by `definitions.rs` (clap derive)
//!   2. `Commands::batch_support()`               — generated from the table below
//!   3. `Commands::variant_name()`                — generated from the table below
//!   4. `dispatch.rs` Group A match               — generated from the table below
//!   5. `dispatch.rs` Group B match               — generated from the table below
//!
//! Adding a new command now means:
//!
//!   * Declare the variant in `definitions.rs::Commands` (clap requires a real enum).
//!   * Add one row to either the `group_a` or `group_b` list below.
//!   * Implement the handler in `commands/<area>/`.
//!
//! A missing row is a compile error: every emitter expands to an exhaustive
//! match over `Commands`, so the variant has nowhere to go without a row.
//! The legacy `_ => {}` and `_ => unreachable!()` catch-alls in dispatch are
//! gone — every Group A row appears as `Some($wild) => {}` in the Group B
//! match (and vice versa), generated by the macro.
//!
//! ## Row format
//!
//! ```text
//! ($bind_pat, $wild_pat, $name, $batch_support, $body)
//! ```
//!
//! * `$bind_pat`: pattern with `ref`/by-value bindings used by the dispatch
//!   body. Example: `Commands::Brief { ref path, ref output }`.
//! * `$wild_pat`: pattern with `{ .. }` (no bindings). Used in the
//!   `variant_name()` and the cross-group "Some($wild) => {} | unreachable!()"
//!   arms so the compiler doesn't warn about unused bindings.
//!   Example: `Commands::Brief { .. }`.
//! * `$name`: lower-kebab-case command name string used for tracing labels.
//! * `$batch_support`: `BatchSupport::Cli` or `BatchSupport::Daemon`. Can be
//!   any expression evaluated in scope of the bind pattern's bindings, so
//!   `Notes`/`Suggest` use a runtime `match` on the bound subcmd.
//! * `$body`: dispatch arm body block (`Result<()>`).
//!
//! ## Group split
//!
//! * **`group_a`** — store-less / lifecycle / mutation commands. Each body
//!   runs in `run_with` BEFORE `CommandContext::open_readonly`. The dispatch
//!   wraps the body in `return …` so a Group A command short-circuits the
//!   later store-opening path.
//! * **`group_b`** — store-using commands. Each body runs AFTER
//!   `let ctx = CommandContext::open_readonly(&$cli)?;` in `run_with`. The
//!   body has `&$ctx` and `&$cli` in scope.
//!
//! Cfg-gated rows wear a `#[cfg(feature = "…")]` attribute prefix; the
//! attribute is forwarded to every emitted arm so the variant disappears
//! consistently across all four generated functions.

/// Iterate every registered `Commands` variant.
///
/// The emitter macro receives the rows as two named brace groups
/// (`group_a: { … } group_b: { … }`), each containing a comma-separated
/// list of `( $bind_pat , $wild_pat , $name , $bs , $body )` rows.
macro_rules! for_each_command {
    ($emit:ident, cli = $cli:ident, ctx = $ctx:ident, project_cqs_dir = $pcd:ident) => {
        $emit! {
            cli = $cli,
            ctx = $ctx,
            project_cqs_dir = $pcd,
            group_a: {
                // ── lifecycle / no-store / mutation commands ────────────

                (Commands::Init { json },
                 Commands::Init { .. },
                 "init",
                 BatchSupport::Cli,
                 { cmd_init(&$cli, json) }),

                (Commands::Cache { ref subcmd },
                 Commands::Cache { .. },
                 "cache",
                 BatchSupport::Cli,
                 { cmd_cache(&$cli, subcmd) }),

                (Commands::Slot { ref subcmd },
                 Commands::Slot { .. },
                 "slot",
                 BatchSupport::Cli,
                 { cmd_slot(&$cli, subcmd) }),

                (Commands::Doctor { fix, verbose, json },
                 Commands::Doctor { .. },
                 "doctor",
                 BatchSupport::Cli,
                 { cmd_doctor($cli.model.as_deref(), fix, verbose, $cli.json || json) }),

                (Commands::Ping { json },
                 Commands::Ping { .. },
                 "ping",
                 BatchSupport::Cli,
                 { cmd_ping($cli.json || json) }),

                // API-V1.29-6: `cqs refresh` is a daemon-only concept. By the
                // time we reach this arm `try_daemon_query` already forwarded
                // the request if a daemon was running, so we're guaranteed
                // there isn't one. Emit a polite no-op.
                // P2.14: Refresh now carries its own --json (in addition to
                // honoring the global --json), so the macro pattern-match
                // needs to bind the field.
                (Commands::Refresh { json },
                 Commands::Refresh { .. },
                 "refresh",
                 BatchSupport::Daemon,
                 {
                    if $cli.json || json {
                        crate::cli::json_envelope::emit_json(&serde_json::json!({
                            "status": "noop",
                            "message": "no daemon running, nothing to refresh",
                            "refreshed": false,
                            "daemon_running": false,
                            "caches_invalidated": [],
                        }))
                    } else {
                        println!("no daemon running, nothing to refresh");
                        Ok(())
                    }
                 }),

                (Commands::Index { ref args },
                 Commands::Index { .. },
                 "index",
                 BatchSupport::Cli,
                 { cmd_index(&$cli, args) }),

                (Commands::Watch { debounce, no_ignore, poll, serve },
                 Commands::Watch { .. },
                 "watch",
                 BatchSupport::Cli,
                 { watch::cmd_watch(&$cli, debounce, no_ignore, poll, serve) }),

                #[cfg(feature = "serve")]
                (Commands::Serve { port, ref bind, open, no_auth },
                 Commands::Serve { .. },
                 "serve",
                 BatchSupport::Cli,
                 { crate::cli::commands::serve::cmd_serve(port, bind.clone(), open, no_auth) }),

                (Commands::Batch,
                 Commands::Batch,
                 "batch",
                 BatchSupport::Cli,
                 { batch::cmd_batch() }),

                (Commands::Chat,
                 Commands::Chat,
                 "chat",
                 BatchSupport::Cli,
                 { chat::cmd_chat() }),

                (Commands::Completions { shell },
                 Commands::Completions { .. },
                 "completions",
                 BatchSupport::Cli,
                 {
                    cmd_completions(shell);
                    Ok(())
                 }),

                (Commands::TrainData {
                    ref repos,
                    ref output,
                    max_commits,
                    min_msg_len,
                    max_files,
                    dedup_cap,
                    resume,
                    verbose,
                 },
                 Commands::TrainData { .. },
                 "train-data",
                 BatchSupport::Cli,
                 {
                    cmd_train_data(cqs::train_data::TrainDataConfig {
                        repos: repos.clone(),
                        output: output.clone(),
                        // API-V1.22-13: CLI surface uses `Option<usize>`
                        // (None = unlimited). The library's TrainDataConfig
                        // still uses `usize` with `0` as the "no cap"
                        // sentinel — translate at the dispatch boundary to
                        // keep the lib API stable.
                        max_commits: max_commits.unwrap_or(0),
                        min_msg_len,
                        max_files,
                        dedup_cap,
                        resume,
                        verbose,
                    })
                 }),

                (Commands::ExportModel { ref repo, ref output, dim },
                 Commands::ExportModel { .. },
                 "export-model",
                 BatchSupport::Cli,
                 { cmd_export_model(repo, output, dim) }),

                #[cfg(feature = "convert")]
                (Commands::Convert {
                    ref path,
                    ref output,
                    overwrite,
                    dry_run,
                    ref clean_tags,
                    json,
                 },
                 Commands::Convert { .. },
                 "convert",
                 BatchSupport::Cli,
                 {
                    cmd_convert(
                        path,
                        output.as_deref(),
                        overwrite,
                        dry_run,
                        clean_tags.as_deref(),
                        $cli.json || json,
                    )
                 }),

                (Commands::Telemetry { reset, ref reason, all, ref output },
                 Commands::Telemetry { .. },
                 "telemetry",
                 BatchSupport::Cli,
                 {
                    if reset {
                        // API-V1.29-3: forward the resolved --json bit so reset
                        // emits the `{archived_events, archive_path, lock_path}`
                        // envelope instead of silently dropping the flag.
                        cmd_telemetry_reset(&$pcd, reason.as_deref(), $cli.json || output.json)
                    } else {
                        cmd_telemetry(&$pcd, $cli.json || output.json, all)
                    }
                 }),

                (Commands::Project { ref subcmd },
                 Commands::Project { .. },
                 "project",
                 BatchSupport::Cli,
                 { cmd_project(&$cli, subcmd, $cli.try_model_config()?) }),

                // Model: each subcommand opens its own Store at known paths
                // (`cqs model show/list` open readonly; `swap` orchestrates
                // a backup + reindex). None fit through `CommandContext`
                // because `swap` deletes the open store under it.
                (Commands::Model { ref subcmd },
                 Commands::Model { .. },
                 "model",
                 BatchSupport::Cli,
                 { cmd_model(&$cli, subcmd) }),

                // Diff/Drift open stores on arbitrary paths, not via
                // `CommandContext`.
                (Commands::Diff { ref args, ref output },
                 Commands::Diff { .. },
                 "diff",
                 BatchSupport::Cli,
                 {
                    cmd_diff(
                        &args.source,
                        args.target.as_deref(),
                        args.threshold,
                        args.lang.as_deref(),
                        $cli.json || output.json,
                    )
                 }),

                (Commands::Drift { ref args, ref output },
                 Commands::Drift { .. },
                 "drift",
                 BatchSupport::Cli,
                 {
                    cmd_drift(
                        &args.reference,
                        args.threshold,
                        args.min_drift,
                        args.lang.as_deref(),
                        args.limit,
                        $cli.json || output.json,
                    )
                 }),

                (Commands::Ref { ref subcmd },
                 Commands::Ref { .. },
                 "ref",
                 BatchSupport::Cli,
                 { cmd_ref(&$cli, subcmd) }),

                // #946 typestate: Gc mutates the DB (prune_all + HNSW
                // rebuild). Daemon holds `Store<ReadOnly>`, so `prune_all`
                // is literally not callable there. Must go through the CLI
                // path, which opens `Store<ReadWrite>` via
                // `CommandContext::open_readwrite`.
                (Commands::Gc { ref output },
                 Commands::Gc { .. },
                 "gc",
                 BatchSupport::Cli,
                 { cmd_gc(&$cli, $cli.json || output.json) }),

                // AuditMode doesn't use a store — uses find_project_root +
                // resolve_index_dir directly.
                (Commands::AuditMode { ref state, ref expires, ref output },
                 Commands::AuditMode { .. },
                 "audit-mode",
                 BatchSupport::Cli,
                 { cmd_audit_mode(state.as_ref(), expires, $cli.json || output.json) }),

                // Notes: opening the readonly store is optional — mutations
                // (`add`/`update`/`remove`) must work on a fresh project
                // before any `cqs init && cqs index` has run. `cmd_notes`
                // only requires the store for `list`, which it enforces
                // internally.
                //
                // SEC-D.9: don't silently collapse distinct store-open
                // failures (schema corruption, dim mismatch, permission
                // denied, missing index) into a single clueless "Index not
                // found" downstream. Mutations work without an open store
                // so we still pass `None`, but we leave a debug breadcrumb
                // so an operator running with `RUST_LOG=cqs=debug` sees the
                // underlying cause.
                //
                // Notes::List is daemon-dispatchable; mutations must hit
                // CLI for the filesystem reindex. Inline-decide via
                // `match subcmd`.
                (Commands::Notes { ref subcmd },
                 Commands::Notes { .. },
                 "notes",
                 match subcmd {
                    NotesCommand::List { .. } => BatchSupport::Daemon,
                    _ => BatchSupport::Cli,
                 },
                 {
                    let notes_ctx = match crate::cli::CommandContext::open_readonly(&$cli) {
                        Ok(c) => Some(c),
                        Err(e) => {
                            tracing::debug!(
                                error = %e,
                                "Notes: readonly store open failed; mutations will use write-only path"
                            );
                            None
                        }
                    };
                    cmd_notes(&$cli, notes_ctx.as_ref(), subcmd)
                 }),
            }

            group_b: {
                // ── store-using commands (run after `let ctx = …?`) ─────

                (Commands::Affected { ref base, stdin, ref output },
                 Commands::Affected { .. },
                 "affected",
                 BatchSupport::Daemon,
                 { cmd_affected(&$ctx, base.as_deref(), stdin, $cli.json || output.json) }),

                (Commands::Blame { ref args, ref output },
                 Commands::Blame { .. },
                 "blame",
                 BatchSupport::Daemon,
                 {
                    cmd_blame(
                        &$ctx,
                        &args.name,
                        args.commits,
                        args.callers,
                        $cli.json || output.json,
                    )
                 }),

                (Commands::Brief { ref path, ref output },
                 Commands::Brief { .. },
                 "brief",
                 BatchSupport::Cli,
                 { cmd_brief(&$ctx, path, $cli.json || output.json) }),

                (Commands::Stats { ref output },
                 Commands::Stats { .. },
                 "stats",
                 BatchSupport::Daemon,
                 { cmd_stats(&$ctx, $cli.json || output.json) }),

                (Commands::Deps { ref args, ref output },
                 Commands::Deps { .. },
                 "deps",
                 BatchSupport::Daemon,
                 {
                    cmd_deps(
                        &$ctx,
                        &args.name,
                        args.reverse,
                        args.limit_arg.limit,
                        args.cross_project,
                        $cli.json || output.json,
                    )
                 }),

                (Commands::Callers { ref args, ref output },
                 Commands::Callers { .. },
                 "callers",
                 BatchSupport::Daemon,
                 {
                    cmd_callers(
                        &$ctx,
                        &args.name,
                        args.limit_arg.limit,
                        args.cross_project,
                        $cli.json || output.json,
                    )
                 }),

                (Commands::Callees { ref args, ref output },
                 Commands::Callees { .. },
                 "callees",
                 BatchSupport::Daemon,
                 {
                    cmd_callees(
                        &$ctx,
                        &args.name,
                        args.limit_arg.limit,
                        args.cross_project,
                        $cli.json || output.json,
                    )
                 }),

                (Commands::Onboard { ref args, ref output },
                 Commands::Onboard { .. },
                 "onboard",
                 BatchSupport::Daemon,
                 {
                    cmd_onboard(
                        &$ctx,
                        &args.query,
                        args.depth,
                        args.limit_arg.limit,
                        $cli.json || output.json,
                        args.tokens,
                    )
                 }),

                (Commands::Neighbors { ref name, limit, ref output },
                 Commands::Neighbors { .. },
                 "neighbors",
                 BatchSupport::Cli,
                 { cmd_neighbors(&$ctx, name, limit, $cli.json || output.json) }),

                (Commands::Explain { ref args, ref output },
                 Commands::Explain { .. },
                 "explain",
                 BatchSupport::Daemon,
                 {
                    cmd_explain(
                        &$ctx,
                        &args.name,
                        args.limit_arg.limit,
                        $cli.json || output.json,
                        args.tokens,
                    )
                 }),

                (Commands::Similar { ref args, ref output },
                 Commands::Similar { .. },
                 "similar",
                 BatchSupport::Daemon,
                 {
                    cmd_similar(
                        &$ctx,
                        &args.name,
                        args.limit,
                        args.threshold,
                        $cli.json || output.json,
                    )
                 }),

                // Task #8: top-level `--json` ($cli.json) overrides whatever
                // the subcommand's `--format` says. `effective_format()`
                // already honours `output.json`; we OR $cli.json on top so
                // `cqs --json impact foo` works without `--json` on the
                // subcommand.
                (Commands::Impact { ref args, ref output },
                 Commands::Impact { .. },
                 "impact",
                 BatchSupport::Daemon,
                 {
                    let format = if $cli.json {
                        crate::cli::OutputFormat::Json
                    } else {
                        output.effective_format()
                    };
                    cmd_impact(
                        &$ctx,
                        &args.name,
                        args.depth,
                        args.limit_arg.limit,
                        &format,
                        args.suggest_tests,
                        args.type_impact,
                        args.cross_project,
                    )
                 }),

                (Commands::ImpactDiff { ref args, ref output },
                 Commands::ImpactDiff { .. },
                 "impact-diff",
                 BatchSupport::Daemon,
                 {
                    cmd_impact_diff(
                        &$ctx,
                        args.base.as_deref(),
                        args.stdin,
                        $cli.json || output.json,
                    )
                 }),

                (Commands::Review { ref args, ref output },
                 Commands::Review { .. },
                 "review",
                 BatchSupport::Daemon,
                 {
                    let format = if $cli.json {
                        crate::cli::OutputFormat::Json
                    } else {
                        output.effective_format()
                    };
                    cmd_review(&$ctx, args.base.as_deref(), args.stdin, &format, args.tokens)
                 }),

                (Commands::Ci { ref args, ref output },
                 Commands::Ci { .. },
                 "ci",
                 BatchSupport::Daemon,
                 {
                    let format = if $cli.json {
                        crate::cli::OutputFormat::Json
                    } else {
                        output.effective_format()
                    };
                    cmd_ci(
                        &$ctx,
                        args.base.as_deref(),
                        args.stdin,
                        &format,
                        &args.gate,
                        args.tokens,
                    )
                 }),

                (Commands::Trace { ref args, ref output },
                 Commands::Trace { .. },
                 "trace",
                 BatchSupport::Daemon,
                 {
                    let format = if $cli.json {
                        crate::cli::OutputFormat::Json
                    } else {
                        output.effective_format()
                    };
                    cmd_trace(
                        &$ctx,
                        &args.source,
                        &args.target,
                        args.max_depth as usize,
                        args.limit_arg.limit,
                        &format,
                        args.cross_project,
                    )
                 }),

                (Commands::TestMap { ref args, ref output },
                 Commands::TestMap { .. },
                 "test-map",
                 BatchSupport::Daemon,
                 {
                    cmd_test_map(
                        &$ctx,
                        &args.name,
                        args.depth,
                        args.limit_arg.limit,
                        args.cross_project,
                        $cli.json || output.json,
                    )
                 }),

                (Commands::Context { ref args, ref output },
                 Commands::Context { .. },
                 "context",
                 BatchSupport::Daemon,
                 {
                    cmd_context(
                        &$ctx,
                        &args.path,
                        $cli.json || output.json,
                        args.summary,
                        args.compact,
                        args.tokens,
                    )
                 }),

                (Commands::Dead { ref args, ref output },
                 Commands::Dead { .. },
                 "dead",
                 BatchSupport::Daemon,
                 {
                    cmd_dead(
                        &$ctx,
                        $cli.json || output.json,
                        args.include_pub,
                        args.min_confidence,
                    )
                 }),

                (Commands::Gather { ref args, ref output },
                 Commands::Gather { .. },
                 "gather",
                 BatchSupport::Daemon,
                 {
                    cmd_gather(&super::commands::GatherContext {
                        ctx: &$ctx,
                        query: &args.query,
                        expand: args.depth,
                        direction: args.direction,
                        limit: args.limit,
                        max_tokens: args.tokens,
                        ref_name: args.ref_name.as_deref(),
                        json: $cli.json || output.json,
                    })
                 }),

                (Commands::Health { ref output },
                 Commands::Health { .. },
                 "health",
                 BatchSupport::Daemon,
                 { cmd_health(&$ctx, $cli.json || output.json) }),

                (Commands::Stale { ref args, ref output },
                 Commands::Stale { .. },
                 "stale",
                 BatchSupport::Daemon,
                 { cmd_stale(&$ctx, $cli.json || output.json, args.count_only) }),

                // #946 typestate: Suggest with --apply rewrites notes.toml
                // and calls `index_notes` → `replace_notes_for_file` (a
                // write). Classify on the `apply` flag: read-only dry-run
                // is daemon-dispatchable; the write variant must hit CLI.
                (Commands::Suggest { ref args, ref output },
                 Commands::Suggest { .. },
                 "suggest",
                 if args.apply { BatchSupport::Cli } else { BatchSupport::Daemon },
                 { cmd_suggest(&$ctx, $cli.json || output.json, args.apply) }),

                (Commands::Read { ref args, ref output },
                 Commands::Read { .. },
                 "read",
                 BatchSupport::Daemon,
                 {
                    cmd_read(
                        &$ctx,
                        &args.path,
                        args.focus.as_deref(),
                        $cli.json || output.json,
                    )
                 }),

                (Commands::Reconstruct { ref path, ref output },
                 Commands::Reconstruct { .. },
                 "reconstruct",
                 BatchSupport::Cli,
                 { cmd_reconstruct(&$ctx, path, $cli.json || output.json) }),

                (Commands::Related { ref args, ref output },
                 Commands::Related { .. },
                 "related",
                 BatchSupport::Daemon,
                 { cmd_related(&$ctx, &args.name, args.limit, $cli.json || output.json) }),

                (Commands::Where { ref args, ref output },
                 Commands::Where { .. },
                 "where",
                 BatchSupport::Daemon,
                 { cmd_where(&$ctx, &args.description, args.limit, $cli.json || output.json) }),

                (Commands::Scout { ref args, ref output },
                 Commands::Scout { .. },
                 "scout",
                 BatchSupport::Daemon,
                 {
                    cmd_scout(
                        &$ctx,
                        &args.query,
                        args.limit,
                        $cli.json || output.json,
                        args.tokens,
                    )
                 }),

                (Commands::Plan { ref args, ref output },
                 Commands::Plan { .. },
                 "plan",
                 BatchSupport::Daemon,
                 {
                    cmd_plan(
                        &$ctx,
                        &args.description,
                        args.limit,
                        $cli.json || output.json,
                        args.tokens,
                    )
                 }),

                (Commands::Task { ref args, ref output },
                 Commands::Task { .. },
                 "task",
                 BatchSupport::Daemon,
                 {
                    cmd_task(
                        &$ctx,
                        &args.description,
                        args.limit,
                        $cli.json || output.json,
                        args.tokens,
                        args.brief,
                    )
                 }),

                (Commands::TrainPairs { ref output, limit, ref language, contrastive },
                 Commands::TrainPairs { .. },
                 "train-pairs",
                 BatchSupport::Cli,
                 {
                    cmd_train_pairs(
                        &$ctx,
                        output.as_path(),
                        limit,
                        language.as_deref(),
                        contrastive,
                    )
                 }),

                // Eval is a long-running per-process operation. CLI-only:
                // file I/O, progress to stderr, optional --save side effect.
                (Commands::Eval { ref args },
                 Commands::Eval { .. },
                 "eval",
                 BatchSupport::Cli,
                 { cmd_eval(&$ctx, args) }),
            }
        }
    };
}

pub(crate) use for_each_command;
