//! Editable, shell-free `:` command input over the shared storage command contract.

use std::{error::Error, fmt};

use ironstorage::command::{CliAction, CommandRequest, GitRequest, OtpRequest, parse_from};

use crate::action::{ACTIONS, Action};

const HISTORY_LIMIT: usize = 100;

const ROOT_COMMANDS: &[&str] = &[
    "init",
    "list",
    "show",
    "find",
    "grep",
    "insert",
    "edit",
    "generate",
    "remove",
    "move",
    "copy",
    "import-kdbx",
    "git",
    "otp",
    "lock",
    "unlock",
    "help",
    "version",
    "quit",
];
const GIT_COMMANDS: &[&str] = &[
    "init",
    "status",
    "log",
    "diff",
    "add",
    "commit",
    "remote",
    "config",
    "fetch",
    "pull",
    "push",
    "sync",
    "resolve-local",
    "resolve-remote",
];
const GIT_REMOTE_COMMANDS: &[&str] = &["get-url", "add", "set-url", "remove"];
const OTP_COMMANDS: &[&str] = &["code", "insert", "append", "uri", "validate", "version"];
const HELP_TOPICS: &[&str] = &[
    "init",
    "list",
    "show",
    "find",
    "grep",
    "insert",
    "edit",
    "generate",
    "remove",
    "move",
    "copy",
    "import-kdbx",
    "git",
    "otp",
];

/// Auditable mapping from every milestone-01 command family to the TUI command surface.
pub const COMMAND_COVERAGE: &[CommandCoverage] = &[
    coverage("initialize recipients", ":init GPG-ID…"),
    coverage("list a directory", ":list [PATH]"),
    coverage("show an entry", ":show [OPTIONS] [ENTRY]"),
    coverage("find names", ":find TERM…"),
    coverage("grep decrypted entries", ":grep [OPTIONS] PATTERN"),
    coverage("insert an entry", ":insert [OPTIONS] ENTRY"),
    coverage("edit an entry", ":edit ENTRY"),
    coverage("generate a password", ":generate [OPTIONS] ENTRY [LENGTH]"),
    coverage("remove entries or directories", ":remove [OPTIONS] ENTRY"),
    coverage(
        "move entries or directories",
        ":move [OPTIONS] SOURCE DESTINATION",
    ),
    coverage(
        "copy entries or directories",
        ":copy [OPTIONS] SOURCE DESTINATION",
    ),
    coverage("import a KeePass database", ":import-kdbx [OPTIONS] SOURCE"),
    coverage("initialize Git", ":git init"),
    coverage("show Git status", ":git status"),
    coverage("show Git log", ":git log [OPTIONS]"),
    coverage("show Git diff", ":git diff [PATH]…"),
    coverage("stage Git paths", ":git add PATH…"),
    coverage("create a Git commit", ":git commit -m MESSAGE"),
    coverage("manage Git remotes", ":git remote [COMMAND]"),
    coverage(
        "manage Git configuration",
        ":git config (--get KEY | KEY VALUE)",
    ),
    coverage("fetch Git remote", ":git fetch [REMOTE]"),
    coverage("pull Git remote", ":git pull [REMOTE] [BRANCH]"),
    coverage("push Git remote", ":git push [REMOTE] [BRANCH]"),
    coverage("synchronize Git remote", ":git sync [REMOTE]"),
    coverage("resolve Git conflicts locally", ":git resolve-local"),
    coverage("resolve Git conflicts remotely", ":git resolve-remote"),
    coverage("generate an OTP code", ":otp code [OPTIONS] ENTRY"),
    coverage("insert an OTP entry", ":otp insert [OPTIONS] [ENTRY]"),
    coverage("append OTP data", ":otp append [OPTIONS] ENTRY"),
    coverage("present an OTP URI", ":otp uri [OPTIONS] ENTRY"),
    coverage("validate an OTP URI", ":otp validate URI"),
    coverage("show pass-otp version", ":otp version"),
    coverage("show help", ":help [TOPIC]"),
    coverage("show version", ":version"),
    coverage("lock the TUI", ":lock"),
    coverage("unlock the TUI", ":unlock"),
];

