//! Phase 4a–4d2 host-registry commands: app-level ex commands that need `&mut App`.
//!
//! Phase 4e will add more commands here.

use std::sync::LazyLock;

use hjkl_ex::{ArgKind, ExEffect, HostCmd};

use super::App;

/// `:tabnext` / `:tabn` — cycle to the next tab, wrapping around.
pub struct TabNextCmd;

impl HostCmd<App> for TabNextCmd {
    fn name(&self) -> &'static str {
        "tabnext"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &["tabn"]
    }

    /// Vim accepts `:tabn` (4 chars); match that minimum.
    fn min_prefix(&self) -> usize {
        4
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        app.do_tabnext();
        Some(ExEffect::Ok)
    }
}

// ── Phase 4b commands ────────────────────────────────────────────────────────

/// `:split` / `:sp` — open a horizontal split (optional file arg).
pub struct SplitCmd;

impl HostCmd<App> for SplitCmd {
    fn name(&self) -> &'static str {
        "split"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &["sp"]
    }

    fn min_prefix(&self) -> usize {
        2
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::Path
    }

    fn run(&self, app: &mut App, args: &str) -> Option<ExEffect> {
        app.do_split(args.trim());
        Some(ExEffect::Ok)
    }
}

/// `:vsplit` / `:vsp` — open a vertical split (optional file arg).
pub struct VsplitCmd;

impl HostCmd<App> for VsplitCmd {
    fn name(&self) -> &'static str {
        "vsplit"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &["vsp"]
    }

    fn min_prefix(&self) -> usize {
        2
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::Path
    }

    fn run(&self, app: &mut App, args: &str) -> Option<ExEffect> {
        app.do_vsplit(args.trim());
        Some(ExEffect::Ok)
    }
}

/// `:close` / `:clo` — close the focused window.
pub struct CloseCmd;

impl HostCmd<App> for CloseCmd {
    fn name(&self) -> &'static str {
        "close"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &["clo"]
    }

    fn min_prefix(&self) -> usize {
        3
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        app.close_focused_window();
        Some(ExEffect::Ok)
    }
}

/// `:tabnew` — open a new tab (optional file arg).
pub struct TabnewCmd;

impl HostCmd<App> for TabnewCmd {
    fn name(&self) -> &'static str {
        "tabnew"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &["tabedit", "tabe"]
    }

    fn min_prefix(&self) -> usize {
        4
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::Path
    }

    fn run(&self, app: &mut App, args: &str) -> Option<ExEffect> {
        app.do_tabnew(args.trim());
        Some(ExEffect::Ok)
    }
}

/// `:tabprevious` / `:tabp` / `:tabN` / `:tabNext` — cycle to the previous tab,
/// wrapping. The canonical name MUST be the full vim word so prefix matching
/// (`name().starts_with(typed)`) accepts the full `:tabprevious` form — not just
/// `:tabprev` (#263).
pub struct TabprevCmd;

impl HostCmd<App> for TabprevCmd {
    fn name(&self) -> &'static str {
        "tabprevious"
    }

    fn aliases(&self) -> &'static [&'static str] {
        // `tabp`/`tabprev` resolve via prefix match against the full name;
        // `tabN`/`tabNext` (capital N) are distinct vim spellings for prev-tab
        // and must be explicit aliases.
        &["tabN", "tabNext"]
    }

    fn min_prefix(&self) -> usize {
        4
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        app.do_tabprev();
        Some(ExEffect::Ok)
    }
}

/// `:tabclose` / `:tabc` — close the current tab.
pub struct TabcloseCmd;

impl HostCmd<App> for TabcloseCmd {
    fn name(&self) -> &'static str {
        "tabclose"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &["tabc"]
    }

    fn min_prefix(&self) -> usize {
        4
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        app.do_tabclose();
        Some(ExEffect::Ok)
    }
}

/// `:tabmove` — reorder the current tab (optional N/+N/-N arg).
pub struct TabmoveCmd;

impl HostCmd<App> for TabmoveCmd {
    fn name(&self) -> &'static str {
        "tabmove"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &[]
    }

    fn min_prefix(&self) -> usize {
        4
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::Raw
    }

    fn run(&self, app: &mut App, args: &str) -> Option<ExEffect> {
        app.do_tabmove(args.trim());
        Some(ExEffect::Ok)
    }
}

