//! Complete pass-compatible command request contract.

use std::{error::Error, ffi::OsString, fmt, num::NonZeroUsize, path::PathBuf};

use clap::{Args, CommandFactory, Parser, Subcommand, error::ErrorKind};
use clap_complete::{Shell, generate};

use crate::PRODUCT_NAME;
use crate::kdbx::{KdbxImportMode, KdbxImportRequest};

pub const EXIT_SUCCESS: u8 = 0;
pub const EXIT_FAILURE: u8 = 1;
pub const EXIT_USAGE: u8 = EXIT_FAILURE;
pub const EXIT_UNAVAILABLE: u8 = 69;
pub const EXIT_CONFIG: u8 = 78;

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CliAction {
    Run(CliInvocation),
    Display(String),
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CliInvocation {
    config: Option<PathBuf>,
    request: CommandRequest,
}

impl CliInvocation {
    pub fn config(&self) -> Option<&std::path::Path> {
        self.config.as_deref()
    }

    pub fn request(&self) -> &CommandRequest {
        &self.request
    }

    pub fn into_parts(self) -> (Option<PathBuf>, CommandRequest) {
        (self.config, self.request)
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CommandRequest {
    Init(InitRequest),
    List(ListRequest),
    Show(ShowRequest),
    Find(FindRequest),
    Grep(GrepRequest),
    Insert(InsertRequest),
    Edit(EditRequest),
    Generate(GenerateRequest),
    Remove(RemoveRequest),
    Move(MoveRequest),
    Copy(CopyRequest),
    ImportKdbx(KdbxImportRequest),
    Git(GitRequest),
    Otp(OtpRequest),
    Completion { shell: CompletionShell },
    Help { topic: Option<HelpTopic> },
    Version,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CompletionShell {
    Bash,
    Elvish,
    Fish,
    PowerShell,
    Zsh,
}

impl CompletionShell {
    fn generator(self) -> Shell {
        match self {
            Self::Bash => Shell::Bash,
            Self::Elvish => Shell::Elvish,
            Self::Fish => Shell::Fish,
            Self::PowerShell => Shell::PowerShell,
            Self::Zsh => Shell::Zsh,
        }
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InitRequest {
    pub path: Option<String>,
    pub key_identities: Vec<String>,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ListRequest {
    pub path: Option<String>,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ShowRequest {
    pub entry: Option<String>,
    pub presentation: Presentation,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Presentation {
    Terminal,
    Clipboard { line: NonZeroUsize },
    QrCode { line: NonZeroUsize },
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FindRequest {
    pub terms: Vec<String>,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GrepRequest {
    pub pattern: String,
    pub ignore_case: bool,
    pub invert_match: bool,
    pub line_number: bool,
    pub fixed_strings: bool,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InsertRequest {
    pub entry: String,
    pub input: InsertInput,
    pub force: bool,
}

impl InsertRequest {
    pub fn input_plan(&self, standard_input_is_terminal: bool) -> InputPlan {
        if !standard_input_is_terminal {
            return if self.input == InsertInput::Multiline {
                InputPlan::StandardInputToEnd
            } else {
                InputPlan::StandardInputLine
            };
        }
        match self.input {
            InsertInput::HiddenConfirmed => InputPlan::HiddenConfirmed,
            InsertInput::EchoedLine => InputPlan::EchoedLine,
            InsertInput::Multiline => InputPlan::StandardInputToEnd,
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum InsertInput {
    HiddenConfirmed,
    EchoedLine,
    Multiline,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum InputPlan {
    HiddenConfirmed,
    EchoedLine,
    StandardInputLine,
    StandardInputToEnd,
}

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

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GenerateRequest {
    pub entry: String,
    pub length: Option<NonZeroUsize>,
    pub no_symbols: bool,
    pub force: bool,
    pub in_place: bool,
    pub presentation: GeneratedPresentation,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GeneratedPresentation {
    Terminal,
    Clipboard,
    QrCode,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RemoveRequest {
    pub entry: String,
    pub recursive: bool,
    pub force: bool,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MoveRequest {
    pub source: String,
    pub destination: String,
    pub force: bool,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CopyRequest {
    pub source: String,
    pub destination: String,
    pub force: bool,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum GitRequest {
    Init,
    Status,
    Log {
        maximum: Option<NonZeroUsize>,
    },
    Diff {
        paths: Vec<String>,
    },
    Add {
        paths: Vec<String>,
    },
    Commit {
        message: String,
    },
    Remote(GitRemoteRequest),
    Config(GitConfigRequest),
    Fetch {
        remote: Option<String>,
    },
    Pull {
        remote: Option<String>,
        branch: Option<String>,
    },
    Push {
        remote: Option<String>,
        branch: Option<String>,
    },
    Sync {
        remote: Option<String>,
    },
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum GitRemoteRequest {
    List,
    GetUrl { name: String },
    Add { name: String, url: String },
    SetUrl { name: String, url: String },
    Remove { name: String },
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum GitConfigRequest {
    Get { key: String },
    Set { key: String, value: String },
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum OtpRequest {
    Code(OtpCodeRequest),
    Insert(OtpInsertRequest),
    Append(OtpAppendRequest),
    Uri(OtpUriRequest),
    Validate { uri: String },
    Help,
    Version,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct OtpCodeRequest {
    pub entry: String,
    pub clipboard: bool,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct OtpInsertRequest {
    pub entry: Option<String>,
    pub force: bool,
    pub echo: bool,
    pub source: OtpInputSource,
}

impl OtpInsertRequest {
    pub fn input_plan(&self, standard_input_is_terminal: bool) -> InputPlan {
        line_input_plan(self.echo, standard_input_is_terminal)
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct OtpAppendRequest {
    pub entry: String,
    pub force: bool,
    pub echo: bool,
    pub source: OtpInputSource,
}

impl OtpAppendRequest {
    pub fn input_plan(&self, standard_input_is_terminal: bool) -> InputPlan {
        line_input_plan(self.echo, standard_input_is_terminal)
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum OtpInputSource {
    Uri,
    Secret {
        issuer: Option<String>,
        account: Option<String>,
    },
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct OtpUriRequest {
    pub entry: String,
    pub presentation: OtpUriPresentation,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum OtpUriPresentation {
    Terminal,
    Clipboard,
    QrCode,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum HelpTopic {
    Init,
    List,
    Show,
    Find,
    Grep,
    Insert,
    Edit,
    Generate,
    Remove,
    Move,
    Copy,
    ImportKdbx,
    Git,
    Otp,
}

impl HelpTopic {
    fn parse(value: &str) -> Option<Self> {
        match value {
            "init" => Some(Self::Init),
            "ls" | "list" => Some(Self::List),
            "show" => Some(Self::Show),
            "find" | "search" => Some(Self::Find),
            "grep" => Some(Self::Grep),
            "insert" | "add" => Some(Self::Insert),
            "edit" => Some(Self::Edit),
            "generate" => Some(Self::Generate),
            "rm" | "remove" | "delete" => Some(Self::Remove),
            "mv" | "rename" => Some(Self::Move),
            "cp" | "copy" => Some(Self::Copy),
            "import-kdbx" => Some(Self::ImportKdbx),
            "git" => Some(Self::Git),
            "otp" => Some(Self::Otp),
            _ => None,
        }
    }

    fn command_name(self) -> &'static str {
        match self {
            Self::Init => "init",
            Self::List => "ls",
            Self::Show => "show",
            Self::Find => "find",
            Self::Grep => "grep",
            Self::Insert => "insert",
            Self::Edit => "edit",
            Self::Generate => "generate",
            Self::Remove => "rm",
            Self::Move => "mv",
            Self::Copy => "cp",
            Self::ImportKdbx => "import-kdbx",
            Self::Git => "git",
            Self::Otp => "otp",
        }
    }
}

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

impl CliParseError {
    pub fn exit_code(&self) -> u8 {
        self.exit_code
    }
}

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

impl Error for CliParseError {}

pub fn parse_env() -> Result<CliAction, CliParseError> {
    parse_from(std::env::args_os())
}

pub fn parse_from<I, T>(arguments: I) -> Result<CliAction, CliParseError>
where
    I: IntoIterator<Item = T>,
    T: Into<OsString> + Clone,
{
    let arguments = arguments.into_iter().map(Into::into).collect::<Vec<_>>();
    let arguments = normalize_dispatch(arguments);
    match CliArguments::try_parse_from(arguments) {
        Ok(arguments) => convert_arguments(arguments).map(CliAction::Run),
        Err(error)
            if matches!(
                error.kind(),
                ErrorKind::DisplayHelp | ErrorKind::DisplayVersion
            ) =>
        {
            Ok(CliAction::Display(error.to_string()))
        }
        Err(error) => Err(CliParseError {
            message: error.to_string(),
            exit_code: EXIT_USAGE,
        }),
    }
}

pub fn help_text(topic: Option<HelpTopic>) -> String {
    let mut command = CliArguments::command();
    if let Some(topic) = topic {
        let name = topic.command_name();
        if let Some(subcommand) = command.find_subcommand_mut(name) {
            return subcommand.render_long_help().to_string();
        }
    }
    command.render_long_help().to_string()
}

pub fn version_text() -> String {
    format!("{PRODUCT_NAME} {}\n", env!("CARGO_PKG_VERSION"))
}

/// The observable version string from the pinned pass-otp v1.2.0 source.
pub fn otp_version_text() -> &'static str {
    "1.1.1\n"
}

pub fn completion_script(shell: CompletionShell) -> Vec<u8> {
    let mut command = CliArguments::command();
    let mut output = Vec::new();
    generate(shell.generator(), &mut command, "ironstorage", &mut output);
    output
}

#[derive(Parser)]
#[command(
    name = "ironstorage",
    about = "A pass-compatible password-store client",
    disable_help_subcommand = true,
    version
)]
struct CliArguments {
    #[arg(long, global = true, value_name = "PATH")]
    config: Option<PathBuf>,
    #[command(subcommand)]
    command: Option<CommandArguments>,
}

#[derive(Subcommand)]
enum CommandArguments {
    Init(InitArguments),
    #[command(name = "ls", visible_alias = "list")]
    List(ListArguments),
    Show(ShowArguments),
    #[command(visible_alias = "search")]
    Find(FindArguments),
    Grep(GrepArguments),
    #[command(visible_alias = "add")]
    Insert(InsertArguments),
    Edit(EditArguments),
    Generate(GenerateArguments),
    #[command(name = "rm", visible_aliases = ["remove", "delete"])]
    Remove(RemoveArguments),
    #[command(name = "mv", visible_alias = "rename")]
    Move(MoveArguments),
    #[command(name = "cp", visible_alias = "copy")]
    Copy(CopyArguments),
    #[command(name = "import-kdbx")]
    ImportKdbx(ImportKdbxArguments),
    Git(GitArguments),
    Otp(OtpArguments),
    Completion(CompletionArguments),
    Help(HelpArguments),
    Version,
}

#[derive(Args)]
struct CompletionArguments {
    #[arg(value_enum)]
    shell: CompletionShellArgument,
}

#[derive(Clone, Copy, clap::ValueEnum)]
enum CompletionShellArgument {
    Bash,
    Elvish,
    Fish,
    Powershell,
    Zsh,
}

#[derive(Args)]
struct InitArguments {
    #[arg(short = 'p', long = "path")]
    path: Option<String>,
    #[arg(required = true, num_args = 1.., value_name = "GPG-ID")]
    key_identities: Vec<String>,
}

#[derive(Args)]
struct ListArguments {
    path: Option<String>,
}

#[derive(Args)]
struct ShowArguments {
    #[arg(
        short = 'c',
        long = "clip",
        num_args = 0..=1,
        default_missing_value = "1",
        require_equals = true,
        conflicts_with = "qrcode",
        value_name = "LINE"
    )]
    clip: Option<NonZeroUsize>,
    #[arg(
        short = 'q',
        long = "qrcode",
        num_args = 0..=1,
        default_missing_value = "1",
        require_equals = true,
        conflicts_with = "clip",
        value_name = "LINE"
    )]
    qrcode: Option<NonZeroUsize>,
    entry: Option<String>,
}

#[derive(Args)]
struct FindArguments {
    #[arg(required = true, num_args = 1..)]
    terms: Vec<String>,
}

#[derive(Args)]
struct GrepArguments {
    #[arg(short = 'i', long = "ignore-case")]
    ignore_case: bool,
    #[arg(short = 'v', long = "invert-match")]
    invert_match: bool,
    #[arg(short = 'n', long = "line-number")]
    line_number: bool,
    #[arg(short = 'F', long = "fixed-strings")]
    fixed_strings: bool,
    pattern: String,
}

#[derive(Args)]
struct InsertArguments {
    #[arg(short = 'e', long = "echo", conflicts_with = "multiline")]
    echo: bool,
    #[arg(short = 'm', long = "multiline", conflicts_with = "echo")]
    multiline: bool,
    #[arg(short = 'f', long = "force")]
    force: bool,
    entry: String,
}

#[derive(Args)]
struct EditArguments {
    entry: String,
}

#[derive(Args)]
struct GenerateArguments {
    #[arg(short = 'n', long = "no-symbols")]
    no_symbols: bool,
    #[arg(short = 'c', long = "clip", conflicts_with = "qrcode")]
    clip: bool,
    #[arg(short = 'q', long = "qrcode", conflicts_with = "clip")]
    qrcode: bool,
    #[arg(short = 'i', long = "in-place", conflicts_with = "force")]
    in_place: bool,
    #[arg(short = 'f', long = "force", conflicts_with = "in_place")]
    force: bool,
    entry: String,
    length: Option<NonZeroUsize>,
}

#[derive(Args)]
struct RemoveArguments {
    #[arg(short = 'r', long = "recursive")]
    recursive: bool,
    #[arg(short = 'f', long = "force")]
    force: bool,
    entry: String,
}

#[derive(Args)]
struct MoveArguments {
    #[arg(short = 'f', long = "force")]
    force: bool,
    source: String,
    destination: String,
}

#[derive(Args)]
struct CopyArguments {
    #[arg(short = 'f', long = "force")]
    force: bool,
    source: String,
    destination: String,
}

#[derive(Args)]
struct ImportKdbxArguments {
    source: PathBuf,
    #[arg(long, value_name = "PATH")]
    key_file: Option<PathBuf>,
    #[arg(long)]
    quick_add: bool,
}

#[derive(Args)]
struct GitArguments {
    #[command(subcommand)]
    command: GitCommandArguments,
}

#[derive(Subcommand)]
enum GitCommandArguments {
    Init,
    Status,
    Log {
        #[arg(short = 'n', long = "max-count")]
        maximum: Option<NonZeroUsize>,
    },
    Diff {
        paths: Vec<String>,
    },
    Add {
        #[arg(required = true, num_args = 1..)]
        paths: Vec<String>,
    },
    Commit {
        #[arg(short = 'm', long = "message")]
        message: String,
    },
    Remote {
        #[command(subcommand)]
        command: Option<GitRemoteArguments>,
    },
    Config(GitConfigArguments),
    Fetch {
        remote: Option<String>,
    },
    Pull {
        remote: Option<String>,
        branch: Option<String>,
    },
    Push {
        remote: Option<String>,
        branch: Option<String>,
    },
    Sync {
        remote: Option<String>,
    },
}

#[derive(Subcommand)]
enum GitRemoteArguments {
    GetUrl { name: String },
    Add { name: String, url: String },
    SetUrl { name: String, url: String },
    Remove { name: String },
}

#[derive(Args)]
struct GitConfigArguments {
    #[arg(long, conflicts_with = "values")]
    get: Option<String>,
    #[arg(num_args = 2, value_names = ["KEY", "VALUE"])]
    values: Vec<String>,
}

#[derive(Args)]
struct OtpArguments {
    #[command(subcommand)]
    command: OtpCommandArguments,
}

#[derive(Subcommand)]
enum OtpCommandArguments {
    #[command(visible_alias = "show")]
    Code(OtpCodeArguments),
    #[command(visible_alias = "add")]
    Insert(OtpInsertArguments),
    Append(OtpAppendArguments),
    Uri(OtpUriArguments),
    Validate {
        uri: String,
    },
    Version,
}

#[derive(Args)]
struct OtpCodeArguments {
    #[arg(short = 'c', long = "clip")]
    clipboard: bool,
    entry: String,
}

#[derive(Args)]
struct OtpInsertArguments {
    #[arg(short = 'f', long = "force")]
    force: bool,
    #[arg(short = 'e', long = "echo")]
    echo: bool,
    #[arg(short = 's', long = "secret")]
    secret: bool,
    #[arg(short = 'i', long = "issuer")]
    issuer: Option<String>,
    #[arg(short = 'a', long = "account")]
    account: Option<String>,
    entry: Option<String>,
}

#[derive(Args)]
struct OtpAppendArguments {
    #[arg(short = 'f', long = "force")]
    force: bool,
    #[arg(short = 'e', long = "echo")]
    echo: bool,
    #[arg(short = 's', long = "secret")]
    secret: bool,
    #[arg(short = 'i', long = "issuer")]
    issuer: Option<String>,
    #[arg(short = 'a', long = "account")]
    account: Option<String>,
    entry: String,
}

#[derive(Args)]
struct OtpUriArguments {
    #[arg(short = 'c', long = "clip", conflicts_with = "qrcode")]
    clipboard: bool,
    #[arg(short = 'q', long = "qrcode", conflicts_with = "clipboard")]
    qrcode: bool,
    entry: String,
}

#[derive(Args)]
struct HelpArguments {
    topic: Option<String>,
}

fn convert_arguments(arguments: CliArguments) -> Result<CliInvocation, CliParseError> {
    let request = match arguments.command {
        None => CommandRequest::Show(ShowRequest {
            entry: None,
            presentation: Presentation::Terminal,
        }),
        Some(CommandArguments::Init(arguments)) => CommandRequest::Init(InitRequest {
            path: arguments.path,
            key_identities: arguments.key_identities,
        }),
        Some(CommandArguments::List(arguments)) => CommandRequest::List(ListRequest {
            path: arguments.path,
        }),
        Some(CommandArguments::Show(arguments)) => CommandRequest::Show(ShowRequest {
            entry: arguments.entry,
            presentation: match (arguments.clip, arguments.qrcode) {
                (Some(line), None) => Presentation::Clipboard { line },
                (None, Some(line)) => Presentation::QrCode { line },
                (None, None) => Presentation::Terminal,
                (Some(_), Some(_)) => unreachable!("clap rejects conflicting presentation flags"),
            },
        }),
        Some(CommandArguments::Find(arguments)) => CommandRequest::Find(FindRequest {
            terms: arguments.terms,
        }),
        Some(CommandArguments::Grep(arguments)) => CommandRequest::Grep(GrepRequest {
            pattern: arguments.pattern,
            ignore_case: arguments.ignore_case,
            invert_match: arguments.invert_match,
            line_number: arguments.line_number,
            fixed_strings: arguments.fixed_strings,
        }),
        Some(CommandArguments::Insert(arguments)) => CommandRequest::Insert(InsertRequest {
            entry: arguments.entry,
            input: if arguments.multiline {
                InsertInput::Multiline
            } else if arguments.echo {
                InsertInput::EchoedLine
            } else {
                InsertInput::HiddenConfirmed
            },
            force: arguments.force,
        }),
        Some(CommandArguments::Edit(arguments)) => CommandRequest::Edit(EditRequest {
            entry: arguments.entry,
        }),
        Some(CommandArguments::Generate(arguments)) => CommandRequest::Generate(GenerateRequest {
            entry: arguments.entry,
            length: arguments.length,
            no_symbols: arguments.no_symbols,
            force: arguments.force,
            in_place: arguments.in_place,
            presentation: if arguments.clip {
                GeneratedPresentation::Clipboard
            } else if arguments.qrcode {
                GeneratedPresentation::QrCode
            } else {
                GeneratedPresentation::Terminal
            },
        }),
        Some(CommandArguments::Remove(arguments)) => CommandRequest::Remove(RemoveRequest {
            entry: arguments.entry,
            recursive: arguments.recursive,
            force: arguments.force,
        }),
        Some(CommandArguments::Move(arguments)) => CommandRequest::Move(MoveRequest {
            source: arguments.source,
            destination: arguments.destination,
            force: arguments.force,
        }),
        Some(CommandArguments::Copy(arguments)) => CommandRequest::Copy(CopyRequest {
            source: arguments.source,
            destination: arguments.destination,
            force: arguments.force,
        }),
        Some(CommandArguments::ImportKdbx(arguments)) => {
            CommandRequest::ImportKdbx(KdbxImportRequest::new(
                arguments.source,
                arguments.key_file,
                if arguments.quick_add {
                    KdbxImportMode::QuickAdd
                } else {
                    KdbxImportMode::AddAndUpdate
                },
            ))
        }
        Some(CommandArguments::Git(arguments)) => {
            CommandRequest::Git(convert_git(arguments.command)?)
        }
        Some(CommandArguments::Otp(arguments)) => {
            CommandRequest::Otp(convert_otp(arguments.command)?)
        }
        Some(CommandArguments::Completion(arguments)) => CommandRequest::Completion {
            shell: match arguments.shell {
                CompletionShellArgument::Bash => CompletionShell::Bash,
                CompletionShellArgument::Elvish => CompletionShell::Elvish,
                CompletionShellArgument::Fish => CompletionShell::Fish,
                CompletionShellArgument::Powershell => CompletionShell::PowerShell,
                CompletionShellArgument::Zsh => CompletionShell::Zsh,
            },
        },
        Some(CommandArguments::Help(arguments)) => {
            let topic = arguments
                .topic
                .map(|topic| {
                    HelpTopic::parse(&topic).ok_or_else(|| usage_error("unknown help topic"))
                })
                .transpose()?;
            CommandRequest::Help { topic }
        }
        Some(CommandArguments::Version) => CommandRequest::Version,
    };
    Ok(CliInvocation {
        config: arguments.config,
        request,
    })
}

fn convert_git(arguments: GitCommandArguments) -> Result<GitRequest, CliParseError> {
    Ok(match arguments {
        GitCommandArguments::Init => GitRequest::Init,
        GitCommandArguments::Status => GitRequest::Status,
        GitCommandArguments::Log { maximum } => GitRequest::Log { maximum },
        GitCommandArguments::Diff { paths } => GitRequest::Diff { paths },
        GitCommandArguments::Add { paths } => GitRequest::Add { paths },
        GitCommandArguments::Commit { message } => GitRequest::Commit { message },
        GitCommandArguments::Remote { command } => GitRequest::Remote(match command {
            None => GitRemoteRequest::List,
            Some(GitRemoteArguments::GetUrl { name }) => GitRemoteRequest::GetUrl { name },
            Some(GitRemoteArguments::Add { name, url }) => GitRemoteRequest::Add { name, url },
            Some(GitRemoteArguments::SetUrl { name, url }) => {
                GitRemoteRequest::SetUrl { name, url }
            }
            Some(GitRemoteArguments::Remove { name }) => GitRemoteRequest::Remove { name },
        }),
        GitCommandArguments::Config(arguments) => {
            let request = match (arguments.get, arguments.values.as_slice()) {
                (Some(key), []) => GitConfigRequest::Get { key },
                (None, [key, value]) => GitConfigRequest::Set {
                    key: key.clone(),
                    value: value.clone(),
                },
                _ => return Err(usage_error("git config requires --get KEY or KEY VALUE")),
            };
            GitRequest::Config(request)
        }
        GitCommandArguments::Fetch { remote } => GitRequest::Fetch { remote },
        GitCommandArguments::Pull { remote, branch } => GitRequest::Pull { remote, branch },
        GitCommandArguments::Push { remote, branch } => GitRequest::Push { remote, branch },
        GitCommandArguments::Sync { remote } => GitRequest::Sync { remote },
    })
}

fn convert_otp(arguments: OtpCommandArguments) -> Result<OtpRequest, CliParseError> {
    Ok(match arguments {
        OtpCommandArguments::Code(arguments) => OtpRequest::Code(OtpCodeRequest {
            entry: arguments.entry,
            clipboard: arguments.clipboard,
        }),
        OtpCommandArguments::Insert(arguments) => OtpRequest::Insert(OtpInsertRequest {
            entry: arguments.entry,
            force: arguments.force,
            echo: arguments.echo,
            source: otp_source(arguments.secret, arguments.issuer, arguments.account)?,
        }),
        OtpCommandArguments::Append(arguments) => OtpRequest::Append(OtpAppendRequest {
            entry: arguments.entry,
            force: arguments.force,
            echo: arguments.echo,
            source: otp_source(arguments.secret, arguments.issuer, arguments.account)?,
        }),
        OtpCommandArguments::Uri(arguments) => OtpRequest::Uri(OtpUriRequest {
            entry: arguments.entry,
            presentation: if arguments.clipboard {
                OtpUriPresentation::Clipboard
            } else if arguments.qrcode {
                OtpUriPresentation::QrCode
            } else {
                OtpUriPresentation::Terminal
            },
        }),
        OtpCommandArguments::Validate { uri } => OtpRequest::Validate { uri },
        OtpCommandArguments::Version => OtpRequest::Version,
    })
}

fn otp_source(
    secret: bool,
    issuer: Option<String>,
    account: Option<String>,
) -> Result<OtpInputSource, CliParseError> {
    if secret {
        if issuer.is_none() && account.is_none() {
            return Err(usage_error(
                "--secret requires at least one of --issuer or --account",
            ));
        }
        Ok(OtpInputSource::Secret { issuer, account })
    } else if issuer.is_some() || account.is_some() {
        Err(usage_error("--issuer and --account require --secret"))
    } else {
        Ok(OtpInputSource::Uri)
    }
}

fn line_input_plan(echo: bool, standard_input_is_terminal: bool) -> InputPlan {
    if !standard_input_is_terminal {
        InputPlan::StandardInputLine
    } else if echo {
        InputPlan::EchoedLine
    } else {
        InputPlan::HiddenConfirmed
    }
}

fn usage_error(message: &str) -> CliParseError {
    CliParseError {
        message: format!("error: {message}\n"),
        exit_code: EXIT_USAGE,
    }
}

fn normalize_dispatch(mut arguments: Vec<OsString>) -> Vec<OsString> {
    if arguments.is_empty() {
        arguments.push(OsString::from("ironstorage"));
    }
    let Some(command_index) = first_command_index(&arguments) else {
        return arguments;
    };
    let command = arguments[command_index].to_string_lossy();
    const ROOT_COMMANDS: &[&str] = &[
        "init",
        "ls",
        "list",
        "show",
        "find",
        "search",
        "grep",
        "insert",
        "add",
        "edit",
        "generate",
        "rm",
        "remove",
        "delete",
        "mv",
        "rename",
        "cp",
        "copy",
        "import-kdbx",
        "git",
        "otp",
        "help",
        "version",
        "completion",
    ];
    if !ROOT_COMMANDS.contains(&command.as_ref()) && !command.starts_with('-') {
        arguments.insert(command_index, OsString::from("show"));
        normalize_show_presentation(&mut arguments, command_index + 1);
        return arguments;
    }
    if command == "show" {
        normalize_show_presentation(&mut arguments, command_index + 1);
    } else if command == "otp" {
        normalize_otp_dispatch(&mut arguments, command_index + 1);
    }
    arguments
}

fn normalize_show_presentation(arguments: &mut Vec<OsString>, mut index: usize) {
    while index < arguments.len() {
        let argument = arguments[index].to_string_lossy();
        let long_name = match argument.as_ref() {
            "-c" | "--clip" => Some("--clip"),
            "-q" | "--qrcode" => Some("--qrcode"),
            _ => None,
        };
        if let Some(long_name) = long_name {
            let numeric_value = arguments
                .get(index + 1)
                .and_then(|value| value.to_str())
                .filter(|value| {
                    !value.is_empty() && value.bytes().all(|byte| byte.is_ascii_digit())
                })
                .map(str::to_owned);
            if let Some(value) = numeric_value {
                arguments[index] = OsString::from(format!("{long_name}={value}"));
                arguments.remove(index + 1);
            }
        } else if let Some(value) = argument
            .strip_prefix("-c")
            .filter(|value| !value.is_empty() && value.bytes().all(|byte| byte.is_ascii_digit()))
        {
            arguments[index] = OsString::from(format!("--clip={value}"));
        } else if let Some(value) = argument
            .strip_prefix("-q")
            .filter(|value| !value.is_empty() && value.bytes().all(|byte| byte.is_ascii_digit()))
        {
            arguments[index] = OsString::from(format!("--qrcode={value}"));
        }
        index += 1;
    }
}

fn first_command_index(arguments: &[OsString]) -> Option<usize> {
    let mut index = 1;
    while index < arguments.len() {
        let argument = arguments[index].to_string_lossy();
        if argument == "--config" {
            index += 2;
        } else if argument.starts_with("--config=") {
            index += 1;
        } else {
            return Some(index);
        }
    }
    None
}

fn normalize_otp_dispatch(arguments: &mut Vec<OsString>, mut index: usize) {
    while index < arguments.len() {
        let argument = arguments[index].to_string_lossy();
        if argument == "--config" {
            index += 2;
        } else if argument.starts_with("--config=") {
            index += 1;
        } else {
            break;
        }
    }
    let Some(argument) = arguments.get(index).map(|value| value.to_string_lossy()) else {
        return;
    };
    const OTP_COMMANDS: &[&str] = &[
        "code", "show", "insert", "add", "append", "uri", "validate", "help", "version",
    ];
    if argument == "help" {
        arguments[index] = OsString::from("--help");
    } else if argument == "--version" {
        arguments[index] = OsString::from("version");
    } else if !OTP_COMMANDS.contains(&argument.as_ref()) && argument != "--help" && argument != "-h"
    {
        arguments.insert(index, OsString::from("code"));
    }
}