/// Executable feature matrix for the complete upstream pass and pass-otp
/// surface. `None` means the command prompt is the intentional keyboard UI;
/// otherwise the action registry supplies the default hotkey and help entry.
pub const TUI_COVERAGE: &[TuiCoverage] = &[
    tui(
        "initialize recipients",
        "recipient form",
        Some(Action::Initialize),
        ":init GPG-ID…",
    ),
    tui(
        "list a directory",
        "password tree",
        Some(Action::Activate),
        ":list [PATH]",
    ),
    tui(
        "show an entry",
        "structured viewer",
        Some(Action::Activate),
        ":show [ENTRY]",
    ),
    tui(
        "copy a selected entry line",
        "clipboard feedback",
        Some(Action::Copy),
        ":show --clip [LINE] ENTRY",
    ),
    tui(
        "show a selected entry line as QR",
        "resizable QR popup",
        None,
        ":show --qrcode [LINE] ENTRY",
    ),
    tui(
        "find names",
        "incremental tree filter",
        Some(Action::Filter),
        ":find TERM…",
    ),
    tui(
        "grep decrypted entries",
        "decrypted result pane",
        Some(Action::Grep),
        ":grep [OPTIONS] PATTERN",
    ),
    tui(
        "insert an entry",
        "masked insert form",
        Some(Action::InsertEntry),
        ":insert [OPTIONS] ENTRY",
    ),
    tui(
        "edit an entry",
        "structured editor",
        Some(Action::EditEntry),
        ":edit ENTRY",
    ),
    tui(
        "generate a password",
        "generation form",
        Some(Action::GenerateEntry),
        ":generate [OPTIONS] ENTRY [LENGTH]",
    ),
    tui(
        "present a generated password",
        "generation presentation selector",
        Some(Action::GenerateEntry),
        ":generate (--clip | --qrcode) ENTRY [LENGTH]",
    ),
    tui(
        "remove entries or directories",
        "confirmed removal form",
        Some(Action::RemoveEntry),
        ":remove [OPTIONS] ENTRY",
    ),
    tui(
        "move entries or directories",
        "move form",
        Some(Action::MoveEntry),
        ":move [OPTIONS] SOURCE DESTINATION",
    ),
    tui(
        "copy entries or directories",
        "copy form",
        Some(Action::CopyEntry),
        ":copy [OPTIONS] SOURCE DESTINATION",
    ),
    tui(
        "import a KeePass database",
        "KDBX import form",
        Some(Action::ImportKdbx),
        ":import-kdbx [OPTIONS] SOURCE",
    ),
    tui("initialize Git", "Git dashboard", None, ":git init"),
    tui("show Git status", "Git dashboard", None, ":git status"),
    tui("show Git log", "Git dashboard", None, ":git log [OPTIONS]"),
    tui(
        "show Git diff",
        "zeroizing Git detail pane",
        None,
        ":git diff [PATH]…",
    ),
    tui("stage Git paths", "Git dashboard", None, ":git add PATH…"),
    tui(
        "create a Git commit",
        "Git dashboard",
        None,
        ":git commit -m MESSAGE",
    ),
    tui(
        "manage Git remotes",
        "Git dashboard",
        None,
        ":git remote [COMMAND]",
    ),
    tui(
        "manage Git configuration",
        "Git dashboard",
        None,
        ":git config (--get KEY | KEY VALUE)",
    ),
    tui(
        "fetch Git remote",
        "Git progress view",
        None,
        ":git fetch [REMOTE]",
    ),
    tui(
        "pull Git remote",
        "Git progress view",
        Some(Action::GitPull),
        ":git pull [REMOTE] [BRANCH]",
    ),
    tui(
        "push Git remote",
        "Git progress view",
        Some(Action::GitPush),
        ":git push [REMOTE] [BRANCH]",
    ),
    tui(
        "synchronize Git remote",
        "Git progress view",
        None,
        ":git sync [REMOTE]",
    ),
    tui(
        "resolve Git conflicts locally",
        "Git conflict view",
        None,
        ":git resolve-local",
    ),
    tui(
        "resolve Git conflicts remotely",
        "Git conflict view",
        None,
        ":git resolve-remote",
    ),
    tui(
        "generate an OTP code",
        "focused OTP field",
        Some(Action::OtpCode),
        ":otp code [OPTIONS] ENTRY",
    ),
    tui(
        "copy an OTP code",
        "clipboard feedback",
        Some(Action::OtpCopyCode),
        ":otp code --clip ENTRY",
    ),
    tui(
        "insert an OTP entry",
        "masked OTP insert form",
        Some(Action::OtpInsert),
        ":otp insert [OPTIONS] [ENTRY]",
    ),
    tui(
        "append OTP data",
        "masked OTP append form",
        Some(Action::OtpAppend),
        ":otp append [OPTIONS] ENTRY",
    ),
    tui(
        "present an OTP URI",
        "secret URI popup",
        Some(Action::OtpUri),
        ":otp uri ENTRY",
    ),
    tui(
        "copy an OTP URI",
        "clipboard feedback",
        Some(Action::OtpCopyUri),
        ":otp uri --clip ENTRY",
    ),
    tui(
        "present an OTP URI as QR",
        "resizable QR popup",
        Some(Action::OtpQr),
        ":otp uri --qrcode ENTRY",
    ),
    tui(
        "validate an OTP URI",
        "masked validation form",
        Some(Action::OtpValidate),
        ":otp validate URI",
    ),
    tui("show pass-otp version", "status line", None, ":otp version"),
    tui(
        "show help",
        "contextual help overlay",
        Some(Action::Help),
        ":help [TOPIC]",
    ),
    tui("show version", "status line", None, ":version"),
    tui("lock the TUI", "locked screen", Some(Action::Lock), ":lock"),
    tui(
        "unlock the TUI",
        "locked screen",
        Some(Action::Unlock),
        ":unlock",
    ),
];