/// `:only` / `:on` — close all windows except the focused one.
pub struct OnlyCmd;

impl HostCmd<App> for OnlyCmd {
    fn name(&self) -> &'static str {
        "only"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &["on"]
    }

    fn min_prefix(&self) -> usize {
        2
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        app.only_focused_window();
        Some(ExEffect::Ok)
    }
}

// ── Phase 4c commands ────────────────────────────────────────────────────────

/// `:bnext` / `:bn` — advance to the next buffer slot, wrapping.
pub struct BnextCmd;

impl HostCmd<App> for BnextCmd {
    fn name(&self) -> &'static str {
        "bnext"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &["bn"]
    }

    /// Vim accepts `:bn` (2 chars); COMMAND_NAMES confirms min_prefix=2.
    fn min_prefix(&self) -> usize {
        2
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        app.buffer_next();
        Some(ExEffect::Ok)
    }
}

/// `:bprevious` / `:bp` / `:bNext` — retreat to the previous buffer slot, wrapping.
pub struct BprevCmd;

impl HostCmd<App> for BprevCmd {
    fn name(&self) -> &'static str {
        "bprevious"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &["bp", "bNext"]
    }

    /// COMMAND_NAMES: bprevious min_prefix=2; bNext min_prefix=2.
    fn min_prefix(&self) -> usize {
        2
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        app.buffer_prev();
        Some(ExEffect::Ok)
    }
}

/// `:bfirst` / `:bf` — jump to the first buffer slot (index 0).
pub struct BfirstCmd;

impl HostCmd<App> for BfirstCmd {
    fn name(&self) -> &'static str {
        "bfirst"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &["bf"]
    }

    /// COMMAND_NAMES: bfirst min_prefix=2.
    fn min_prefix(&self) -> usize {
        2
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        app.switch_to(0);
        Some(ExEffect::Ok)
    }
}

/// `:blast` / `:bl` — jump to the last buffer slot.
pub struct BlastCmd;

impl HostCmd<App> for BlastCmd {
    fn name(&self) -> &'static str {
        "blast"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &["bl"]
    }

    /// COMMAND_NAMES: blast min_prefix=2.
    fn min_prefix(&self) -> usize {
        2
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        let last = app.slots.len().saturating_sub(1);
        app.switch_to(last);
        Some(ExEffect::Ok)
    }
}

/// `:buffers` / `:ls` / `:files` — display the buffer list in the status area.
pub struct BuffersCmd;

impl HostCmd<App> for BuffersCmd {
    fn name(&self) -> &'static str {
        "buffers"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &["ls", "files"]
    }

    /// COMMAND_NAMES: buffers/ls/files all min_prefix=2.
    fn min_prefix(&self) -> usize {
        2
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        let info = app.list_buffers();
        Some(ExEffect::Info(info))
    }
}

/// `:clipboard` — display the clipboard backend capabilities.
/// Not in COMMAND_NAMES; matched by exact name only.
pub struct ClipboardCmd;

impl HostCmd<App> for ClipboardCmd {
    fn name(&self) -> &'static str {
        "clipboard"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &[]
    }

    /// Not in COMMAND_NAMES; use full-name length as min_prefix (no abbreviation).
    fn min_prefix(&self) -> usize {
        4
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        let info = app.clipboard_status();
        Some(ExEffect::Info(info))
    }
}

// ── Phase 4d2 commands ───────────────────────────────────────────────────────

/// `:picker` — open the fuzzy file picker.
pub struct PickerCmd;

impl HostCmd<App> for PickerCmd {
    fn name(&self) -> &'static str {
        "picker"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &[]
    }

    fn min_prefix(&self) -> usize {
        6
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        app.open_picker();
        Some(ExEffect::Ok)
    }
}

/// `:rg [pattern]` — open the ripgrep content-search picker.
pub struct RgCmd;

impl HostCmd<App> for RgCmd {
    fn name(&self) -> &'static str {
        "rg"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &[]
    }

    fn min_prefix(&self) -> usize {
        2
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::Raw
    }

    fn run(&self, app: &mut App, args: &str) -> Option<ExEffect> {
        let pattern = if args.is_empty() { None } else { Some(args) };
        app.open_grep_picker(pattern);
        Some(ExEffect::Ok)
    }
}