const fn tui(
    operation: &'static str,
    element: &'static str,
    action: Option<Action>,
    command: &'static str,
) -> TuiCoverage {
    TuiCoverage {
        operation,
        element,
        action,
        command,
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TuiCoverage {
    pub operation: &'static str,
    pub element: &'static str,
    pub action: Option<Action>,
    pub command: &'static str,
}

impl TuiCoverage {
    pub fn default_hotkey(self) -> Option<&'static str> {
        let action = self.action?;
        ACTIONS
            .iter()
            .find(|spec| spec.action == action)
            .and_then(|spec| spec.bindings.first())
            .map(|binding| binding.display)
    }
}

const fn coverage(operation: &'static str, command: &'static str) -> CommandCoverage {
    CommandCoverage { operation, command }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct CommandCoverage {
    pub operation: &'static str,
    pub command: &'static str,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CommandInvocation {
    Storage(CommandRequest),
    Display(String),
    Lock,
    Unlock,
    Quit,
    ResolveGit(ironstorage::git::GitConflictChoice),
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CommandError {
    message: String,
}

impl CommandError {
    fn new(message: impl Into<String>) -> Self {
        Self {
            message: message.into(),
        }
    }
}

impl fmt::Display for CommandError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(&self.message)
    }
}

impl Error for CommandError {}

#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct CommandLine {
    input: String,
    cursor: usize,
    history: Vec<String>,
    history_position: Option<usize>,
    history_draft: String,
    completion: Option<CompletionCycle>,
}

#[derive(Clone, Debug, Eq, PartialEq)]
struct CompletionCycle {
    start: usize,
    end: usize,
    candidates: Vec<String>,
    selected: usize,
}

impl CommandLine {
    pub fn input(&self) -> &str {
        &self.input
    }

    pub fn cursor(&self) -> usize {
        self.cursor
    }

    pub fn history(&self) -> &[String] {
        &self.history
    }

    pub fn display(&self) -> String {
        if is_secret_bearing(&self.input) {
            let command = tokenize(&self.input)
                .ok()
                .and_then(|tokens| {
                    let keep = if tokens.first().is_some_and(|token| token == "otp") {
                        2
                    } else {
                        1
                    };
                    (!tokens.is_empty()).then(|| {
                        format!(
                            "{} [hidden secret-bearing arguments]",
                            tokens[..tokens.len().min(keep)].join(" ")
                        )
                    })
                })
                .unwrap_or_else(|| "[hidden secret-bearing command]".to_owned());
            return command;
        }
        let mut displayed = self.input.clone();
        displayed.insert(self.cursor, '▏');
        displayed
    }

    pub fn clear(&mut self) {
        self.input.clear();
        self.cursor = 0;
        self.history_position = None;
        self.history_draft.clear();
        self.completion = None;
    }

    pub fn insert(&mut self, character: char) {
        if character.is_control() {
            return;
        }
        self.reset_navigation();
        self.input.insert(self.cursor, character);
        self.cursor += character.len_utf8();
    }

    pub fn backspace(&mut self) {
        self.reset_navigation();
        if let Some(previous) = previous_boundary(&self.input, self.cursor) {
            self.input.drain(previous..self.cursor);
            self.cursor = previous;
        }
    }

    pub fn delete(&mut self) {
        self.reset_navigation();
        if let Some(next) = next_boundary(&self.input, self.cursor) {
            self.input.drain(self.cursor..next);
        }
    }

    pub fn move_left(&mut self) {
        self.completion = None;
        if let Some(previous) = previous_boundary(&self.input, self.cursor) {
            self.cursor = previous;
        }
    }

    pub fn move_right(&mut self) {
        self.completion = None;
        if let Some(next) = next_boundary(&self.input, self.cursor) {
            self.cursor = next;
        }
    }

    pub fn move_home(&mut self) {
        self.completion = None;
        self.cursor = 0;
    }

    pub fn move_end(&mut self) {
        self.completion = None;
        self.cursor = self.input.len();
    }

    pub fn previous_history(&mut self) {
        self.completion = None;
        if self.history.is_empty() {
            return;
        }
        let position = match self.history_position {
            None => {
                self.history_draft.clone_from(&self.input);
                self.history.len() - 1
            }
            Some(position) => position.saturating_sub(1),
        };
        self.history_position = Some(position);
        self.input.clone_from(&self.history[position]);
        self.cursor = self.input.len();
    }

    pub fn next_history(&mut self) {
        self.completion = None;
        let Some(position) = self.history_position else {
            return;
        };
        if position + 1 < self.history.len() {
            self.history_position = Some(position + 1);
            self.input.clone_from(&self.history[position + 1]);
        } else {
            self.history_position = None;
            self.input.clone_from(&self.history_draft);
        }
        self.cursor = self.input.len();
    }

    pub fn complete(&mut self, paths: &[String], reverse: bool) -> bool {
        if let Some(cycle) = self.completion.as_mut() {
            cycle.selected = if reverse {
                cycle
                    .selected
                    .checked_sub(1)
                    .unwrap_or(cycle.candidates.len() - 1)
            } else {
                (cycle.selected + 1) % cycle.candidates.len()
            };
            let replacement = cycle.candidates[cycle.selected].clone();
            self.input
                .replace_range(cycle.start..cycle.end, &replacement);
            cycle.end = cycle.start + replacement.len();
            self.cursor = cycle.end;
            return true;
        }

        let fragment = completion_fragment(&self.input, self.cursor);
        let preceding = tokenize(&self.input[..fragment.start]).unwrap_or_default();
        let candidates = completion_candidates(&preceding, &fragment.value, paths)
            .into_iter()
            .map(|candidate| quote_argument(&candidate))
            .collect::<Vec<_>>();
        if candidates.is_empty() {
            return false;
        }
        let selected = if reverse { candidates.len() - 1 } else { 0 };
        let replacement = if candidates.len() == 1 {
            format!("{} ", candidates[0])
        } else {
            candidates[selected].clone()
        };
        self.input
            .replace_range(fragment.start..fragment.end, &replacement);
        self.cursor = fragment.start + replacement.len();
        if candidates.len() > 1 {
            self.completion = Some(CompletionCycle {
                start: fragment.start,
                end: self.cursor,
                candidates,
                selected,
            });
        }
        true
    }

    pub fn submit(&mut self) -> Result<CommandInvocation, CommandError> {
        let raw = self.input.trim().to_owned();
        let result = parse_command(&raw);
        if !raw.is_empty() && !is_secret_bearing(&raw) && self.history.last() != Some(&raw) {
            self.history.push(raw);
            if self.history.len() > HISTORY_LIMIT {
                self.history.remove(0);
            }
        }
        self.clear();
        result
    }

    fn reset_navigation(&mut self) {
        self.history_position = None;
        self.history_draft.clear();
        self.completion = None;
    }
}