/// `:b [num|name]` — switch to a buffer by 1-based index or filename fragment.
pub struct BCmd;

impl HostCmd<App> for BCmd {
    fn name(&self) -> &'static str {
        "b"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &[]
    }

    /// min_prefix=1 so `:b <arg>` resolves; `:b#` is excluded by exact match
    /// (the legacy arm keeps that).
    fn min_prefix(&self) -> usize {
        1
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::View
    }

    fn run(&self, app: &mut App, args: &str) -> Option<ExEffect> {
        let arg = args.trim();
        if arg.is_empty() {
            return Some(ExEffect::Error("E94: No matching buffer".into()));
        }
        if arg.chars().all(|c| c.is_ascii_digit()) {
            let n: usize = arg.parse().unwrap_or(0);
            // Buffer numbers are raw 1-based slot indices (matches `:ls`'s
            // `i + 1` numbering, which is never renumbered/compacted around
            // the special panes). NO special slot is a valid `:b N` target —
            // every one of them is excluded from `:ls`'s listing and
            // `switch_to` silently no-ops on all of them — so treat them the
            // same as out-of-range. Explorer-only here meant `:b N` on the
            // quickfix or `q:` scratch slot passed validation and then did
            // nothing at all, with no message (#63 Phase 5).
            let is_valid = n != 0 && app.slots.get(n - 1).is_some_and(|s| !s.is_special());
            if !is_valid {
                return Some(ExEffect::Error(format!("E86: Buffer {n} does not exist")));
            }
            app.switch_to(n - 1);
        } else {
            let arg_lower = arg.to_lowercase();
            let matches: Vec<usize> = app
                .slots
                .iter()
                .enumerate()
                .filter(|(_, s)| {
                    s.filename
                        .as_ref()
                        .and_then(|p| p.file_name())
                        .and_then(|n| n.to_str())
                        .is_some_and(|n| n.to_lowercase().contains(&arg_lower))
                })
                .map(|(i, _)| i)
                .collect();
            match matches.len() {
                0 => {
                    return Some(ExEffect::Error(format!(
                        "E94: No matching buffer for {arg}"
                    )));
                }
                1 => {
                    app.switch_to(matches[0]);
                }
                _ => {
                    return Some(ExEffect::Error(format!(
                        "E93: More than one match for {arg}"
                    )));
                }
            }
        }
        Some(ExEffect::Ok)
    }
}

/// `:bpicker` — open the buffer picker.
pub struct BpickerCmd;

impl HostCmd<App> for BpickerCmd {
    fn name(&self) -> &'static str {
        "bpicker"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &[]
    }

    fn min_prefix(&self) -> usize {
        6
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        app.open_buffer_picker();
        Some(ExEffect::Ok)
    }
}

/// `:checktime` — reload buffers that changed on disk.
pub struct ChecktimeCmd;

impl HostCmd<App> for ChecktimeCmd {
    fn name(&self) -> &'static str {
        "checktime"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &[]
    }

    fn min_prefix(&self) -> usize {
        5
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        app.checktime_all();
        Some(ExEffect::Ok)
    }
}

/// `:DiffOrig` — read-only split with the unified diff of the buffer vs disk (#208).
pub struct DiffOrigCmd;

impl HostCmd<App> for DiffOrigCmd {
    fn name(&self) -> &'static str {
        "DiffOrig"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &[]
    }

    fn min_prefix(&self) -> usize {
        8
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        app.diff_orig();
        Some(ExEffect::Ok)
    }
}

/// `:diffthis` — mark the focused window for diff mode (#208 Phase 2).
pub struct DiffThisCmd;

impl HostCmd<App> for DiffThisCmd {
    fn name(&self) -> &'static str {
        "diffthis"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &[]
    }

    fn min_prefix(&self) -> usize {
        5
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        app.diff_this();
        Some(ExEffect::Ok)
    }
}

/// `:diffsplit {file}` — open `{file}` in a vertical split and diff it (#208).
pub struct DiffSplitCmd;

impl HostCmd<App> for DiffSplitCmd {
    fn name(&self) -> &'static str {
        "diffsplit"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &[]
    }

    fn min_prefix(&self) -> usize {
        5
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::Path
    }

    fn run(&self, app: &mut App, args: &str) -> Option<ExEffect> {
        app.diff_split(args.trim());
        Some(ExEffect::Ok)
    }
}