pub fn parse_command(input: &str) -> Result<CommandInvocation, CommandError> {
    let input = input.trim().strip_prefix(':').unwrap_or(input.trim());
    if input.is_empty() {
        return Err(CommandError::new("command is empty"));
    }
    let sensitive = is_secret_bearing(input);
    let tokens = tokenize(input).map_err(|error| {
        if sensitive {
            CommandError::new("invalid secret-bearing command; arguments omitted")
        } else {
            error
        }
    })?;
    let Some(command) = tokens.first().map(String::as_str) else {
        return Err(CommandError::new("command is empty"));
    };
    if tokens.as_slice() == ["git", "resolve-local"] {
        return Ok(CommandInvocation::ResolveGit(
            ironstorage::git::GitConflictChoice::Local,
        ));
    }
    if tokens.as_slice() == ["git", "resolve-remote"] {
        return Ok(CommandInvocation::ResolveGit(
            ironstorage::git::GitConflictChoice::Remote,
        ));
    }
    let builtin = match command {
        "lock" => Some(CommandInvocation::Lock),
        "unlock" => Some(CommandInvocation::Unlock),
        "quit" | "q" => Some(CommandInvocation::Quit),
        "git-resolve-local" => Some(CommandInvocation::ResolveGit(
            ironstorage::git::GitConflictChoice::Local,
        )),
        "git-resolve-remote" => Some(CommandInvocation::ResolveGit(
            ironstorage::git::GitConflictChoice::Remote,
        )),
        _ => None,
    };
    if let Some(invocation) = builtin {
        if tokens.len() != 1 {
            return Err(CommandError::new(format!(
                ":{command} does not accept arguments"
            )));
        }
        return Ok(invocation);
    }

    let arguments = std::iter::once("ironstorage".to_owned()).chain(tokens);
    match parse_from(arguments) {
        Ok(CliAction::Display(text)) => Ok(CommandInvocation::Display(text)),
        Ok(CliAction::Run(invocation)) => {
            let (config, request) = invocation.into_parts();
            if config.is_some() {
                return Err(CommandError::new(
                    "the TUI uses its loaded configuration; --config is unavailable in command mode",
                ));
            }
            if matches!(request, CommandRequest::Completion { .. }) {
                return Err(CommandError::new(
                    "shell completion scripts are unavailable inside the interactive TUI",
                ));
            }
            Ok(CommandInvocation::Storage(request))
        }
        Err(_error) if sensitive => Err(CommandError::new(
            "invalid secret-bearing command; arguments omitted",
        )),
        Err(error) => Err(CommandError::new(error.to_string().trim().to_owned())),
    }
}

pub fn operation_name(request: &CommandRequest) -> &'static str {
    match request {
        CommandRequest::Init(_) => "initialization",
        CommandRequest::List(_) => "list",
        CommandRequest::Show(_) => "show",
        CommandRequest::Find(_) => "find",
        CommandRequest::Grep(_) => "grep",
        CommandRequest::Insert(_) => "insert",
        CommandRequest::Edit(_) => "edit",
        CommandRequest::Generate(_) => "generate",
        CommandRequest::Remove(_) => "remove",
        CommandRequest::Move(_) => "move",
        CommandRequest::Copy(_) => "copy",
        CommandRequest::ImportKdbx(_) => "KDBX import",
        CommandRequest::Git(request) => match request {
            GitRequest::Init => "Git init",
            GitRequest::Status => "Git status",
            GitRequest::Log { .. } => "Git log",
            GitRequest::Diff { .. } => "Git diff",
            GitRequest::Add { .. } => "Git add",
            GitRequest::Commit { .. } => "Git commit",
            GitRequest::Remote(_) => "Git remote",
            GitRequest::Config(_) => "Git config",
            GitRequest::Fetch { .. } => "Git fetch",
            GitRequest::Pull { .. } => "Git pull",
            GitRequest::Push { .. } => "Git push",
            GitRequest::Sync { .. } => "Git sync",
        },
        CommandRequest::Otp(request) => match request {
            OtpRequest::Code(_) => "OTP code",
            OtpRequest::Insert(_) => "OTP insert",
            OtpRequest::Append(_) => "OTP append",
            OtpRequest::Uri(_) => "OTP URI",
            OtpRequest::Validate { .. } => "OTP validation",
            OtpRequest::Help => "OTP help",
            OtpRequest::Version => "OTP version",
        },
        CommandRequest::Completion { .. } => "completion",
        CommandRequest::Help { .. } => "help",
        CommandRequest::Version => "version",
    }
}