/// `:diffoff` — remove the focused window from diff mode (#208 Phase 2).
pub struct DiffOffCmd;

impl HostCmd<App> for DiffOffCmd {
    fn name(&self) -> &'static str {
        "diffoff"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &[]
    }

    fn min_prefix(&self) -> usize {
        5
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        app.diff_off();
        Some(ExEffect::Ok)
    }
}

/// `:vnew` — open a vertical split with a fresh empty unnamed buffer.
pub struct VnewCmd;

impl HostCmd<App> for VnewCmd {
    fn name(&self) -> &'static str {
        "vnew"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &[]
    }

    fn min_prefix(&self) -> usize {
        4
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        app.do_vnew();
        Some(ExEffect::Ok)
    }
}

/// `:new` — open a horizontal split with a fresh empty unnamed buffer.
pub struct NewCmd;

impl HostCmd<App> for NewCmd {
    fn name(&self) -> &'static str {
        "new"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &[]
    }

    fn min_prefix(&self) -> usize {
        3
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        app.do_new();
        Some(ExEffect::Ok)
    }
}

/// `:tabfirst` / `:tabrewind` / `:tabr` — jump to the first tab.
pub struct TabfirstCmd;

impl HostCmd<App> for TabfirstCmd {
    fn name(&self) -> &'static str {
        "tabfirst"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &["tabrewind", "tabr"]
    }

    fn min_prefix(&self) -> usize {
        4
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        app.do_tabfirst();
        Some(ExEffect::Ok)
    }
}

/// `:tablast` — jump to the last tab.
pub struct TablastCmd;

impl HostCmd<App> for TablastCmd {
    fn name(&self) -> &'static str {
        "tablast"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &[]
    }

    fn min_prefix(&self) -> usize {
        4
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        app.do_tablast();
        Some(ExEffect::Ok)
    }
}

// ── Phase 4f commands ────────────────────────────────────────────────────────

/// Parse a resize argument string (`"+N"`, `"-N"`, or `"N"`) into an i32 delta.
fn parse_resize_arg(arg: &str) -> Option<i32> {
    let arg = arg.trim();
    if arg.is_empty() {
        return None;
    }
    if let Some(rest) = arg.strip_prefix('+') {
        rest.trim().parse::<i32>().ok()
    } else if let Some(rest) = arg.strip_prefix('-') {
        rest.trim().parse::<i32>().ok().map(|n| -n)
    } else {
        arg.parse::<i32>().ok()
    }
}

/// `:tabonly` / `:tabo` — close all tabs except the current one.
pub struct TabonlyCmd;

impl HostCmd<App> for TabonlyCmd {
    fn name(&self) -> &'static str {
        "tabonly"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &["tabo"]
    }

    /// Vim accepts `:tabo` (4 chars); match that minimum.
    fn min_prefix(&self) -> usize {
        4
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        app.do_tabonly();
        Some(ExEffect::Ok)
    }
}

/// `:tabs` — show an info popup listing all tabs.
pub struct TabsCmd;

impl HostCmd<App> for TabsCmd {
    fn name(&self) -> &'static str {
        "tabs"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &[]
    }

    /// Full name length — no standard abbreviation.
    fn min_prefix(&self) -> usize {
        4
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        app.do_tabs();
        Some(ExEffect::Ok)
    }
}

/// `:resize [+|-]N` — adjust focused window height.
pub struct ResizeCmd;

impl HostCmd<App> for ResizeCmd {
    fn name(&self) -> &'static str {
        "resize"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &[]
    }

    fn min_prefix(&self) -> usize {
        6
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::Raw
    }

    fn run(&self, app: &mut App, args: &str) -> Option<ExEffect> {
        if let Some(delta) = parse_resize_arg(args) {
            app.resize_height(delta);
            Some(ExEffect::Ok)
        } else {
            Some(ExEffect::Error("E: invalid resize argument".into()))
        }
    }
}

/// `:vertical resize [+|-]N` / `:vert res [+|-]N` — adjust focused window width.
///
/// `split_name_args` gives name=`"vertical"` (or `"vert"`), args=`"resize +5"` etc.
/// We strip the leading `resize`/`res` sub-word and parse the delta.
pub struct VerticalResizeCmd;