fn tokenize(input: &str) -> Result<Vec<String>, CommandError> {
    #[derive(Clone, Copy, Eq, PartialEq)]
    enum Quote {
        None,
        Single,
        Double,
    }

    let mut tokens = Vec::new();
    let mut token = String::new();
    let mut quote = Quote::None;
    let mut escaped = false;
    let mut started = false;
    for character in input.chars() {
        if escaped {
            token.push(character);
            escaped = false;
            started = true;
            continue;
        }
        match (quote, character) {
            (Quote::None | Quote::Double, '\\') => {
                escaped = true;
                started = true;
            }
            (Quote::None, '\'') => {
                quote = Quote::Single;
                started = true;
            }
            (Quote::Single, '\'') => quote = Quote::None,
            (Quote::None, '"') => {
                quote = Quote::Double;
                started = true;
            }
            (Quote::Double, '"') => quote = Quote::None,
            (Quote::None, character) if character.is_whitespace() => {
                if started {
                    tokens.push(std::mem::take(&mut token));
                    started = false;
                }
            }
            (_, character) => {
                token.push(character);
                started = true;
            }
        }
    }
    if escaped {
        return Err(CommandError::new("command ends with an incomplete escape"));
    }
    if quote != Quote::None {
        return Err(CommandError::new("command contains an unterminated quote"));
    }
    if started {
        tokens.push(token);
    }
    Ok(tokens)
}

#[derive(Debug)]
struct CompletionFragment {
    start: usize,
    end: usize,
    value: String,
}

fn completion_fragment(input: &str, cursor: usize) -> CompletionFragment {
    let mut start = 0;
    let mut quote = None;
    let mut escaped = false;
    for (index, character) in input[..cursor].char_indices() {
        if escaped {
            escaped = false;
            continue;
        }
        match (quote, character) {
            (Some('"'), '\\') | (None, '\\') => escaped = true,
            (None, '\'' | '"') => quote = Some(character),
            (Some(active), character) if active == character => quote = None,
            (None, character) if character.is_whitespace() => {
                start = index + character.len_utf8();
            }
            _ => {}
        }
    }
    let raw = &input[start..cursor];
    let value = tokenize(raw)
        .ok()
        .and_then(|mut tokens| tokens.pop())
        .unwrap_or_else(|| raw.trim_start_matches(['\'', '"']).replace("\\ ", " "));
    CompletionFragment {
        start,
        end: cursor,
        value,
    }
}

fn completion_candidates(preceding: &[String], fragment: &str, paths: &[String]) -> Vec<String> {
    let candidates: &[&str] = match preceding {
        [] => ROOT_COMMANDS,
        [command] if command == "git" => GIT_COMMANDS,
        [command, remote] if command == "git" && remote == "remote" => GIT_REMOTE_COMMANDS,
        [command] if command == "otp" => OTP_COMMANDS,
        [command] if command == "help" => HELP_TOPICS,
        [command, ..] if command == "init" && fragment.starts_with('-') => &["--path"],
        [command, ..] if command == "show" && fragment.starts_with('-') => &["--clip", "--qrcode"],
        [command, ..] if command == "grep" && fragment.starts_with('-') => &[
            "--ignore-case",
            "--invert-match",
            "--line-number",
            "--fixed-strings",
        ],
        [command, ..] if command == "generate" && fragment.starts_with('-') => &[
            "--no-symbols",
            "--clip",
            "--qrcode",
            "--in-place",
            "--force",
        ],
        [command, ..] if command == "insert" && fragment.starts_with('-') => {
            &["--echo", "--multiline", "--force"]
        }
        [command, ..] if command == "remove" && fragment.starts_with('-') => {
            &["--recursive", "--force"]
        }
        [command, ..]
            if matches!(command.as_str(), "move" | "copy") && fragment.starts_with('-') =>
        {
            &["--force"]
        }
        [git, subcommand, ..]
            if git == "git" && subcommand == "log" && fragment.starts_with('-') =>
        {
            &["--max-count"]
        }
        [git, subcommand, ..]
            if git == "git" && subcommand == "commit" && fragment.starts_with('-') =>
        {
            &["--message"]
        }
        [git, subcommand, ..]
            if git == "git" && subcommand == "config" && fragment.starts_with('-') =>
        {
            &["--get"]
        }
        [otp, subcommand, ..]
            if otp == "otp" && subcommand == "code" && fragment.starts_with('-') =>
        {
            &["--clip"]
        }
        [otp, subcommand, ..]
            if otp == "otp"
                && matches!(subcommand.as_str(), "insert" | "append")
                && fragment.starts_with('-') =>
        {
            &["--force", "--echo", "--secret", "--issuer", "--account"]
        }
        [otp, subcommand, ..]
            if otp == "otp" && subcommand == "uri" && fragment.starts_with('-') =>
        {
            &["--clip", "--qrcode"]
        }
        _ => &[],
    };
    let mut matches = candidates
        .iter()
        .filter(|candidate| candidate.starts_with(fragment))
        .map(|candidate| (*candidate).to_owned())
        .collect::<Vec<_>>();
    if candidates.is_empty() && command_accepts_path(preceding) {
        matches.extend(
            paths
                .iter()
                .filter(|path| path.starts_with(fragment))
                .cloned(),
        );
    }
    matches.sort();
    matches.dedup();
    matches
}