impl HostCmd<App> for VerticalResizeCmd {
    fn name(&self) -> &'static str {
        "vertical"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &["vert"]
    }

    fn min_prefix(&self) -> usize {
        4
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::Raw
    }

    fn run(&self, app: &mut App, args: &str) -> Option<ExEffect> {
        // args is e.g. "resize +5" or "res +5" or "resize"
        // Strip the mandatory "resize"/"res" sub-word; defer if absent.
        let rest = args
            .strip_prefix("resize")
            .or_else(|| args.strip_prefix("res"))?;
        if let Some(delta) = parse_resize_arg(rest) {
            app.resize_width(delta);
            Some(ExEffect::Ok)
        } else {
            Some(ExEffect::Error("E: invalid resize argument".into()))
        }
    }
}

/// `:Rename <newname>` — LSP symbol rename.
pub struct RenameCmd;

impl HostCmd<App> for RenameCmd {
    fn name(&self) -> &'static str {
        "Rename"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &[]
    }

    /// Full name — uppercase commands have no standard abbreviation.
    fn min_prefix(&self) -> usize {
        6
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::Raw
    }

    fn run(&self, app: &mut App, args: &str) -> Option<ExEffect> {
        let new_name = args.trim().to_string();
        if new_name.is_empty() {
            // TODO: open prompt-based rename UI (Phase 6).
            Some(ExEffect::Error("E: usage: :Rename <newname>".into()))
        } else {
            app.lsp_rename(new_name);
            Some(ExEffect::Ok)
        }
    }
}

/// `:LspFormat` / `:Format` — LSP whole-file format.
pub struct LspFormatCmd;

impl HostCmd<App> for LspFormatCmd {
    fn name(&self) -> &'static str {
        "LspFormat"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &["Format"]
    }

    fn min_prefix(&self) -> usize {
        9
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        // TODO: range formatting when invoked from visual mode (Phase 6).
        app.lsp_format();
        Some(ExEffect::Ok)
    }
}

/// `:GitDiff` — preview the git hunk under the cursor in an info popup (#115).
pub struct GitDiffCmd;

impl HostCmd<App> for GitDiffCmd {
    fn name(&self) -> &'static str {
        "GitDiff"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &[]
    }

    fn min_prefix(&self) -> usize {
        "GitDiff".len()
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        app.git_show_hunk_diff();
        Some(ExEffect::Ok)
    }
}

/// `:GitStage` — stage the git hunk under the cursor into the index (#115).
pub struct GitStageCmd;

impl HostCmd<App> for GitStageCmd {
    fn name(&self) -> &'static str {
        "GitStage"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &[]
    }

    fn min_prefix(&self) -> usize {
        "GitStage".len()
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        app.git_stage_hunk_at_cursor();
        Some(ExEffect::Ok)
    }
}

/// `:GitRevert` — discard the git hunk under the cursor, restoring HEAD (#115).
pub struct GitRevertCmd;

impl HostCmd<App> for GitRevertCmd {
    fn name(&self) -> &'static str {
        "GitRevert"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &[]
    }

    fn min_prefix(&self) -> usize {
        "GitRevert".len()
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        app.git_revert_hunk_at_cursor();
        Some(ExEffect::Ok)
    }
}

/// `:GitUnstage` — unstage the staged git hunk under the cursor from the index
/// (#115).
pub struct GitUnstageCmd;

impl HostCmd<App> for GitUnstageCmd {
    fn name(&self) -> &'static str {
        "GitUnstage"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &[]
    }

    fn min_prefix(&self) -> usize {
        "GitUnstage".len()
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        app.git_unstage_hunk_at_cursor();
        Some(ExEffect::Ok)
    }
}

/// `:GitBlame` — toggle the left-side git blame column for the active buffer (#202).
pub struct GitBlameCmd;

impl HostCmd<App> for GitBlameCmd {
    fn name(&self) -> &'static str {
        "GitBlame"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &[]
    }

    fn min_prefix(&self) -> usize {
        "GitBlame".len()
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        app.toggle_blame_column();
        Some(ExEffect::Ok)
    }
}

/// `:LspCodeAction` / `:CodeAction` — LSP code actions.
pub struct LspCodeActionCmd;

impl HostCmd<App> for LspCodeActionCmd {
    fn name(&self) -> &'static str {
        "LspCodeAction"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &["CodeAction"]
    }

    fn min_prefix(&self) -> usize {
        13
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        app.lsp_code_actions();
        Some(ExEffect::Ok)
    }
}

/// `:lopen` — open the diagnostics picker.
pub struct LopenCmd;

impl HostCmd<App> for LopenCmd {
    fn name(&self) -> &'static str {
        "lopen"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &[]
    }

    fn min_prefix(&self) -> usize {
        5
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        app.open_diag_picker();
        Some(ExEffect::Ok)
    }
}

/// `:lnext` — jump to the next diagnostic.
pub struct LnextCmd;

impl HostCmd<App> for LnextCmd {
    fn name(&self) -> &'static str {
        "lnext"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &[]
    }

    fn min_prefix(&self) -> usize {
        5
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        app.lnext_severity(None);
        Some(ExEffect::Ok)
    }
}

/// `:lprev` — jump to the previous diagnostic.
pub struct LprevCmd;

impl HostCmd<App> for LprevCmd {
    fn name(&self) -> &'static str {
        "lprev"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &[]
    }

    fn min_prefix(&self) -> usize {
        5
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        app.lprev_severity(None);
        Some(ExEffect::Ok)
    }
}

/// `:lfirst` — jump to the first diagnostic.
pub struct LfirstCmd;

impl HostCmd<App> for LfirstCmd {
    fn name(&self) -> &'static str {
        "lfirst"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &[]
    }

    fn min_prefix(&self) -> usize {
        6
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        app.ldiag_first();
        Some(ExEffect::Ok)
    }
}

/// `:llast` — jump to the last diagnostic.
pub struct LlastCmd;

impl HostCmd<App> for LlastCmd {
    fn name(&self) -> &'static str {
        "llast"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &[]
    }

    fn min_prefix(&self) -> usize {
        5
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        app.ldiag_last();
        Some(ExEffect::Ok)
    }
}

/// `:LspInfo` — display LSP server status.
pub struct LspInfoCmd;

impl HostCmd<App> for LspInfoCmd {
    fn name(&self) -> &'static str {
        "LspInfo"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &[]
    }

    fn min_prefix(&self) -> usize {
        7
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        app.show_lsp_info();
        Some(ExEffect::Ok)
    }
}

/// `:Anvil [install|uninstall|update] [name]` — plugin manager.
///
/// `split_name_args` gives name=`"Anvil"`, args=`"install foo"` etc.
///
/// # Security: gated on the shell policy, like `:!`
///
/// Anvil installs download archives and run package-manager build scripts
/// (`cargo`/`npm`/`pip`/`go`), so they are shell-out in disguise: the gate in
/// `run` refuses the whole surface (install/uninstall/update and the no-arg
/// picker, whose selection triggers installs) in `--embed` / `--nvim-api` /
/// `--headless` mode unless the host passed `--allow-shell`. Auditors: see
/// the `:!` gate in `hjkl-ex/src/shell.rs` and the `:make` / `:grep` guards
/// in `quickfix.rs` — same policy, same message.
pub struct AnvilCmd;

impl HostCmd<App> for AnvilCmd {
    fn name(&self) -> &'static str {
        "Anvil"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &[]
    }

    /// Full name — no standard abbreviation.
    fn min_prefix(&self) -> usize {
        5
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::Enum
    }

    /// Subcommands accepted by `run`'s match below.
    fn arg_choices(&self) -> &'static [&'static str] {
        &["install", "uninstall", "update"]
    }

    fn run(&self, app: &mut App, args: &str) -> Option<ExEffect> {
        // Anvil installs are shell-out in disguise (archive download + native
        // build scripts), so gate the whole surface on the shell policy like
        // `:!` / `:make` / `:grep`. Covers install, uninstall, update, and the
        // no-arg picker — its selection triggers installs.
        if hjkl_engine::policy::shell_disabled() {
            return Some(ExEffect::Error(
                "shell commands are disabled in this mode (pass --allow-shell to enable)".into(),
            ));
        }
        let parts: Vec<&str> = args.split_whitespace().collect();
        match parts.as_slice() {
            [] => {
                app.open_anvil_picker();
                Some(ExEffect::Ok)
            }
            ["install", name] => {
                app.anvil_install(name);
                Some(ExEffect::Ok)
            }
            ["uninstall", name] => {
                app.anvil_uninstall(name);
                Some(ExEffect::Ok)
            }
            ["update"] => {
                app.anvil_update_all();
                Some(ExEffect::Ok)
            }
            ["update", name] => {
                app.anvil_update(name);
                Some(ExEffect::Ok)
            }
            _ => Some(ExEffect::Error(
                "usage: :Anvil [install|uninstall|update] [name]".into(),
            )),
        }
    }
}