fn command_accepts_path(tokens: &[String]) -> bool {
    matches!(
        tokens.first().map(String::as_str),
        Some(
            "list"
                | "ls"
                | "show"
                | "insert"
                | "edit"
                | "generate"
                | "remove"
                | "rm"
                | "move"
                | "mv"
                | "copy"
                | "cp"
        )
    ) || matches!(
        tokens,
        [command, subcommand, ..]
            if (command == "otp"
                && matches!(subcommand.as_str(), "code" | "show" | "insert" | "append" | "uri"))
                || (command == "git" && matches!(subcommand.as_str(), "diff" | "add"))
    )
}

fn quote_argument(argument: &str) -> String {
    if !argument.is_empty()
        && argument
            .chars()
            .all(|character| character.is_alphanumeric() || "/._-:@".contains(character))
    {
        return argument.to_owned();
    }
    format!("'{}'", argument.replace('\'', "'\\''"))
}

fn is_secret_bearing(input: &str) -> bool {
    let lowercase = input.to_ascii_lowercase();
    if lowercase.contains("otpauth://") {
        return true;
    }
    let tokens = tokenize(input).unwrap_or_else(|_| {
        input
            .split_whitespace()
            .map(str::to_owned)
            .collect::<Vec<_>>()
    });
    matches!(tokens.as_slice(), [otp, validate, ..] if otp == "otp" && validate == "validate")
        || matches!(tokens.as_slice(), [git, config, ..] if git == "git" && config == "config")
        || matches!(tokens.as_slice(), [git, remote, action, ..]
            if git == "git" && remote == "remote" && matches!(action.as_str(), "add" | "set-url"))
}

fn previous_boundary(value: &str, cursor: usize) -> Option<usize> {
    value[..cursor]
        .char_indices()
        .next_back()
        .map(|(index, _)| index)
}

fn next_boundary(value: &str, cursor: usize) -> Option<usize> {
    value[cursor..].char_indices().nth(1).map_or_else(
        || (cursor < value.len()).then_some(value.len()),
        |(index, _)| Some(cursor + index),
    )
}

#[cfg(test)]
mod tests {
    use super::*;
    use ironstorage::command::{GitRemoteRequest, RemoveRequest};

    #[test]
    fn quoting_and_paths_with_spaces_use_the_shared_request_parser() {
        assert!(matches!(
            parse_command("show 'team accounts/mail primary'"),
            Ok(CommandInvocation::Storage(CommandRequest::Show(request)))
                if request.entry.as_deref() == Some("team accounts/mail primary")
        ));
        assert!(matches!(
            parse_command("git commit -m \"rotate signing key\""),
            Ok(CommandInvocation::Storage(CommandRequest::Git(GitRequest::Commit { message })))
                if message == "rotate signing key"
        ));
        assert!(parse_command("show 'unfinished").is_err());
    }

    #[test]
    fn editing_history_and_secret_omission_are_unicode_safe() {
        let mut line = CommandLine::default();
        for character in "show café/mail".chars() {
            line.insert(character);
        }
        line.move_left();
        line.backspace();
        line.insert('i');
        assert_eq!(line.input(), "show café/mail");
        assert!(line.submit().is_ok());
        assert_eq!(line.history(), &["show café/mail"]);

        for character in "otp validate otpauth://totp/a?secret=TOPSECRET".chars() {
            line.insert(character);
        }
        assert!(!line.display().contains("TOPSECRET"));
        assert!(line.submit().is_ok());
        assert_eq!(line.history(), &["show café/mail"]);
        line.previous_history();
        assert_eq!(line.input(), "show café/mail");
        line.next_history();
        assert!(line.input().is_empty());
    }