// ── Notifications command ──────────────────────────────────────────────────────

/// `:notifications` / `:notif` — dump the notification ring as an info popup.
///
/// Newest entry first. Format: `[-HH:MM:SS] [SEVERITY] body`.
pub struct NotificationsCmd;

impl HostCmd<App> for NotificationsCmd {
    fn name(&self) -> &'static str {
        "notifications"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &["notif"]
    }

    fn min_prefix(&self) -> usize {
        5 // "notif"
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::None
    }

    fn run(&self, app: &mut App, _args: &str) -> Option<ExEffect> {
        use std::time::SystemTime;
        let now = SystemTime::now();
        let entries: Vec<String> = app
            .bus
            .history()
            .collect::<Vec<_>>()
            .into_iter()
            .rev()
            .map(|h| {
                let elapsed = now.duration_since(h.ts).map_or_else(
                    |_| "?".to_string(),
                    |d| {
                        let secs = d.as_secs();
                        format!(
                            "-{:02}:{:02}:{:02}",
                            secs / 3600,
                            (secs % 3600) / 60,
                            secs % 60
                        )
                    },
                );
                format!(
                    "[{}] [{}] {}",
                    elapsed,
                    h.severity.label(),
                    h.display_body()
                )
            })
            .collect();
        let content = if entries.is_empty() {
            "(no notifications)".to_string()
        } else {
            entries.join("\n")
        };
        Some(ExEffect::Info(content))
    }
}

/// `:syntax [on|off|enable|disable|...]` — toggle bonsai syntax highlighting.
///
/// `on`/`enable` re-attaches grammars for every open slot's path and triggers
/// a fresh recompute. `off`/`disable` clears installed spans on every slot
/// and short-circuits future parses. Other args (e.g. `:syntax sync`,
/// `:syntax clear`) are accepted as no-ops for vim parity. Bare `:syntax`
/// reports current state.
pub struct SyntaxCmd;

impl HostCmd<App> for SyntaxCmd {
    fn name(&self) -> &'static str {
        "syntax"
    }

    fn aliases(&self) -> &'static [&'static str] {
        &[]
    }

    /// vim COMMAND_NAMES lists `:syntax` with abbrev `:syn`; match that.
    fn min_prefix(&self) -> usize {
        3
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::Enum
    }

    /// The meaningful subcommands `run` matches below (other args are accepted
    /// as vim-permissive no-ops but aren't offered for completion).
    fn arg_choices(&self) -> &'static [&'static str] {
        &["disable", "enable", "off", "on"]
    }

    fn run(&self, app: &mut App, args: &str) -> Option<ExEffect> {
        let arg = args.trim();
        if arg.is_empty() {
            let state = if app.syntax_enabled { "ON" } else { "OFF" };
            return Some(ExEffect::Info(format!("syntax: {state}")));
        }
        match arg.to_ascii_lowercase().as_str() {
            "on" | "enable" => {
                app.set_syntax_enabled(true);
                Some(ExEffect::Ok)
            }
            "off" | "disable" => {
                app.set_syntax_enabled(false);
                Some(ExEffect::Ok)
            }
            // Vim-permissive: accept other subcommands (`sync`, `clear`,
            // `reset`, language names, …) as no-ops rather than errors.
            _ => Some(ExEffect::Ok),
        }
    }
}

/// `:colorscheme [name]` / `:colo` — switch the active theme (UI chrome +
/// syntax). Any registered scheme (`theme::bundled_theme_names()`:
/// dark/light/tokyonight/catppuccin/gruvbox/nord/dracula/onedark) is accepted;
/// unknown names → `E185`. Bare `:colorscheme` (or `?`) reports the current
/// scheme.
pub struct ColorschemeCmd;

impl HostCmd<App> for ColorschemeCmd {
    fn name(&self) -> &'static str {
        "colorscheme"
    }

    /// Vim accepts `:colo` (4 chars) as the minimum abbreviation; matching that
    /// covers `:colo`/`:color`/`:colors`/`:colorsc` without explicit aliases.
    fn min_prefix(&self) -> usize {
        4
    }

    fn arg_kind(&self) -> ArgKind {
        ArgKind::Colorscheme
    }

    fn run(&self, app: &mut App, args: &str) -> Option<ExEffect> {
        let arg = args.split_whitespace().next().unwrap_or("");
        match arg {
            "" | "?" => {
                let cur = app.colorscheme.clone();
                app.bus.info(format!("colorscheme {cur}"));
            }
            name if crate::theme::load_named(name).is_some() => {
                app.apply_colorscheme(name);
                app.bus.info(format!("colorscheme {name}"));
            }
            other => {
                let avail = crate::theme::bundled_theme_names().join(", ");
                app.bus.error(format!(
                    "E185: cannot find colorscheme '{other}' (available: {avail})"
                ));
            }
        }
        Some(ExEffect::Ok)
    }
}

// ── Registry ─────────────────────────────────────────────────────────────────

fn build_registry() -> hjkl_ex::HostRegistry<App> {
    let mut reg = hjkl_ex::HostRegistry::new();
    // Phase 4a
    reg.add(Box::new(TabNextCmd));
    // Phase 4b
    reg.add(Box::new(SplitCmd));
    reg.add(Box::new(VsplitCmd));
    reg.add(Box::new(CloseCmd));
    reg.add(Box::new(TabnewCmd));
    reg.add(Box::new(TabprevCmd));
    reg.add(Box::new(TabcloseCmd));
    reg.add(Box::new(TabmoveCmd));
    reg.add(Box::new(OnlyCmd));
    // Phase 4c
    reg.add(Box::new(BnextCmd));
    reg.add(Box::new(BprevCmd));
    reg.add(Box::new(BfirstCmd));
    reg.add(Box::new(BlastCmd));
    reg.add(Box::new(BuffersCmd));
    reg.add(Box::new(ClipboardCmd));
    // Phase 4d2
    reg.add(Box::new(PickerCmd));
    reg.add(Box::new(RgCmd));
    reg.add(Box::new(BCmd));
    reg.add(Box::new(BpickerCmd));
    reg.add(Box::new(ChecktimeCmd));
    reg.add(Box::new(DiffOrigCmd));
    reg.add(Box::new(DiffThisCmd));
    reg.add(Box::new(DiffSplitCmd));
    reg.add(Box::new(DiffOffCmd));
    reg.add(Box::new(VnewCmd));
    reg.add(Box::new(NewCmd));
    reg.add(Box::new(TabfirstCmd));
    reg.add(Box::new(TablastCmd));
    // Phase 4f
    reg.add(Box::new(TabonlyCmd));
    reg.add(Box::new(TabsCmd));
    reg.add(Box::new(ResizeCmd));
    reg.add(Box::new(VerticalResizeCmd));
    reg.add(Box::new(RenameCmd));
    reg.add(Box::new(LspFormatCmd));
    reg.add(Box::new(GitDiffCmd));
    reg.add(Box::new(GitStageCmd));
    reg.add(Box::new(GitRevertCmd));
    reg.add(Box::new(GitUnstageCmd));
    reg.add(Box::new(GitBlameCmd));
    reg.add(Box::new(LspCodeActionCmd));
    reg.add(Box::new(LopenCmd));
    reg.add(Box::new(LnextCmd));
    reg.add(Box::new(LprevCmd));
    reg.add(Box::new(LfirstCmd));
    reg.add(Box::new(LlastCmd));
    reg.add(Box::new(LspInfoCmd));
    reg.add(Box::new(AnvilCmd));
    reg.add(Box::new(NotificationsCmd));
    reg.add(Box::new(SyntaxCmd));
    reg.add(Box::new(ColorschemeCmd));
    reg
}

/// Static host registry — built once on first access, reused for every dispatch.
static HOST_REGISTRY: LazyLock<hjkl_ex::HostRegistry<App>> = LazyLock::new(build_registry);

/// Return a reference to the static host registry.
pub fn host_registry() -> &'static hjkl_ex::HostRegistry<App> {
    &HOST_REGISTRY
}