    #[test]
    fn completion_is_context_aware_and_quotes_paths() {
        let paths = vec!["team accounts/mail primary".to_owned()];
        let mut line = CommandLine::default();
        for character in "gi".chars() {
            line.insert(character);
        }
        assert!(line.complete(&paths, false));
        assert_eq!(line.input(), "git ");
        for character in "pu".chars() {
            line.insert(character);
        }
        assert!(line.complete(&paths, false));
        assert!(matches!(line.input(), "git pull" | "git push"));

        line.clear();
        for character in "show team".chars() {
            line.insert(character);
        }
        assert!(line.complete(&paths, false));
        assert_eq!(line.input(), "show 'team accounts/mail primary' ");
    }

    #[test]
    fn invalid_flags_cancellation_inputs_and_unavailable_meta_actions_are_typed() {
        assert!(parse_command("generate --clip --qrcode entry").is_err());
        assert!(
            parse_command("completion bash")
                .expect_err("interactive completion is unavailable")
                .to_string()
                .contains("unavailable")
        );
        assert!(matches!(
            parse_command(":lock"),
            Ok(CommandInvocation::Lock)
        ));
        assert!(matches!(
            parse_command(":unlock"),
            Ok(CommandInvocation::Unlock)
        ));
        assert!(matches!(
            parse_command(":quit"),
            Ok(CommandInvocation::Quit)
        ));
    }

    #[test]
    fn destructive_and_nested_requests_preserve_typed_storage_contracts() {
        assert_eq!(
            parse_command("git resolve-local"),
            Ok(CommandInvocation::ResolveGit(
                ironstorage::git::GitConflictChoice::Local
            ))
        );
        assert_eq!(
            parse_command("git resolve-remote"),
            Ok(CommandInvocation::ResolveGit(
                ironstorage::git::GitConflictChoice::Remote
            ))
        );
        assert_eq!(
            parse_command("remove -r folder"),
            Ok(CommandInvocation::Storage(CommandRequest::Remove(
                RemoveRequest {
                    entry: "folder".to_owned(),
                    recursive: true,
                    force: false,
                }
            )))
        );
        assert!(matches!(
            parse_command("git remote set-url origin https://example.test/repo"),
            Ok(CommandInvocation::Storage(CommandRequest::Git(GitRequest::Remote(
                GitRemoteRequest::SetUrl { name, url }
            )))) if name == "origin" && url == "https://example.test/repo"
        ));
    }

    #[test]
    fn coverage_table_has_every_command_family_and_no_duplicates() {
        assert!(COMMAND_COVERAGE.len() >= 33);
        for (index, row) in COMMAND_COVERAGE.iter().enumerate() {
            assert!(row.command.starts_with(':'));
            assert!(!row.operation.is_empty());
            assert!(!COMMAND_COVERAGE[index + 1..].contains(row));
        }
        for command in [
            ":init",
            ":list",
            ":show",
            ":find",
            ":grep",
            ":insert",
            ":edit",
            ":generate",
            ":remove",
            ":move",
            ":copy",
            ":git",
            ":otp",
            ":lock",
            ":unlock",
            ":help",
            ":version",
        ] {
            assert!(
                COMMAND_COVERAGE
                    .iter()
                    .any(|row| row.command.starts_with(command))
            );
        }

        for command_row in COMMAND_COVERAGE {
            assert!(
                TUI_COVERAGE
                    .iter()
                    .any(|row| row.operation == command_row.operation),
                "missing TUI coverage for {}",
                command_row.operation
            );
        }
        for (index, row) in TUI_COVERAGE.iter().enumerate() {
            assert!(!row.operation.is_empty());
            assert!(!row.element.is_empty());
            assert!(row.command.starts_with(':'));
            assert!(
                !TUI_COVERAGE[index + 1..]
                    .iter()
                    .any(|other| other.operation == row.operation),
                "duplicate TUI operation {}",
                row.operation
            );
            if row.action.is_some() {
                assert!(
                    row.default_hotkey().is_some(),
                    "{} has no registered default hotkey",
                    row.operation
                );
            }
        }
    }

    #[test]
    fn command_mode_source_has_no_process_launch_boundary() {
        let source = include_str!("command.rs");
        let process_type = ["process", "::Command"].concat();
        let constructor = ["Command", "::new("].concat();
        for forbidden in [process_type, constructor] {
            assert!(!source.contains(&forbidden));
        }
        for executable in ["pass ", "git ", "gpg ", "ironstorage "] {
            assert!(!source.contains(&format!("{executable}--")));
        }
    }
}
