use std::collections::BTreeMap;
use std::ffi::OsStr;
use std::fs::{self, File, OpenOptions};
use std::io::{self, IsTerminal, Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};

use anyhow::{anyhow, bail, Context, Result};
use clap::{ArgGroup, Parser, Subcommand};
use ed25519_dalek::SigningKey;
use rand::RngCore;
use serde_json::json;
use time::{format_description::well_known::Rfc3339, OffsetDateTime};
use tzap_core::format::{
    FormatError, CRYPTO_HEADER_FIXED_LEN, READER_MAX_ARGON2ID_M_COST_KIB,
    READER_MAX_ARGON2ID_PARALLELISM, READER_MAX_ARGON2ID_T_COST, VOLUME_HEADER_LEN,
};
use tzap_core::reader::{ArchiveEntry, ArchiveIndexEntry};
use tzap_core::wire::{CryptoHeader, CryptoHeaderFixed, VolumeHeader};
use tzap_core::{
    extract_non_seekable_stream_to_dir, extract_non_seekable_stream_to_dir_with_bootstrap_sidecar,
    extract_unencrypted_non_seekable_stream_to_dir,
    extract_unencrypted_non_seekable_stream_to_dir_with_bootstrap_sidecar,
    list_non_seekable_stream, list_non_seekable_stream_with_bootstrap_sidecar,
    list_unencrypted_non_seekable_stream,
    list_unencrypted_non_seekable_stream_with_bootstrap_sidecar, open_seekable_archive,
    open_seekable_archive_with_bootstrap_sidecar_options,
    public_no_key_verify_volumes_with_options, verify_non_seekable_stream_with_bootstrap_sidecar,
    verify_non_seekable_stream_with_options,
    verify_unencrypted_non_seekable_stream_with_bootstrap_sidecar,
    verify_unencrypted_non_seekable_stream_with_options, write_archive,
    write_archive_sources_to_sink_ordered_parallel, write_archive_with_dictionary,
    write_archive_with_dictionary_and_kdf, write_archive_with_dictionary_and_root_auth,
    write_archive_with_dictionary_kdf_and_root_auth, write_archive_with_kdf,
    write_archive_with_root_auth, write_archive_with_root_auth_and_kdf,
    write_sized_raw_member_archive_to_sink_with_kdf_and_root_auth,
    write_tar_stream_archive_to_sink_with_kdf_and_root_auth, AeadAlgo, ArchiveContentVerification,
    ArchiveRepairPatch, ArchiveWriteError, ArchiveWriteSink, ExtractError, KdfAlgo, KdfParams,
    MasterKey, MemoryArchiveSink, MetadataDiagnostic, NonSeekableReaderOptions, OpenedArchive,
    PublicNoKeyVerification, ReaderOptions, RegularFile, RegularFileSource, RootAuthSigningRequest,
    RootAuthVerification, RootAuthWriterConfig, SafeExtractionOptions, StreamingRawWriterSummary,
    StreamingTarWriterSummary, TarEntryKind, WriterOptions, WriterTimings, WrittenArchiveSummary,
};
use tzap_plugin_signing::ed25519_raw::{
    self, Ed25519RootAuthOutcome, Ed25519VerificationMode, ED25519_AUTHENTICATOR_ID,
    ED25519_AUTHENTICATOR_VALUE_LEN,
};
use tzap_plugin_signing::x509_chain::{
    self, X509RootAuthReport, X509RootAuthSigner, X509_AUTHENTICATOR_ID,
};

mod plaintext_spool;
use plaintext_spool::{spool_unknown_size_raw_stdin, ExplicitPlaintextSpool};

const EXIT_USAGE: u8 = 2;
const EXIT_IO: u8 = 3;
const EXIT_WRONG_KEY: u8 = 10;
const EXIT_CORRUPT_ARCHIVE: u8 = 11;
const EXIT_UNSUPPORTED_REVISION: u8 = 12;
const EXIT_UNSAFE_PATH: u8 = 13;
const EXIT_MISSING_BOOTSTRAP: u8 = 14;
const EXIT_UNSUPPORTED_FEATURE: u8 = 16;
const EXIT_GENERIC: u8 = 1;

const DEFAULT_ARGON2_T_COST: u32 = 3;
const DEFAULT_ARGON2_M_COST_KIB: u32 = 262_144;
const DEFAULT_ARGON2_PARALLELISM: u32 = 4;
const DEFAULT_ARGON2_SALT_LEN: usize = 16;
const INSECURE_ZERO_KEY: [u8; 32] = [0; 32];
const LARGE_CREATE_LAYOUT_THRESHOLD: u64 = 100 * 1024 * 1024 * 1024;

type CliRootAuthAuthenticator<'a> =
    dyn FnMut(&RootAuthSigningRequest) -> std::result::Result<Vec<u8>, FormatError> + 'a;

#[derive(Debug, Parser)]
#[command(name = "tzap")]
#[command(version)]
#[command(about = "Create, list, verify, and extract v43 archives")]
#[command(
    long_about = "Create, list, verify, and extract v43 archives.\n\nCreate selects one protection mode: `--keyfile` for encrypted raw-key archives, `--password` or `--password-stdin` for encrypted passphrase archives, or `--no-encryption` for explicit plaintext archives. Plaintext archives can be listed, verified, and extracted without a password or keyfile. The `verify --public-no-key` mode verifies signed public RootAuth commitments without the archive key.\n\nSize suffixes accepted by size flags:\n  0-9 (bytes), K/KB/KiB, M/MB/MiB, G/GB/GiB.\n\nMulti-volume output naming for this CLI:\n  - one volume: --output writes exactly that path\n  - multiple volumes: --output backup.tzap writes backup.vol000.tzap, backup.vol001.tzap, ...\n\nExit codes:\n  2  usage / argument error\n  3  I/O failure (missing file, permission denied, etc.)\n  10 wrong key\n  11 archive corruption or integrity mismatch\n  12 unsupported archive revision / format version\n  13 unsafe extraction attempt\n  14 missing required bootstrap metadata\n  16 unsupported feature in this CLI/core version\n  1  generic failure\n\nSubcommands:\n  create   Build a new archive\n  extract  Extract files from an archive\n  list     List archive contents\n  verify   Validate archive integrity\n  keygen   Generate a random raw keyfile\n  signing-keygen Generate an Ed25519 RootAuth signing keypair"
)]
struct Cli {
    #[command(subcommand)]
    command: Command,

    #[arg(long = "quiet", global = true, help = "Suppress success summaries.")]
    quiet: bool,

    #[arg(long = "verbose", global = true, help = "Enable verbose diagnostics.")]
    verbose: bool,
}

#[allow(clippy::large_enum_variant)]
#[derive(Debug, Subcommand)]
enum Command {
    #[command(
        about = "Create a new archive",
        long_about = "Create a new archive from files and directories.\n\nThe command writes one output path for single-volume archives, or `.vol000.tzap`, `.vol001.tzap`, ... files for multi-volume archives.",
        after_help = "Examples:\n  tzap create --keyfile key.hex -o backup.tzap file.txt\n  tzap create --password -o backup.tzap file.txt\n  tzap create --password-stdin --argon2-t-cost 1 --argon2-m-cost-kib 8192 -o backup.tzap file.txt\n  tar cf - ./dir | tzap create --tar-stdin --keyfile key.hex -o backup.tzap -\n  tzap create --keyfile key.hex --signing-key root.signing.hex -o backup.tzap file.txt\n  tzap create --keyfile key.hex --signing-cert signer.pem --signing-private-key signer.key -o backup.tzap file.txt\n  tzap create --keyfile key.hex -o backup.tzap --volumes 3 dir/\n  tzap create --keyfile key.hex --volume-size 64M --volume-loss-tolerance 1 -o backup.tzap dir/\n  tzap create --keyfile key.hex --bootstrap-out backup.tzap.bootstrap file.txt",
        group(ArgGroup::new("create-key-source").args([
            "password_stdin",
            "password",
            "keyfile",
            "no_encryption",
        ]))
    )]
    Create {
        #[arg(
            short = 'o',
            long = "output",
            value_name = "ARCHIVE",
            help = "Write output to ARCHIVE (single volume) or base path for multi-volume output."
        )]
        output: String,

        #[arg(
            long = "volumes",
            value_name = "COUNT",
            conflicts_with = "volume_size",
            help = "Create exactly COUNT output volumes."
        )]
        volumes: Option<u32>,

        #[arg(
            long = "volume-size",
            value_name = "SIZE",
            conflicts_with = "volumes",
            help = "Create as many fixed-size output volumes as needed."
        )]
        volume_size: Option<String>,

        #[arg(
            long = "volume-loss-tolerance",
            value_name = "COUNT",
            help = "Allowed missing-volume recovery tolerance for multi-volume archives."
        )]
        volume_loss_tolerance: Option<u8>,

        #[arg(
            long = "bit-rot-buffer-pct",
            value_name = "PERCENT",
            default_value_t = 5,
            help = "Percent of archive reserved for bit-rot recovery structures."
        )]
        bit_rot_buffer_pct: u8,

        #[arg(
            long = "password-stdin",
            conflicts_with = "keyfile",
            conflicts_with = "password",
            conflicts_with = "no_encryption",
            value_name = "STDIN",
            help = "Read passphrase from stdin; one trailing LF or CRLF is stripped."
        )]
        password_stdin: bool,

        #[arg(
            long = "password",
            conflicts_with = "keyfile",
            conflicts_with = "password_stdin",
            conflicts_with = "no_encryption",
            help = "Read passphrase from an interactive prompt."
        )]
        password: bool,

        #[arg(
            long = "keyfile",
            value_name = "KEYFILE",
            conflicts_with = "no_encryption",
            help = "Use a raw key from KEYFILE."
        )]
        keyfile: Option<String>,

        #[arg(
            long = "no-encryption",
            help = "Create an explicit plaintext v43 archive with no password or keyfile."
        )]
        no_encryption: bool,

        #[arg(
            long = "insecure-zero-key",
            hide = true,
            help = "Removed in v43; use --no-encryption for plaintext archives."
        )]
        insecure_zero_key: bool,

        #[arg(
            long = "force",
            help = "Overwrite existing output files and bootstrap sidecar."
        )]
        force: bool,

        #[arg(
            long = "argon2-t-cost",
            value_name = "COUNT",
            default_value_t = DEFAULT_ARGON2_T_COST,
            help = "Argon2 iterations when deriving from passphrase."
        )]
        argon2_t_cost: u32,

        #[arg(
            long = "argon2-m-cost-kib",
            value_name = "KIB",
            default_value_t = DEFAULT_ARGON2_M_COST_KIB,
            help = "Argon2 memory cost (KiB) when deriving from passphrase."
        )]
        argon2_m_cost_kib: u32,

        #[arg(
            long = "argon2-parallelism",
            value_name = "COUNT",
            default_value_t = DEFAULT_ARGON2_PARALLELISM,
            help = "Argon2 parallelism when deriving from passphrase."
        )]
        argon2_parallelism: u32,

        #[arg(
            long = "dictionary",
            value_name = "FILE",
            help = "Read compression dictionary from FILE."
        )]
        dictionary: Option<String>,

        #[arg(
            long = "signing-key",
            value_name = "FILE",
            conflicts_with = "signing_cert",
            help = "Sign RootAuth with an Ed25519 signing key seed from FILE."
        )]
        signing_key: Option<String>,

        #[arg(
            long = "signing-cert",
            value_name = "FILE",
            conflicts_with = "signing_key",
            requires = "signing_private_key",
            help = "Sign RootAuth with an X.509 leaf certificate from FILE."
        )]
        signing_cert: Option<String>,

        #[arg(
            long = "signing-private-key",
            value_name = "FILE",
            conflicts_with = "signing_key",
            requires = "signing_cert",
            help = "Private key for --signing-cert."
        )]
        signing_private_key: Option<String>,

        #[arg(
            long = "signing-chain",
            value_name = "FILE",
            requires = "signing_cert",
            help = "PEM or DER intermediate certificate chain for --signing-cert."
        )]
        signing_chain: Vec<String>,

        #[arg(
            long = "bootstrap-out",
            value_name = "FILE",
            help = "Write bootstrap recovery sidecar to FILE (single-volume output only)."
        )]
        bootstrap_out: Option<String>,

        #[arg(
            long = "tar-stdin",
            help = "Treat PATH '-' as a tar stream read from stdin."
        )]
        tar_stdin: bool,

        #[arg(
            long = "raw-stdin",
            help = "Treat PATH '-' as one raw stdin member named by --stdin-name."
        )]
        raw_stdin: bool,

        #[arg(
            long = "stdin-name",
            value_name = "PATH",
            help = "Archive member path for --raw-stdin."
        )]
        stdin_name: Option<String>,

        #[arg(
            long = "stdin-size",
            value_name = "SIZE",
            help = "Expected byte size for known-size --raw-stdin."
        )]
        stdin_size: Option<String>,

        #[arg(
            long = "spool-stdin",
            help = "Spool unknown-size raw stdin to a restrictive temporary file before archiving."
        )]
        spool_stdin: bool,

        #[arg(
            long = "compression-level",
            value_name = "LEVEL",
            default_value_t = 3,
            help = "zstd compression level."
        )]
        compression_level: i32,

        #[arg(
            long = "chunk-size",
            value_name = "SIZE",
            help = "Compression chunk size (default: auto by input size)."
        )]
        chunk_size: Option<String>,

        #[arg(
            long = "envelope-size",
            value_name = "SIZE",
            help = "Archive envelope size (default: auto by input size)."
        )]
        envelope_size: Option<String>,

        #[arg(
            long = "block-size",
            value_name = "SIZE",
            help = "Block size for archive payload layout (default: auto by input size)."
        )]
        block_size: Option<String>,

        #[arg(
            long = "jobs",
            value_name = "N",
            help = "Worker jobs for reader/writer CPU work (default: logical CPU count)."
        )]
        jobs: Option<usize>,

        #[arg(
            long = "timings",
            help = "Print create-stage timing breakdown to stderr."
        )]
        timings: bool,

        #[arg(
            long = "dry-run",
            help = "Print a create plan and file summary without writing archive bytes."
        )]
        dry_run: bool,

        #[arg(
            required = true,
            value_name = "PATH",
            help = "One or more input files or directories."
        )]
        paths: Vec<String>,
    },
    #[command(
        about = "Extract files from an archive",
        long_about = "Extract one or many archive members into a directory, with safe-path protections enabled by default.",
        after_help = "Examples:\n  tzap extract --keyfile key.hex -C out/ backup.tzap\n  tzap extract --keyfile key.hex backup.tzap file.txt\n  tzap extract --keyfile key.hex --stdout backup.tzap hello.txt > out.bin\n  tzap extract --password-stdin --overwrite backup.tzap target/\n  tzap extract --dry-run -C out backup.tzap file.txt\n  tzap extract --bootstrap backup.tzap.bootstrap -C out backup.tzap",
        group(
            ArgGroup::new("open-key-source")
                .args(["password_stdin", "password", "keyfile", "insecure_zero_key"])
        )
    )]
    Extract {
        #[arg(
            value_name = "ARCHIVE",
            help = "Archive input. A .volNNN.tzap path discovers sibling volumes unless --volume is used."
        )]
        archive: String,

        #[arg(
            value_name = "PATH",
            help = "Optional archive member paths to extract."
        )]
        paths: Vec<String>,

        #[arg(
            short = 'C',
            long = "directory",
            value_name = "DIR",
            default_value = ".",
            help = "Destination directory for extracted files."
        )]
        directory: String,

        #[arg(
            long = "stdout",
            conflicts_with = "dry_run",
            help = "Write a single selected member to stdout."
        )]
        stdout: bool,

        #[arg(
            long = "dry-run",
            help = "Show what would be extracted without writing files."
        )]
        dry_run: bool,

        #[arg(long = "overwrite", help = "Allow overwriting existing output files.")]
        overwrite: bool,

        #[arg(
            long = "password-stdin",
            conflicts_with = "keyfile",
            conflicts_with = "password",
            conflicts_with = "insecure_zero_key",
            value_name = "STDIN",
            help = "Read passphrase from stdin; one trailing LF or CRLF is stripped."
        )]
        password_stdin: bool,

        #[arg(
            long = "password",
            conflicts_with = "keyfile",
            conflicts_with = "password_stdin",
            conflicts_with = "insecure_zero_key",
            help = "Read passphrase from an interactive prompt."
        )]
        password: bool,

        #[arg(
            long = "keyfile",
            value_name = "KEYFILE",
            conflicts_with = "insecure_zero_key",
            help = "Use a raw key from KEYFILE."
        )]
        keyfile: Option<String>,

        #[arg(
            long = "insecure-zero-key",
            hide = true,
            help = "Removed in v43; plaintext archives need no key source."
        )]
        insecure_zero_key: bool,

        #[arg(
            long = "bootstrap",
            value_name = "FILE",
            help = "Use bootstrap sidecar FILE for single-volume archive input."
        )]
        bootstrap: Option<String>,

        #[arg(
            long = "volume",
            value_name = "FILE",
            help = "Explicit additional volume path."
        )]
        volumes: Vec<String>,

        #[arg(
            long = "jobs",
            value_name = "N",
            help = "Worker jobs for reader CPU work (default: logical CPU count)."
        )]
        jobs: Option<usize>,
    },
    #[command(
        about = "List archive contents",
        long_about = "List archive members in plain format by default.",
        after_help = "Examples:\n  tzap list --keyfile key.hex backup.tzap\n  tzap list --keyfile key.hex --long backup.tzap\n  tzap list --keyfile key.hex --json backup.tzap\n  tzap list --password-stdin --bootstrap backup.tzap.bootstrap backup.tzap",
        group(
            ArgGroup::new("open-key-source")
                .args(["password_stdin", "password", "keyfile", "insecure_zero_key"])
        )
    )]
    List {
        #[arg(
            value_name = "ARCHIVE",
            help = "Archive to inspect. A .volNNN.tzap path discovers sibling volumes unless --volume is used."
        )]
        archive: String,

        #[arg(
            long = "password-stdin",
            conflicts_with = "keyfile",
            conflicts_with = "password",
            conflicts_with = "insecure_zero_key",
            value_name = "STDIN",
            help = "Read passphrase from stdin; one trailing LF or CRLF is stripped."
        )]
        password_stdin: bool,

        #[arg(
            long = "password",
            conflicts_with = "keyfile",
            conflicts_with = "password_stdin",
            conflicts_with = "insecure_zero_key",
            help = "Read passphrase from an interactive prompt."
        )]
        password: bool,

        #[arg(
            long = "keyfile",
            value_name = "KEYFILE",
            conflicts_with = "insecure_zero_key",
            help = "Use a raw key from KEYFILE."
        )]
        keyfile: Option<String>,

        #[arg(
            long = "insecure-zero-key",
            hide = true,
            help = "Removed in v43; plaintext archives need no key source."
        )]
        insecure_zero_key: bool,

        #[arg(
            long = "bootstrap",
            value_name = "FILE",
            help = "Use bootstrap sidecar FILE for single-volume archive input."
        )]
        bootstrap: Option<String>,

        #[arg(
            long = "volume",
            value_name = "FILE",
            help = "Explicit additional volume path."
        )]
        volumes: Vec<String>,

        #[arg(
            long = "long",
            conflicts_with = "json",
            help = "Use verbose listing output."
        )]
        long: bool,

        #[arg(
            long = "json",
            conflicts_with = "long",
            help = "Emit stable machine-readable JSON output."
        )]
        json: bool,

        #[arg(
            long = "jobs",
            value_name = "N",
            help = "Worker jobs for reader CPU work (default: logical CPU count)."
        )]
        jobs: Option<usize>,
    },
    #[command(
        about = "Verify archive integrity",
        long_about = "Verify archive signatures and checksum integrity. No payload changes are made unless --write-repaired is set; original archive files are never modified.\n\nEncrypted archives need --keyfile, --password, or --password-stdin. Unencrypted v43 archives need no key source. With --public-no-key, verify uses the v43 public RootAuth profile and does not require the archive key.",
        after_help = "Examples:\n  tzap verify --keyfile key.hex backup.tzap\n  tzap verify --keyfile key.hex --write-repaired backup.tzap\n  tzap verify --keyfile key.hex --trusted-public-key root.public.hex backup.tzap\n  tzap verify --keyfile key.hex --trusted-ca-cert root-ca.pem backup.tzap\n  tzap verify --public-no-key --trusted-public-key root.public.hex backup.tzap\n  tzap verify --public-no-key --trusted-ca-cert root-ca.pem backup.tzap\n  tzap verify --keyfile key.hex backup.vol000.tzap backup.vol001.tzap\n  tzap verify --password-stdin backup.tzap\n  tzap verify --json --keyfile key.hex backup.tzap\n  tzap verify --quiet --keyfile key.hex backup.tzap\n\nFor multi-volume archives named `.volNNN.tzap`, passing any one volume discovers matching siblings in the same directory. Additional positionals are explicit extra volumes."
    )]
    Verify {
        #[arg(
            required = true,
            value_name = "ARCHIVE",
            help = "Archive path. A .volNNN.tzap path discovers sibling volumes unless extra archive paths are supplied."
        )]
        archives: Vec<String>,

        #[arg(
            long = "password-stdin",
            conflicts_with = "keyfile",
            conflicts_with = "password",
            conflicts_with = "insecure_zero_key",
            value_name = "STDIN",
            help = "Read passphrase from stdin; one trailing LF or CRLF is stripped."
        )]
        password_stdin: bool,

        #[arg(
            long = "password",
            conflicts_with = "keyfile",
            conflicts_with = "password_stdin",
            conflicts_with = "insecure_zero_key",
            help = "Read passphrase from an interactive prompt."
        )]
        password: bool,

        #[arg(
            long = "keyfile",
            value_name = "KEYFILE",
            conflicts_with = "insecure_zero_key",
            help = "Use a raw key from KEYFILE."
        )]
        keyfile: Option<String>,

        #[arg(
            long = "insecure-zero-key",
            hide = true,
            help = "Removed in v43; plaintext archives need no key source."
        )]
        insecure_zero_key: bool,

        #[arg(
            long = "trusted-public-key",
            value_name = "FILE",
            help = "Verify Ed25519 RootAuth with trusted public key FILE."
        )]
        trusted_public_key: Option<String>,

        #[arg(
            long = "trusted-ca-cert",
            value_name = "FILE",
            help = "Verify X.509 RootAuth with trusted CA certificate FILE."
        )]
        trusted_ca_cert: Vec<String>,

        #[arg(
            long = "trusted-system-roots",
            help = "Allow X.509 RootAuth verification with OpenSSL default trust roots."
        )]
        trusted_system_roots: bool,

        #[arg(
            long = "public-no-key",
            help = "Verify v43 public RootAuth commitments without the archive key."
        )]
        public_no_key: bool,

        #[arg(
            long = "bootstrap",
            value_name = "FILE",
            help = "Use bootstrap sidecar FILE for single-volume archive input."
        )]
        bootstrap: Option<String>,

        #[arg(
            long = "json",
            conflicts_with = "quiet",
            help = "Emit stable machine-readable JSON output."
        )]
        json: bool,

        #[arg(
            long = "write-repaired",
            help = "After successful key-holding verification, write repaired copies for volumes that had recoverable block damage."
        )]
        write_repaired: bool,

        #[arg(
            long = "jobs",
            value_name = "N",
            help = "Worker jobs for reader CPU work (default: logical CPU count)."
        )]
        jobs: Option<usize>,
    },
    #[command(
        about = "Generate a random raw key",
        long_about = "Generate a random 32-byte raw key and write it as 64 lowercase hex characters.\n\nBy default, --output refuses to overwrite an existing file.\nUse --force if you want to replace it.\n\nUse --stdout to print the key to stdout instead.",
        group(
            ArgGroup::new("keygen-output")
                .required(true)
                .args(["output", "stdout"])
        )
    )]
    Keygen {
        #[arg(
            short = 'o',
            long = "output",
            value_name = "KEYFILE",
            conflicts_with = "stdout",
            help = "Write the generated key to KEYFILE."
        )]
        output: Option<String>,

        #[arg(long = "stdout", help = "Write the generated key to stdout.")]
        stdout: bool,

        #[arg(long = "force", help = "Overwrite an existing output keyfile.")]
        force: bool,
    },
    #[command(
        name = "signing-keygen",
        about = "Generate an Ed25519 RootAuth signing keypair",
        long_about = "Generate an Ed25519 RootAuth signing keypair. The secret output is a 32-byte signing seed encoded as 64 lowercase hex characters; the public output is a 32-byte Ed25519 verifying key encoded the same way."
    )]
    SigningKeygen {
        #[arg(
            long = "secret-output",
            value_name = "FILE",
            help = "Write the generated Ed25519 signing seed to FILE."
        )]
        secret_output: String,

        #[arg(
            long = "public-output",
            value_name = "FILE",
            help = "Write the generated Ed25519 public key to FILE."
        )]
        public_output: String,

        #[arg(long = "force", help = "Overwrite existing keypair output files.")]
        force: bool,
    },
}

fn main() -> ExitCode {
    let cli = match Cli::try_parse() {
        Ok(cli) => cli,
        Err(err) => {
            let code = if err.use_stderr() { EXIT_USAGE } else { 0 };
            let _ = err.print();
            return ExitCode::from(code);
        }
    };
    if cli.quiet && matches!(&cli.command, Command::Verify { json: true, .. }) {
        eprintln!("error: --quiet cannot be used with --json for verify");
        return ExitCode::from(EXIT_USAGE);
    }
    let is_verify_json = matches!(&cli.command, Command::Verify { json: true, .. });

    match run(cli) {
        Ok(()) => ExitCode::SUCCESS,
        Err(err) => {
            let diagnostic = classify_error(&err);
            if !is_verify_json {
                if diagnostic.action.is_empty() {
                    eprintln!("tzap: {}: {err:#}", diagnostic.label);
                } else {
                    eprintln!("tzap: {}: {err:#}: {}", diagnostic.label, diagnostic.action);
                }
            }
            ExitCode::from(diagnostic.exit_code)
        }
    }
}

fn run(cli: Cli) -> Result<()> {
    let quiet = cli.quiet;
    match cli.command {
        Command::Create {
            output,
            volumes,
            volume_size,
            volume_loss_tolerance,
            bit_rot_buffer_pct,
            password_stdin,
            password,
            keyfile,
            no_encryption,
            insecure_zero_key,
            force,
            dry_run,
            argon2_t_cost,
            argon2_m_cost_kib,
            argon2_parallelism,
            dictionary,
            signing_key,
            signing_cert,
            signing_private_key,
            signing_chain,
            bootstrap_out,
            tar_stdin,
            raw_stdin,
            stdin_name,
            stdin_size,
            spool_stdin,
            compression_level,
            chunk_size,
            envelope_size,
            block_size,
            jobs,
            timings,
            paths,
        } => {
            let create_total_started = Instant::now();
            let jobs = resolve_jobs(jobs)?;
            let resolved_volume_loss_tolerance = resolve_create_volume_loss_tolerance(
                volume_loss_tolerance,
                volumes,
                volume_size.as_deref(),
                tar_stdin || raw_stdin || spool_stdin,
            );
            let layout_overrides = CreateLayoutOverrides {
                chunk_size: chunk_size.as_deref(),
                envelope_size: envelope_size.as_deref(),
                block_size: block_size.as_deref(),
            };
            let build_writer_options = |total_input_size: Option<u64>| -> Result<WriterOptions> {
                let mut options = create_writer_options(CreateWriterOptionsArgs {
                    volumes,
                    volume_size: volume_size.as_deref(),
                    volume_loss_tolerance: resolved_volume_loss_tolerance,
                    bit_rot_buffer_pct,
                    compression_level,
                    jobs,
                    layout_overrides,
                    total_input_size,
                })?;
                if no_encryption {
                    options.aead_algo = AeadAlgo::None;
                }
                Ok(options)
            };
            validate_create_key_source(
                keyfile.as_deref(),
                password_stdin,
                password,
                no_encryption,
                insecure_zero_key,
            )?;
            if bootstrap_out.is_some() && (volumes.unwrap_or(1) > 1 || volume_size.is_some()) {
                return Err(FormatError::WriterUnsupported(
                    "--bootstrap-out is currently supported only for single-volume output",
                )
                .into());
            }
            reject_create_stdout_sentinels(&output, bootstrap_out.as_deref())?;
            let stdin_mode = validate_create_stdin_mode(CreateStdinArgs {
                tar_stdin,
                raw_stdin,
                stdin_name: stdin_name.as_deref(),
                stdin_size: stdin_size.as_deref(),
                spool_stdin,
                paths: &paths,
                password_stdin,
                password,
                has_dictionary: dictionary.is_some(),
                volumes,
                volume_size: volume_size.as_deref(),
                volume_loss_tolerance,
            })?;

            ensure_create_output_paths_can_be_written(
                &output,
                volumes,
                volume_size.is_some(),
                bootstrap_out.as_deref(),
                force,
            )?;
            if let Some(stdin_mode) = stdin_mode {
                if dry_run {
                    let dry_run_input_size = match stdin_mode {
                        CreateStdinMode::RawKnownSize => Some(parse_size(
                            stdin_size.as_deref().expect("validated stdin-size"),
                        )?),
                        CreateStdinMode::Tar
                        | CreateStdinMode::RawSpool
                        | CreateStdinMode::RawUnknownSize => None,
                    };
                    let options = build_writer_options(dry_run_input_size)?;
                    validate_create_writer_options(&options)?;
                }
                if dry_run {
                    eprintln!("create dry-run summary:");
                    eprintln!("  files: streaming stdin");
                    eprintln!("  input bytes: unknown until stdin is consumed");
                    eprintln!(
                        "  key mode: {}",
                        create_key_mode_label(
                            keyfile.as_deref(),
                            password_stdin,
                            password,
                            no_encryption,
                            insecure_zero_key
                        )
                    );
                    eprintln!(
                        "  root auth: {}",
                        create_root_auth_mode_label(
                            signing_key.as_deref(),
                            signing_cert.as_deref()
                        )
                    );
                    eprintln!(
                        "  volume mode: {}",
                        describe_planned_volume_mode(volumes, volume_size.as_deref())
                    );
                    eprintln!("  planned archive paths:");
                    for path in create_dry_run_output_paths(&output, volumes, volume_size.is_some())
                    {
                        eprintln!("    {path}");
                    }
                    if let Some(bootstrap_path) = bootstrap_out.as_ref() {
                        eprintln!("  bootstrap: {}", bootstrap_path);
                    }
                    return Ok(());
                }

                if matches!(stdin_mode, CreateStdinMode::RawUnknownSize) {
                    return Err(FormatError::WriterUnsupported(
                        "unknown-size raw stdin without --spool-stdin requires the future raw_stream_v1 profile",
                    )
                    .into());
                }

                let key = load_create_key(
                    keyfile.as_deref(),
                    password_stdin,
                    password,
                    no_encryption,
                    insecure_zero_key,
                    argon2_t_cost,
                    argon2_m_cost_kib,
                    argon2_parallelism,
                )?;
                let root_auth_profile = load_create_root_auth_profile(
                    signing_key.as_deref(),
                    signing_cert.as_deref(),
                    signing_private_key.as_deref(),
                    &signing_chain,
                )?;
                let root_auth = root_auth_profile
                    .as_ref()
                    .map(CreateRootAuthProfile::root_auth_writer_config)
                    .transpose()?;
                let core_writer_started = Instant::now();
                let (bootstrap_sidecar, summary_text, writer_timings) = match stdin_mode {
                    CreateStdinMode::Tar => {
                        let options = build_writer_options(None)?;
                        validate_create_writer_options(&options)?;
                        let (summary, bootstrap_sidecar) = write_tar_stdin_archive_output(
                            &output,
                            &key,
                            options,
                            root_auth,
                            root_auth_profile.as_ref(),
                            force,
                        )?;
                        let summary_text = format!(
                            "created {} file(s), {} tar bytes in, {} archive bytes, {} volume(s), volume-loss tolerance {}, bit-rot buffer {}%",
                            summary.input_member_count,
                            summary.input_tar_bytes,
                            summary.archive.archive_bytes,
                            summary.archive.volume_count,
                            resolved_volume_loss_tolerance,
                            bit_rot_buffer_pct
                        );
                        (bootstrap_sidecar, summary_text, summary.archive.timings)
                    }
                    CreateStdinMode::RawKnownSize => {
                        let stdin_size =
                            parse_size(stdin_size.as_deref().expect("validated stdin-size"))?;
                        let options = build_writer_options(Some(stdin_size))?;
                        validate_create_writer_options(&options)?;
                        let (summary, bootstrap_sidecar) = write_raw_stdin_archive_output(
                            &output,
                            io::stdin().lock(),
                            stdin_name.as_deref().expect("validated stdin-name"),
                            stdin_size,
                            &key,
                            options,
                            root_auth,
                            root_auth_profile.as_ref(),
                            force,
                        )?;
                        let summary_text = format!(
                            "created 1 file(s), {} raw bytes in, {} archive bytes, {} volume(s), volume-loss tolerance {}, bit-rot buffer {}%",
                            summary.input_bytes,
                            summary.archive.archive_bytes,
                            summary.archive.volume_count,
                            resolved_volume_loss_tolerance,
                            bit_rot_buffer_pct
                        );
                        (bootstrap_sidecar, summary_text, summary.archive.timings)
                    }
                    CreateStdinMode::RawSpool => {
                        let stdin = io::stdin();
                        let mut stdin_lock = stdin.lock();
                        let spool = spool_unknown_size_raw_stdin(
                            &mut stdin_lock,
                            u64::MAX,
                            ExplicitPlaintextSpool::acknowledge_plaintext_spool(),
                        )?;
                        let known_size_source = spool.known_size_source();
                        let spool_reader = spool.reopen()?;
                        let options = build_writer_options(Some(known_size_source.size()))?;
                        validate_create_writer_options(&options)?;
                        let (summary, bootstrap_sidecar) = write_raw_stdin_archive_output(
                            &output,
                            spool_reader,
                            stdin_name.as_deref().expect("validated stdin-name"),
                            known_size_source.size(),
                            &key,
                            options,
                            root_auth,
                            root_auth_profile.as_ref(),
                            force,
                        )?;
                        let summary_text = format!(
                            "created 1 file(s), {} spooled raw bytes in, {} archive bytes, {} volume(s), volume-loss tolerance {}, bit-rot buffer {}%",
                            summary.input_bytes,
                            summary.archive.archive_bytes,
                            summary.archive.volume_count,
                            resolved_volume_loss_tolerance,
                            bit_rot_buffer_pct
                        );
                        (bootstrap_sidecar, summary_text, summary.archive.timings)
                    }
                    CreateStdinMode::RawUnknownSize => unreachable!("rejected before key loading"),
                };
                let core_writer = core_writer_started.elapsed();
                let write_outputs_started = Instant::now();
                if let Some(path) = bootstrap_out.as_deref() {
                    if bootstrap_sidecar.is_empty() {
                        return Err(FormatError::WriterUnsupported(
                            "bootstrap output is unavailable for this archive shape",
                        )
                        .into());
                    }
                    write_bootstrap_output(path, &bootstrap_sidecar, force)?;
                }
                let write_outputs = write_outputs_started.elapsed();
                emit_success_summary(quiet, &summary_text)?;
                if let Some(profile) = root_auth_profile.as_ref() {
                    emit_success_summary(
                        quiet,
                        &format!("  root auth: {} signed", profile.label()),
                    )?;
                }
                if let Some(path) = bootstrap_out.as_ref() {
                    emit_success_summary(quiet, &format!("  bootstrap output: {}", path))?;
                }
                if timings {
                    emit_create_timing_report(
                        Duration::default(),
                        Duration::default(),
                        core_writer,
                        write_outputs,
                        create_total_started.elapsed(),
                        writer_timings,
                    )?;
                }
                return Ok(());
            }
            let scan_inputs_started = Instant::now();
            let input_specs = collect_input_specs(&paths)?;
            let scan_inputs = scan_inputs_started.elapsed();
            let bootstrap_output = bootstrap_out.clone();
            let input_bytes = input_specs_total_size(&input_specs)?;
            let options = build_writer_options(Some(input_bytes))?;
            validate_create_writer_options(&options)?;

            if dry_run {
                eprintln!("create dry-run summary:");
                eprintln!("  files: {}", input_specs.len());
                eprintln!("  input bytes: {}", input_bytes);
                eprintln!(
                    "  key mode: {}",
                    create_key_mode_label(
                        keyfile.as_deref(),
                        password_stdin,
                        password,
                        no_encryption,
                        insecure_zero_key
                    )
                );
                eprintln!(
                    "  root auth: {}",
                    create_root_auth_mode_label(signing_key.as_deref(), signing_cert.as_deref())
                );
                eprintln!(
                    "  volume mode: {}",
                    describe_planned_volume_mode(volumes, volume_size.as_deref())
                );
                eprintln!("  planned archive paths:");
                for path in create_dry_run_output_paths(&output, volumes, volume_size.is_some()) {
                    eprintln!("    {path}");
                }
                if let Some(bootstrap_path) = bootstrap_output {
                    eprintln!("  bootstrap: {}", bootstrap_path);
                }
                return Ok(());
            }

            let key = load_create_key(
                keyfile.as_deref(),
                password_stdin,
                password,
                no_encryption,
                insecure_zero_key,
                argon2_t_cost,
                argon2_m_cost_kib,
                argon2_parallelism,
            )?;
            let dictionary_bytes = dictionary
                .as_deref()
                .map(|path| {
                    fs::read(path).with_context(|| format!("failed to read dictionary {path}"))
                })
                .transpose()?;
            let root_auth_profile = load_create_root_auth_profile(
                signing_key.as_deref(),
                signing_cert.as_deref(),
                signing_private_key.as_deref(),
                &signing_chain,
            )?;
            let root_auth = root_auth_profile
                .as_ref()
                .map(CreateRootAuthProfile::root_auth_writer_config)
                .transpose()?;

            if dictionary_bytes.is_none()
                && options.target_volume_size.is_none()
                && options.volume_loss_tolerance == 0
            {
                let core_writer_started = Instant::now();
                let (archive, sink) = write_file_inputs_ordered_parallel_to_memory(
                    &input_specs,
                    &key,
                    options,
                    root_auth,
                    root_auth_profile.as_ref(),
                )
                .context("failed to create archive")?;
                let core_writer = core_writer_started.elapsed();

                let write_outputs_started = Instant::now();
                write_archive_outputs(&output, &sink.volumes, force)?;
                if let Some(path) = bootstrap_out.as_deref() {
                    if sink.bootstrap_sidecar.is_empty() {
                        return Err(FormatError::WriterUnsupported(
                            "bootstrap output is unavailable for this archive shape",
                        )
                        .into());
                    }
                    write_bootstrap_output(path, &sink.bootstrap_sidecar, force)?;
                }
                let write_outputs = write_outputs_started.elapsed();
                let summary = format!(
                    "created {} file(s), {} bytes in, {} archive bytes, {} volume(s), volume-loss tolerance {}, bit-rot buffer {}%",
                    input_specs.len(),
                    input_bytes,
                    archive.archive_bytes,
                    archive.volume_count,
                    resolved_volume_loss_tolerance,
                    bit_rot_buffer_pct
                );
                emit_success_summary(quiet, &summary)?;
                if let Some(profile) = root_auth_profile.as_ref() {
                    emit_success_summary(
                        quiet,
                        &format!("  root auth: {} signed", profile.label()),
                    )?;
                }
                if let Some(path) = bootstrap_output {
                    emit_success_summary(quiet, &format!("  bootstrap output: {}", path))?;
                }
                if timings {
                    emit_create_timing_report(
                        scan_inputs,
                        Duration::default(),
                        core_writer,
                        write_outputs,
                        create_total_started.elapsed(),
                        archive.timings,
                    )?;
                }
                return Ok(());
            }

            let read_inputs_started = Instant::now();
            let inputs = collect_input_files(&input_specs)?;
            let read_inputs = read_inputs_started.elapsed();
            let regular_files = inputs
                .iter()
                .map(|file| RegularFile {
                    path: file.archive_path.as_str(),
                    contents: &file.contents,
                    mode: file.mode,
                    mtime: file.mtime,
                })
                .collect::<Vec<_>>();
            let core_writer_started = Instant::now();
            let archive = match (
                &dictionary_bytes,
                &key.kdf_params,
                root_auth,
                root_auth_profile.as_ref(),
            ) {
                (Some(dictionary), KdfParams::Raw, Some(root_auth), Some(profile)) => {
                    write_archive_with_dictionary_and_root_auth(
                        &regular_files,
                        &key.master_key,
                        options,
                        dictionary,
                        root_auth,
                        |request| root_auth_authenticator_value(profile, request),
                    )
                }
                (Some(dictionary), kdf_params, Some(root_auth), Some(profile)) => {
                    write_archive_with_dictionary_kdf_and_root_auth(
                        &regular_files,
                        &key.master_key,
                        options,
                        dictionary,
                        kdf_params,
                        root_auth,
                        |request| root_auth_authenticator_value(profile, request),
                    )
                }
                (None, KdfParams::Raw, Some(root_auth), Some(profile)) => {
                    write_archive_with_root_auth(
                        &regular_files,
                        &key.master_key,
                        options,
                        root_auth,
                        |request| root_auth_authenticator_value(profile, request),
                    )
                }
                (None, kdf_params, Some(root_auth), Some(profile)) => {
                    write_archive_with_root_auth_and_kdf(
                        &regular_files,
                        &key.master_key,
                        options,
                        kdf_params,
                        root_auth,
                        |request| root_auth_authenticator_value(profile, request),
                    )
                }
                (Some(dictionary), KdfParams::Raw, None, _) => write_archive_with_dictionary(
                    &regular_files,
                    &key.master_key,
                    options,
                    dictionary,
                ),
                (Some(dictionary), kdf_params, None, _) => write_archive_with_dictionary_and_kdf(
                    &regular_files,
                    &key.master_key,
                    options,
                    dictionary,
                    kdf_params,
                ),
                (None, KdfParams::Raw, None, _) => {
                    write_archive(&regular_files, &key.master_key, options)
                }
                (None, kdf_params, None, _) => {
                    write_archive_with_kdf(&regular_files, &key.master_key, options, kdf_params)
                }
                (_, _, Some(_), None) => unreachable!("root auth requires signing profile"),
            }
            .context("failed to create archive")?;
            let core_writer = core_writer_started.elapsed();

            let output_paths = create_output_paths(&output, archive.volumes.len());
            if !force {
                check_archive_paths_free_for_write(&output_paths)?;
            }
            if let Some(bootstrap_path) = &bootstrap_output {
                if !force {
                    check_output_path_free("bootstrap", Path::new(bootstrap_path))?;
                }
            }

            let write_outputs_started = Instant::now();
            write_archive_outputs(&output, &archive.volumes, force)?;
            if let Some(path) = bootstrap_out {
                if archive.bootstrap_sidecar.is_empty() {
                    return Err(FormatError::WriterUnsupported(
                        "bootstrap output is unavailable for this archive shape",
                    )
                    .into());
                }
                write_bootstrap_output(&path, &archive.bootstrap_sidecar, force)?;
            }
            let write_outputs = write_outputs_started.elapsed();
            let summary = format!(
                "created {} file(s), {} bytes in, {} archive bytes, {} volume(s), volume-loss tolerance {}, bit-rot buffer {}%",
                regular_files.len(),
                input_bytes,
                archive.volumes.iter().map(|volume| volume.len() as u64).sum::<u64>(),
                archive.volumes.len(),
                resolved_volume_loss_tolerance,
                bit_rot_buffer_pct
            );
            emit_success_summary(quiet, &summary)?;
            if let Some(profile) = root_auth_profile.as_ref() {
                emit_success_summary(quiet, &format!("  root auth: {} signed", profile.label()))?;
            }
            if let Some(path) = bootstrap_output {
                emit_success_summary(quiet, &format!("  bootstrap output: {}", path))?;
            }
            if timings {
                emit_create_timing_report(
                    scan_inputs,
                    read_inputs,
                    core_writer,
                    write_outputs,
                    create_total_started.elapsed(),
                    archive.timings,
                )?;
            }
            Ok(())
        }
        Command::Extract {
            archive,
            paths,
            directory,
            stdout,
            dry_run,
            overwrite,
            password_stdin,
            password,
            keyfile,
            insecure_zero_key,
            bootstrap,
            volumes,
            jobs,
        } => {
            let reader_options = reader_options(resolve_jobs(jobs)?);
            reject_multi_volume_bootstrap(1 + volumes.len(), bootstrap.as_deref())?;
            reject_stdout_extract_shape(stdout, paths.len())?;
            if archive == "-" {
                reject_archive_stdin_open_options(ArchiveStdinOpenOptions {
                    paths: &paths,
                    stdout,
                    volumes: &volumes,
                    password_stdin,
                    password,
                    keyfile: keyfile.as_deref(),
                    insecure_zero_key,
                })?;
                if dry_run {
                    eprintln!("extract dry-run summary:");
                    eprintln!("  input: archive stdin");
                    eprintln!("  destination: {}", directory);
                    eprintln!("  mode: staged non-seekable extract-all");
                    return Ok(());
                }
                let options = SafeExtractionOptions {
                    overwrite_existing: overwrite,
                };
                let bootstrap_bytes = read_optional_bootstrap_sidecar(bootstrap.as_deref())?;
                let stdin = io::stdin();
                let report = if let Some(keyfile) = keyfile.as_deref() {
                    let master_key = load_archive_stdin_key(
                        Some(keyfile),
                        password_stdin,
                        password,
                        insecure_zero_key,
                    )?;
                    if let Some(bootstrap_bytes) = bootstrap_bytes.as_deref() {
                        extract_non_seekable_stream_to_dir_with_bootstrap_sidecar(
                            stdin.lock(),
                            bootstrap_bytes,
                            &master_key,
                            Path::new(&directory),
                            non_seekable_reader_options(reader_options),
                            options,
                        )
                    } else {
                        extract_non_seekable_stream_to_dir(
                            stdin.lock(),
                            &master_key,
                            Path::new(&directory),
                            non_seekable_reader_options(reader_options),
                            options,
                        )
                    }
                } else {
                    if let Some(bootstrap_bytes) = bootstrap_bytes.as_deref() {
                        extract_unencrypted_non_seekable_stream_to_dir_with_bootstrap_sidecar(
                            stdin.lock(),
                            bootstrap_bytes,
                            Path::new(&directory),
                            non_seekable_reader_options(reader_options),
                            options,
                        )
                    } else {
                        extract_unencrypted_non_seekable_stream_to_dir(
                            stdin.lock(),
                            Path::new(&directory),
                            non_seekable_reader_options(reader_options),
                            options,
                        )
                    }
                }
                .context("failed to extract non-seekable archive stream")?;
                emit_success_summary(
                    quiet,
                    &format!(
                        "extracted {} member(s), {} degraded metadata items to {} using staged non-seekable stream extraction",
                        report.extracted_member_count,
                        report.degraded_metadata_count,
                        directory
                    ),
                )?;
                return Ok(());
            }
            let selection = resolve_archive_input_paths(&archive, &volumes, bootstrap.is_none())?;
            let master_key = load_open_key_from_paths(
                keyfile.as_deref(),
                password_stdin,
                password,
                insecure_zero_key,
                &selection.paths,
            )?;
            let opened = open_selection_maybe_bootstrap(
                &selection,
                &master_key,
                bootstrap.as_deref(),
                reader_options,
            )
            .with_context(|| format!("failed to open archive {archive}"))?;
            let (requested_entries, missing_paths) = if stdout || dry_run || !paths.is_empty() {
                resolve_extract_index_entries(&opened, &paths)?
            } else {
                (Vec::new(), Vec::new())
            };
            if !missing_paths.is_empty() {
                for missing in missing_paths {
                    eprintln!("missing archive path: {missing}");
                }
                return Err(anyhow!("missing requested archive paths"));
            }
            if stdout {
                let path = requested_entries[0].path.as_str();
                let mut stdout = io::stdout().lock();
                let diagnostics = match opened.extract_file_to_writer(path, &mut stdout) {
                    Ok(Some(diagnostics)) => diagnostics,
                    Ok(None) => bail!("path not found in archive: {path}"),
                    Err(ExtractError::Format(FormatError::ReaderUnsupported(message)))
                        if message.contains("regular file") =>
                    {
                        bail!("--stdout supports regular file members only");
                    }
                    Err(err) => return Err(err.into()),
                };
                stdout.flush()?;
                emit_member_metadata_diagnostics(quiet, path, &diagnostics)?;
                return Ok(());
            }

            if dry_run {
                eprintln!("extract dry-run summary:");
                eprintln!("  destination: {}", directory);
                eprintln!("  archive members:");
                for entry in &requested_entries {
                    eprintln!("    {} ({} bytes)", entry.path, entry.file_data_size);
                }
                return Ok(());
            }

            let root = PathBuf::from(directory);
            fs::create_dir_all(&root).with_context(|| {
                format!("failed to create extraction directory {}", root.display())
            })?;
            let mut extracted_count = 0u64;
            let mut degraded_metadata_count = 0u64;
            let options = SafeExtractionOptions {
                overwrite_existing: overwrite,
            };
            let diagnostics = if paths.is_empty() {
                opened.extract_indexed_files_to(&root, options, reader_options.jobs)?
            } else {
                extract_entries_to_dir_parallel(
                    &opened,
                    requested_entries,
                    &root,
                    options,
                    reader_options.jobs,
                )?
            };
            for (path, diagnostics) in diagnostics {
                extracted_count = extracted_count
                    .checked_add(1)
                    .ok_or_else(|| anyhow!("extracted path count overflow"))?;
                degraded_metadata_count = degraded_metadata_count
                    .checked_add(diagnostics.len() as u64)
                    .ok_or_else(|| anyhow!("degraded metadata count overflow"))?;
                emit_member_metadata_diagnostics(quiet, &path, &diagnostics)?;
            }
            emit_success_summary(
                quiet,
                &format!(
                    "extracted {extracted_count} file(s), {degraded_metadata_count} degraded metadata items to {}",
                    root.display()
                ),
            )?;
            Ok(())
        }
        Command::List {
            archive,
            password_stdin,
            password,
            keyfile,
            insecure_zero_key,
            bootstrap,
            volumes,
            long,
            json,
            jobs,
        } => {
            let reader_options = reader_options(resolve_jobs(jobs)?);
            reject_multi_volume_bootstrap(1 + volumes.len(), bootstrap.as_deref())?;
            if archive == "-" {
                reject_archive_stdin_list_options(
                    &volumes,
                    password_stdin,
                    password,
                    keyfile.as_deref(),
                    insecure_zero_key,
                )?;
                let bootstrap_bytes = read_optional_bootstrap_sidecar(bootstrap.as_deref())?;
                let stdin = io::stdin();
                let report = if let Some(keyfile) = keyfile.as_deref() {
                    let master_key = load_archive_stdin_key(
                        Some(keyfile),
                        password_stdin,
                        password,
                        insecure_zero_key,
                    )?;
                    if let Some(bootstrap_bytes) = bootstrap_bytes.as_deref() {
                        list_non_seekable_stream_with_bootstrap_sidecar(
                            stdin.lock(),
                            bootstrap_bytes,
                            &master_key,
                            non_seekable_reader_options(reader_options),
                        )
                    } else {
                        list_non_seekable_stream(
                            stdin.lock(),
                            &master_key,
                            non_seekable_reader_options(reader_options),
                        )
                    }
                } else {
                    if let Some(bootstrap_bytes) = bootstrap_bytes.as_deref() {
                        list_unencrypted_non_seekable_stream_with_bootstrap_sidecar(
                            stdin.lock(),
                            bootstrap_bytes,
                            non_seekable_reader_options(reader_options),
                        )
                    } else {
                        list_unencrypted_non_seekable_stream(
                            stdin.lock(),
                            non_seekable_reader_options(reader_options),
                        )
                    }
                }
                .context("failed to list non-seekable archive stream")?;
                emit_entry_metadata_diagnostics(quiet, &report.entries)?;
                if json {
                    let files = report
                        .entries
                        .iter()
                        .map(|entry| {
                            let kind = match entry.kind {
                                TarEntryKind::Regular => "file",
                                TarEntryKind::Directory => "directory",
                                TarEntryKind::Symlink => "symlink",
                                TarEntryKind::Hardlink => "hardlink",
                            };
                            json!({
                                "path": &entry.path,
                                "kind": kind,
                                "size": entry.file_data_size,
                                "mode": entry.mode,
                                "mtime": entry.mtime,
                            })
                        })
                        .collect::<Vec<_>>();
                    println!(
                        "{}",
                        serde_json::to_string(&json!({
                            "streaming_mode": "non-seekable",
                            "verification": {
                                "file_count": report.verification.file_count,
                                "tar_total_size": report.verification.tar_total_size,
                            },
                            "files": files,
                        }))
                        .context("failed to encode list output as JSON")?
                    );
                    return Ok(());
                }
                if long {
                    for entry in report.entries {
                        let kind = match entry.kind {
                            TarEntryKind::Regular => "file",
                            TarEntryKind::Directory => "directory",
                            TarEntryKind::Symlink => "symlink",
                            TarEntryKind::Hardlink => "hardlink",
                        };
                        println!(
                            "{}\t{}\t{}\t{}\t{}",
                            entry.file_data_size, kind, entry.mode, entry.mtime, entry.path
                        );
                    }
                    return Ok(());
                }
                for entry in report.entries {
                    println!("{}", entry.path);
                }
                return Ok(());
            }
            let selection = resolve_archive_input_paths(&archive, &volumes, bootstrap.is_none())?;
            let master_key = load_open_key_from_paths(
                keyfile.as_deref(),
                password_stdin,
                password,
                insecure_zero_key,
                &selection.paths,
            )?;
            let opened = open_selection_maybe_bootstrap(
                &selection,
                &master_key,
                bootstrap.as_deref(),
                reader_options,
            )
            .with_context(|| format!("failed to open archive {archive}"))?;
            if json || long {
                let entries = opened.list_files()?;
                emit_entry_metadata_diagnostics(quiet, &entries)?;
                if json {
                    let files = entries
                        .iter()
                        .map(|entry| {
                            let kind = match entry.kind {
                                TarEntryKind::Regular => "file",
                                TarEntryKind::Directory => "directory",
                                TarEntryKind::Symlink => "symlink",
                                TarEntryKind::Hardlink => "hardlink",
                            };
                            json!({
                                "path": &entry.path,
                                "kind": kind,
                                "size": entry.file_data_size,
                                "mode": entry.mode,
                                "mtime": entry.mtime,
                            })
                        })
                        .collect::<Vec<_>>();
                    println!(
                        "{}",
                        serde_json::to_string(&json!({ "files": files }))
                            .context("failed to encode list output as JSON")?
                    );
                    Ok(())
                } else {
                    for entry in entries {
                        let kind = match entry.kind {
                            TarEntryKind::Regular => "file",
                            TarEntryKind::Directory => "directory",
                            TarEntryKind::Symlink => "symlink",
                            TarEntryKind::Hardlink => "hardlink",
                        };
                        println!(
                            "{}\t{}\t{}\t{}\t{}",
                            entry.file_data_size, kind, entry.mode, entry.mtime, entry.path
                        );
                    }
                    Ok(())
                }
            } else {
                for entry in opened.list_index_entries()? {
                    println!("{}", entry.path);
                }
                Ok(())
            }
        }
        Command::Verify {
            archives,
            password_stdin,
            password,
            keyfile,
            insecure_zero_key,
            trusted_public_key,
            trusted_ca_cert,
            trusted_system_roots,
            public_no_key,
            bootstrap,
            json,
            write_repaired,
            jobs,
        } => {
            let reader_options = reader_options(resolve_jobs(jobs)?);
            let first = archives
                .first()
                .ok_or_else(|| anyhow!("at least one archive volume is required"))?;
            let archive_paths = archives.to_vec();
            if archives.iter().any(|path| path == "-") {
                if write_repaired {
                    let err = anyhow!(FormatError::ReaderUnsupported(
                        "--write-repaired is not supported for archive stdin",
                    ));
                    if json {
                        emit_verify_json_error(&archive_paths, None, None, &err)?;
                    }
                    return Err(err);
                }
                if json && archives.len() != 1 {
                    let err = anyhow!(FormatError::ReaderUnsupported(
                        "archive stdin must be the only archive input",
                    ));
                    emit_verify_json_error(&archive_paths, None, None, &err)?;
                    return Err(err);
                }
                if first != "-" || archives.len() != 1 {
                    return Err(anyhow!(FormatError::ReaderUnsupported(
                        "archive stdin must be the only archive input",
                    )));
                }
                if public_no_key {
                    let err = anyhow!(FormatError::ReaderUnsupported(
                        "public no-key verification is not supported for archive stdin",
                    ));
                    if json {
                        emit_verify_json_error(&archive_paths, None, None, &err)?;
                    }
                    return Err(err);
                }
                if trusted_public_key.is_some()
                    || !trusted_ca_cert.is_empty()
                    || trusted_system_roots
                {
                    let err = anyhow!(FormatError::ReaderUnsupported(
                        "RootAuth external verification is not supported for archive stdin",
                    ));
                    if json {
                        emit_verify_json_error(&archive_paths, None, None, &err)?;
                    }
                    return Err(err);
                }
                let bootstrap_bytes = match read_optional_bootstrap_sidecar(bootstrap.as_deref()) {
                    Ok(bootstrap_bytes) => bootstrap_bytes,
                    Err(err) => {
                        if json {
                            emit_verify_json_error(&archive_paths, None, None, &err)?;
                        }
                        return Err(err);
                    }
                };
                let stdin = io::stdin();
                let result = if let Some(keyfile) = keyfile.as_deref() {
                    let master_key = match load_archive_stdin_key(
                        Some(keyfile),
                        password_stdin,
                        password,
                        insecure_zero_key,
                    ) {
                        Ok(master_key) => master_key,
                        Err(err) => {
                            if json {
                                emit_verify_json_error(&archive_paths, None, None, &err)?;
                            }
                            return Err(err);
                        }
                    };
                    if let Some(bootstrap_bytes) = bootstrap_bytes.as_deref() {
                        verify_non_seekable_stream_with_bootstrap_sidecar(
                            stdin.lock(),
                            bootstrap_bytes,
                            &master_key,
                            non_seekable_reader_options(reader_options),
                        )
                    } else {
                        verify_non_seekable_stream_with_options(
                            stdin.lock(),
                            &master_key,
                            non_seekable_reader_options(reader_options),
                        )
                    }
                } else {
                    if let Some(bootstrap_bytes) = bootstrap_bytes.as_deref() {
                        verify_unencrypted_non_seekable_stream_with_bootstrap_sidecar(
                            stdin.lock(),
                            bootstrap_bytes,
                            non_seekable_reader_options(reader_options),
                        )
                    } else {
                        verify_unencrypted_non_seekable_stream_with_options(
                            stdin.lock(),
                            non_seekable_reader_options(reader_options),
                        )
                    }
                }
                .context("failed to verify non-seekable archive stream");
                let report = match result {
                    Ok(report) => report,
                    Err(err) => {
                        if json {
                            emit_verify_json_error(&archive_paths, None, None, &err)?;
                        }
                        return Err(err);
                    }
                };
                if json {
                    println!(
                        "{}",
                        serde_json::to_string(&json!({
                            "ok": true,
                            "archives": &archive_paths,
                            "verification_mode": "key-holding-non-seekable-stream",
                            "volume_count": report.total_volumes,
                            "file_count": report.file_count,
                            "tar_total_size": report.tar_total_size,
                        }))
                        .context("failed to encode verify output as JSON")?
                    );
                    return Ok(());
                }
                emit_success_stdout(
                    quiet,
                    &format!(
                        "-: OK non-seekable stream ({} volume(s), {} file(s))",
                        report.total_volumes, report.file_count
                    ),
                )?;
                return Ok(());
            }
            if public_no_key {
                if write_repaired {
                    let err = anyhow!(FormatError::ReaderUnsupported(
                        "--write-repaired requires key-holding verification",
                    ));
                    if json {
                        emit_verify_json_error(&archive_paths, None, None, &err)?;
                    }
                    return Err(err);
                }
                return run_public_no_key_verify(PublicNoKeyVerifyRequest {
                    archive_paths: &archive_paths,
                    trusted_public_key: trusted_public_key.as_deref(),
                    trusted_ca_cert: &trusted_ca_cert,
                    trusted_system_roots,
                    password_stdin,
                    password,
                    keyfile: keyfile.as_deref(),
                    insecure_zero_key,
                    bootstrap: bootstrap.as_deref(),
                    reader_options,
                    quiet,
                    json,
                });
            }
            if let Err(err) = validate_verify_key_holding_key_source(
                keyfile.as_deref(),
                password_stdin,
                password,
                insecure_zero_key,
            ) {
                if json {
                    emit_verify_json_error(&archive_paths, None, None, &err)?;
                }
                return Err(err);
            }
            if let Err(err) = reject_multi_volume_bootstrap(archives.len(), bootstrap.as_deref()) {
                if json {
                    emit_verify_json_error(&archive_paths, None, None, &err)?;
                }
                return Err(err);
            }
            if write_repaired && bootstrap.is_some() {
                let err = anyhow!(FormatError::ReaderUnsupported(
                    "--write-repaired is not supported with --bootstrap",
                ));
                if json {
                    emit_verify_json_error(&archive_paths, None, None, &err)?;
                }
                return Err(err);
            }
            let selection =
                match resolve_archive_input_paths(first, &archives[1..], bootstrap.is_none()) {
                    Ok(selection) => selection,
                    Err(err) => {
                        if json {
                            emit_verify_json_error(&archive_paths, None, None, &err)?;
                        }
                        return Err(err);
                    }
                };
            let archive_paths = selection.paths.clone();
            let master_key = match load_open_key_from_paths(
                keyfile.as_deref(),
                password_stdin,
                password,
                insecure_zero_key,
                &selection.paths,
            ) {
                Ok(master_key) => master_key,
                Err(err) => {
                    if json {
                        emit_verify_json_error(&archive_paths, None, None, &err)?;
                    }
                    return Err(err);
                }
            };
            let opened_selection = match open_selection_maybe_bootstrap_resolved(
                &selection,
                &master_key,
                bootstrap.as_deref(),
                reader_options,
            )
            .with_context(|| format!("failed to open archive {first}"))
            {
                Ok(opened) => opened,
                Err(err) => {
                    if json {
                        emit_verify_json_error(&archive_paths, None, None, &err)?;
                    }
                    return Err(err);
                }
            };
            let archive_paths = opened_selection.paths;
            let opened = opened_selection.opened;
            let result = opened
                .verify_content()
                .with_context(|| format!("failed to verify archive {first}"));
            let volume_count = opened.manifest_footer.total_volumes;
            let file_count = opened.index_root.header.file_count;
            match result {
                Ok(content_verification) => {
                    let root_auth = match verify_opened_root_auth(
                        &opened,
                        &content_verification,
                        trusted_public_key.as_deref(),
                        &trusted_ca_cert,
                        trusted_system_roots,
                    )
                    .with_context(|| format!("failed to verify RootAuth for {first}"))
                    {
                        Ok(root_auth) => root_auth,
                        Err(err) => {
                            if json {
                                emit_verify_json_error(
                                    &archive_paths,
                                    Some(volume_count as u64),
                                    Some(file_count),
                                    &err,
                                )?;
                            }
                            return Err(err);
                        }
                    };
                    let entries = opened.list_files()?;
                    emit_entry_metadata_diagnostics(quiet, &entries)?;
                    let repaired_outputs = if write_repaired {
                        match write_repaired_archive_copies(&archive_paths, &opened) {
                            Ok(outputs) => outputs,
                            Err(err) => {
                                if json {
                                    emit_verify_json_error(
                                        &archive_paths,
                                        Some(volume_count as u64),
                                        Some(file_count),
                                        &err,
                                    )?;
                                }
                                return Err(err);
                            }
                        }
                    } else {
                        Vec::new()
                    };
                    if json {
                        let mut payload = json!({
                            "ok": true,
                            "archives": &archive_paths,
                            "verification_mode": "key-holding",
                            "volume_count": volume_count,
                            "file_count": file_count,
                        });
                        if let Some(root_auth) = &root_auth {
                            payload["root_auth"] = root_auth_json(root_auth);
                        }
                        if write_repaired {
                            payload["repaired_outputs"] = json!(repaired_outputs
                                .iter()
                                .map(|output| json!({
                                    "path": output.path.clone(),
                                    "volume_index": output.volume_index,
                                    "repaired_block_count": output.repaired_block_count,
                                }))
                                .collect::<Vec<_>>());
                        }
                        println!(
                            "{}",
                            serde_json::to_string(&payload)
                                .context("failed to encode verify output as JSON")?
                        );
                        return Ok(());
                    }
                    emit_success_stdout(
                        quiet,
                        &format!(
                            "{}: OK ({} volume(s), {} file(s))",
                            first, volume_count, file_count
                        ),
                    )?;
                    if let Some(root_auth) = &root_auth {
                        emit_root_auth_stdout(quiet, root_auth)?;
                    }
                    if write_repaired {
                        if repaired_outputs.is_empty() {
                            emit_success_stdout(
                                quiet,
                                "no repaired output written; no recoverable block damage found",
                            )?;
                        } else {
                            for output in repaired_outputs {
                                emit_success_stdout(
                                    quiet,
                                    &format!(
                                        "wrote repaired volume copy {} ({} block(s))",
                                        output.path, output.repaired_block_count
                                    ),
                                )?;
                            }
                        }
                    }
                    Ok(())
                }
                Err(err) => {
                    if json {
                        emit_verify_json_error(
                            &archive_paths,
                            Some(volume_count as u64),
                            Some(file_count),
                            &err,
                        )?;
                    }
                    Err(err)
                }
            }
        }
        Command::Keygen {
            output,
            stdout,
            force,
        } => {
            let bytes = generate_random_key_material()?;
            let key_hex = format!("{}\n", encode_hex(&bytes));
            if stdout {
                print!("{}", key_hex);
                io::stdout().flush()?;
                return Ok(());
            }
            let output = output.expect("--output required by clap");
            write_keyfile(&output, &key_hex, force).context("failed to write keyfile")?;
            emit_success_summary(quiet, &format!("wrote keyfile to {}", output))?;
            Ok(())
        }
        Command::SigningKeygen {
            secret_output,
            public_output,
            force,
        } => {
            if Path::new(&secret_output) == Path::new(&public_output) {
                return Err(UsageError(
                    "--secret-output and --public-output must be different paths",
                )
                .into());
            }
            if !force {
                check_output_path_free("signing secret output", Path::new(&secret_output))?;
                check_output_path_free("signing public output", Path::new(&public_output))?;
            }
            let signing_key = generate_ed25519_signing_key();
            let secret_hex = format!("{}\n", encode_hex(&signing_key.to_bytes()));
            let public_hex = format!("{}\n", encode_hex(&signing_key.verifying_key().to_bytes()));
            write_atomic_output_file(
                "signing secret",
                Path::new(&secret_output),
                secret_hex.as_bytes(),
                force,
            )?;
            write_atomic_output_file(
                "signing public key",
                Path::new(&public_output),
                public_hex.as_bytes(),
                force,
            )?;
            emit_success_summary(
                quiet,
                &format!("wrote signing keypair to {secret_output} and {public_output}"),
            )?;
            Ok(())
        }
    }
}

fn emit_success_summary(quiet: bool, message: &str) -> io::Result<()> {
    if quiet {
        return Ok(());
    }
    eprintln!("{message}");
    Ok(())
}

fn emit_create_timing_report(
    scan_inputs: Duration,
    read_inputs: Duration,
    core_writer: Duration,
    write_outputs: Duration,
    total: Duration,
    writer: WriterTimings,
) -> io::Result<()> {
    let accounted = scan_inputs + read_inputs + core_writer + write_outputs;
    let other_cli = total.saturating_sub(accounted);
    eprintln!("create timings:");
    eprintln!("  scan inputs: {}", format_duration(scan_inputs));
    eprintln!("  read inputs: {}", format_duration(read_inputs));
    eprintln!("  core writer: {}", format_duration(core_writer));
    eprintln!("  write outputs: {}", format_duration(write_outputs));
    eprintln!("  other CLI: {}", format_duration(other_cli));
    eprintln!("  total: {}", format_duration(total));
    eprintln!("writer timings:");
    eprintln!("  plan payload: {}", format_duration(writer.plan_payload));
    eprintln!("  plan metadata: {}", format_duration(writer.plan_metadata));
    eprintln!("  emit payload: {}", format_duration(writer.emit_payload));
    eprintln!("  emit metadata: {}", format_duration(writer.emit_metadata));
    eprintln!("  total: {}", format_duration(writer.total));
    Ok(())
}

fn format_duration(duration: Duration) -> String {
    format!("{:.3}s", duration.as_secs_f64())
}

fn emit_success_stdout(quiet: bool, message: &str) -> io::Result<()> {
    if quiet {
        return Ok(());
    }
    println!("{message}");
    Ok(())
}

#[derive(Debug, Clone, Copy)]
struct CreateLayoutOverrides<'a> {
    chunk_size: Option<&'a str>,
    envelope_size: Option<&'a str>,
    block_size: Option<&'a str>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct CreateLayout {
    block_size: u32,
    chunk_size: u32,
    envelope_target_size: u32,
}

#[derive(Debug, Clone, Copy)]
struct CreateWriterOptionsArgs<'a> {
    volumes: Option<u32>,
    volume_size: Option<&'a str>,
    volume_loss_tolerance: u8,
    bit_rot_buffer_pct: u8,
    compression_level: i32,
    jobs: usize,
    layout_overrides: CreateLayoutOverrides<'a>,
    total_input_size: Option<u64>,
}

fn create_writer_options(args: CreateWriterOptionsArgs<'_>) -> Result<WriterOptions> {
    let layout = resolve_create_layout(args.layout_overrides, args.total_input_size)?;
    Ok(WriterOptions {
        stripe_width: args.volumes.unwrap_or(1),
        target_volume_size: args
            .volume_size
            .map(|value| parse_size(value).with_context(|| format!("invalid volume-size: {value}")))
            .transpose()?,
        volume_loss_tolerance: args.volume_loss_tolerance,
        bit_rot_buffer_pct: args.bit_rot_buffer_pct,
        zstd_level: args.compression_level,
        jobs: args.jobs,
        chunk_size: layout.chunk_size,
        envelope_target_size: layout.envelope_target_size,
        block_size: layout.block_size,
        ..WriterOptions::default()
    })
}

fn resolve_create_layout(
    overrides: CreateLayoutOverrides<'_>,
    total_input_size: Option<u64>,
) -> Result<CreateLayout> {
    let mut layout = default_create_layout(total_input_size);
    if let Some(value) = overrides.block_size {
        layout.block_size = parse_size_u32(value, "block-size")?;
    }
    if let Some(value) = overrides.envelope_size {
        layout.envelope_target_size = parse_size_u32(value, "envelope-size")?;
    }
    if let Some(value) = overrides.chunk_size {
        layout.chunk_size = parse_size_u32(value, "chunk-size")?;
        if overrides.envelope_size.is_none() && layout.chunk_size > layout.envelope_target_size {
            layout.envelope_target_size = layout.chunk_size;
        }
    }
    Ok(layout)
}

fn default_create_layout(total_input_size: Option<u64>) -> CreateLayout {
    match total_input_size {
        Some(size) if size <= LARGE_CREATE_LAYOUT_THRESHOLD => CreateLayout {
            block_size: 64 * 1024,
            chunk_size: 256 * 1024,
            envelope_target_size: 1024 * 1024,
        },
        Some(_) | None => CreateLayout {
            block_size: 1024 * 1024,
            chunk_size: 32 * 1024 * 1024,
            envelope_target_size: 64 * 1024 * 1024,
        },
    }
}

fn default_jobs() -> usize {
    std::thread::available_parallelism()
        .map(|jobs| jobs.get())
        .unwrap_or(1)
}

fn resolve_jobs(jobs: Option<usize>) -> Result<usize> {
    let jobs = jobs.unwrap_or_else(default_jobs);
    if jobs == 0 {
        return Err(UsageError("--jobs must be at least 1").into());
    }
    Ok(jobs)
}

fn reader_options(jobs: usize) -> ReaderOptions {
    ReaderOptions {
        jobs,
        ..ReaderOptions::default()
    }
}

fn non_seekable_reader_options(reader: ReaderOptions) -> NonSeekableReaderOptions {
    NonSeekableReaderOptions {
        reader,
        ..NonSeekableReaderOptions::default()
    }
}

fn metadata_diagnostic_line(path: &str, diagnostic: &MetadataDiagnostic) -> String {
    format!(
        "tzap: degraded-metadata: {}: {}: {}",
        path, diagnostic.profile, diagnostic.message
    )
}

fn emit_member_metadata_diagnostics(
    quiet: bool,
    path: &str,
    diagnostics: &[MetadataDiagnostic],
) -> io::Result<()> {
    if quiet {
        return Ok(());
    }
    for diagnostic in diagnostics {
        eprintln!("{}", metadata_diagnostic_line(path, diagnostic));
    }
    Ok(())
}

fn metadata_diagnostic_lines_for_entries(entries: &[ArchiveEntry]) -> Vec<String> {
    entries
        .iter()
        .flat_map(|entry| {
            entry
                .diagnostics
                .iter()
                .map(|diagnostic| metadata_diagnostic_line(&entry.path, diagnostic))
        })
        .collect()
}

#[cfg(test)]
fn metadata_diagnostic_lines_for_paths(entries: &[ArchiveEntry], paths: &[String]) -> Vec<String> {
    paths
        .iter()
        .filter_map(|path| entries.iter().find(|entry| entry.path == *path))
        .flat_map(|entry| {
            entry
                .diagnostics
                .iter()
                .map(|diagnostic| metadata_diagnostic_line(&entry.path, diagnostic))
        })
        .collect()
}

fn emit_entry_metadata_diagnostics(quiet: bool, entries: &[ArchiveEntry]) -> io::Result<()> {
    if quiet {
        return Ok(());
    }
    for line in metadata_diagnostic_lines_for_entries(entries) {
        eprintln!("{line}");
    }
    Ok(())
}

fn emit_verify_json_error(
    archives: &[String],
    volume_count: Option<u64>,
    file_count: Option<u64>,
    err: &anyhow::Error,
) -> Result<()> {
    let diagnostic = classify_error(err);
    let mut payload = json!({
        "ok": false,
        "archives": archives,
        "error": {
            "label": diagnostic.label,
            "action": diagnostic.action,
            "message": err.to_string(),
        },
    });
    if let Some(volume_count) = volume_count {
        payload["volume_count"] = json!(volume_count);
    }
    if let Some(file_count) = file_count {
        payload["file_count"] = json!(file_count);
    }
    println!(
        "{}",
        serde_json::to_string(&payload).context("failed to encode verify error output as JSON")?
    );
    Ok(())
}

#[derive(Debug)]
struct InputFile {
    archive_path: String,
    contents: Vec<u8>,
    mode: u32,
    mtime: u64,
}

#[derive(Debug)]
struct InputSpec {
    source: PathBuf,
    archive_path: String,
    mode: u32,
    mtime: u64,
    size: u64,
}

impl RegularFileSource for InputSpec {
    fn archive_path(&self) -> &str {
        &self.archive_path
    }

    fn file_data_size(&self) -> u64 {
        self.size
    }

    fn mode(&self) -> u32 {
        self.mode
    }

    fn mtime(&self) -> u64 {
        self.mtime
    }

    fn open(&self) -> std::result::Result<Box<dyn Read + '_>, ArchiveWriteError> {
        File::open(&self.source)
            .map(|file| Box::new(file) as Box<dyn Read + '_>)
            .map_err(ArchiveWriteError::Io)
    }
}

#[derive(Debug)]
struct CreateKey {
    master_key: MasterKey,
    kdf_params: KdfParams,
}

#[derive(Debug)]
enum CreateRootAuthProfile {
    Ed25519 {
        signing_key: SigningKey,
        signer_identity: [u8; 32],
    },
    X509(X509RootAuthSigner),
}

#[derive(Debug)]
enum VerifiedRootAuth {
    Ed25519(RootAuthVerification),
    X509 {
        verification: RootAuthVerification,
        report: X509RootAuthReport,
    },
}

#[derive(Debug)]
enum PublicNoKeyTrust {
    Ed25519 {
        public_key: [u8; 32],
    },
    X509 {
        trusted_roots_der: Vec<Vec<u8>>,
        trusted_system_roots: bool,
    },
}

#[derive(Debug)]
enum VerifiedPublicNoKeyRootAuth {
    Ed25519(PublicNoKeyVerification),
    X509 {
        verification: PublicNoKeyVerification,
        report: X509RootAuthReport,
    },
}

#[derive(Debug, Clone, Copy)]
enum CreateStdinMode {
    Tar,
    RawUnknownSize,
    RawKnownSize,
    RawSpool,
}

#[derive(Debug)]
struct CreateStdinArgs<'a> {
    tar_stdin: bool,
    raw_stdin: bool,
    stdin_name: Option<&'a str>,
    stdin_size: Option<&'a str>,
    spool_stdin: bool,
    paths: &'a [String],
    password_stdin: bool,
    password: bool,
    has_dictionary: bool,
    volumes: Option<u32>,
    volume_size: Option<&'a str>,
    volume_loss_tolerance: Option<u8>,
}

#[derive(Debug, Clone, Copy)]
struct Diagnostic {
    label: &'static str,
    exit_code: u8,
    action: &'static str,
}

#[derive(Debug)]
struct UsageError(&'static str);

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

impl std::error::Error for UsageError {}

fn collect_input_specs(paths: &[String]) -> Result<Vec<InputSpec>> {
    let mut out = Vec::new();
    for path in paths {
        let input = PathBuf::from(path);
        let base = input
            .file_name()
            .and_then(OsStr::to_str)
            .ok_or_else(|| anyhow!("input path has no valid UTF-8 file name: {path}"))?
            .to_owned();
        collect_one_input_spec(&input, Path::new(&base), &mut out)
            .with_context(|| format!("failed to collect input {path}"))?;
    }
    out.sort_by(|left, right| left.archive_path.cmp(&right.archive_path));
    Ok(out)
}

fn input_specs_total_size(specs: &[InputSpec]) -> Result<u64> {
    specs.iter().try_fold(0u64, |sum, entry| {
        sum.checked_add(entry.size)
            .ok_or_else(|| anyhow!("input byte count overflow"))
    })
}

fn collect_input_files(specs: &[InputSpec]) -> Result<Vec<InputFile>> {
    let mut out = Vec::new();
    for spec in specs {
        let contents = fs::read(&spec.source)
            .with_context(|| format!("failed to read input {}", spec.source.display()))?;
        out.push(InputFile {
            archive_path: spec.archive_path.clone(),
            contents,
            mode: spec.mode,
            mtime: spec.mtime,
        });
    }
    Ok(out)
}

fn collect_one_input_spec(
    input: &Path,
    archive_path: &Path,
    out: &mut Vec<InputSpec>,
) -> Result<()> {
    let metadata = fs::symlink_metadata(input)
        .with_context(|| format!("failed to inspect input {}", input.display()))?;
    if metadata.file_type().is_symlink() {
        bail!("refusing to archive symlink input {}", input.display());
    }
    if metadata.is_dir() {
        let mut entries = fs::read_dir(input)
            .with_context(|| format!("failed to read directory {}", input.display()))?
            .collect::<Result<Vec<_>, _>>()?;
        entries.sort_by_key(|entry| entry.file_name());
        for entry in entries {
            let child_name = entry
                .file_name()
                .into_string()
                .map_err(|_| anyhow!("input path is not valid UTF-8"))?;
            collect_one_input_spec(&entry.path(), &archive_path.join(child_name), out)?;
        }
        return Ok(());
    }
    if !metadata.is_file() {
        bail!("unsupported input type {}", input.display());
    }
    let archive_path = archive_path_to_string(archive_path)?;
    out.push(InputSpec {
        source: input.to_owned(),
        archive_path,
        mode: readonly_mode(&metadata),
        mtime: 0,
        size: metadata.len(),
    });
    Ok(())
}

fn resolve_extract_index_entries(
    opened: &OpenedArchive,
    requested: &[String],
) -> Result<(Vec<ArchiveIndexEntry>, Vec<String>)> {
    if requested.is_empty() {
        return Ok((opened.list_index_entries()?, Vec::new()));
    }

    let mut resolved = Vec::with_capacity(requested.len());
    let mut missing = Vec::new();
    for path in requested {
        match opened.lookup_index_entry(path)? {
            Some(entry) => resolved.push(entry),
            None => missing.push(path.clone()),
        }
    }
    Ok((resolved, missing))
}

fn extract_entries_to_dir_parallel(
    opened: &OpenedArchive,
    entries: Vec<ArchiveIndexEntry>,
    root: &Path,
    options: SafeExtractionOptions,
    jobs: usize,
) -> Result<Vec<(String, Vec<MetadataDiagnostic>)>> {
    if entries.is_empty() {
        return Ok(Vec::new());
    }
    if jobs <= 1 || entries.len() <= 1 {
        return entries
            .into_iter()
            .map(|entry| {
                let path = entry.path;
                let diagnostics = opened
                    .extract_file_to(&path, root, options)?
                    .ok_or_else(|| anyhow!("path not found in archive: {path}"))?;
                Ok((path, diagnostics))
            })
            .collect();
    }

    let worker_count = jobs.min(entries.len());
    let chunk_size = entries.len().div_ceil(worker_count);
    std::thread::scope(|scope| {
        let handles = entries
            .chunks(chunk_size)
            .map(|chunk| {
                let chunk = chunk.to_vec();
                scope.spawn(move || {
                    chunk
                        .into_iter()
                        .map(|entry| {
                            let path = entry.path;
                            let diagnostics = opened
                                .extract_file_to(&path, root, options)?
                                .ok_or_else(|| anyhow!("path not found in archive: {path}"))?;
                            Ok((path, diagnostics))
                        })
                        .collect::<Result<Vec<_>>>()
                })
            })
            .collect::<Vec<_>>();
        let mut out = Vec::new();
        for handle in handles {
            let mut chunk = handle
                .join()
                .map_err(|_| anyhow!("extract worker panicked"))??;
            out.append(&mut chunk);
        }
        Ok(out)
    })
}

fn archive_path_to_string(path: &Path) -> Result<String> {
    let mut parts = Vec::new();
    for component in path.components() {
        let std::path::Component::Normal(part) = component else {
            bail!("unsafe archive path component in {}", path.display());
        };
        parts.push(
            part.to_str()
                .ok_or_else(|| anyhow!("archive path is not valid UTF-8"))?
                .to_owned(),
        );
    }
    if parts.is_empty() {
        bail!("empty archive path");
    }
    Ok(parts.join("/"))
}

#[cfg(unix)]
fn readonly_mode(metadata: &fs::Metadata) -> u32 {
    use std::os::unix::fs::PermissionsExt;
    metadata.permissions().mode() & 0o777
}

#[cfg(not(unix))]
fn readonly_mode(_metadata: &fs::Metadata) -> u32 {
    0o644
}

fn write_archive_outputs(output: &str, volumes: &[Vec<u8>], force: bool) -> Result<()> {
    if volumes.is_empty() {
        bail!("writer returned no volumes");
    }
    let output_paths = create_output_paths(output, volumes.len());
    let mut temps = create_archive_output_temps(&output_paths)?;
    for ((temp, output_path), volume) in temps.iter_mut().zip(&output_paths).zip(volumes) {
        temp.as_file_mut().write_all(volume).with_context(|| {
            format!(
                "failed to write temporary archive volume for {}",
                output_path.display()
            )
        })?;
    }
    flush_archive_output_temps(&mut temps, &output_paths)?;
    publish_archive_output_temps(temps, &output_paths, force)?;
    Ok(())
}

struct PathBackedArchiveSink<'a> {
    temps: &'a mut [tempfile::NamedTempFile],
    bootstrap_sidecar: Vec<u8>,
}

impl ArchiveWriteSink for PathBackedArchiveSink<'_> {
    fn begin_archive(&mut self, volume_count: usize) -> std::result::Result<(), ArchiveWriteError> {
        if volume_count != self.temps.len() {
            return Err(FormatError::WriterInvariant(
                "stdin file sink volume count does not match output paths",
            )
            .into());
        }
        for temp in self.temps.iter_mut() {
            let file = temp.as_file_mut();
            file.set_len(0).map_err(ArchiveWriteError::Io)?;
            file.seek(SeekFrom::Start(0))
                .map_err(ArchiveWriteError::Io)?;
        }
        self.bootstrap_sidecar.clear();
        Ok(())
    }

    fn write_volume(
        &mut self,
        volume_index: usize,
        bytes: &[u8],
    ) -> std::result::Result<(), ArchiveWriteError> {
        let temp = self
            .temps
            .get_mut(volume_index)
            .ok_or(FormatError::WriterInvariant(
                "stdin file sink volume index is out of bounds",
            ))?;
        temp.as_file_mut()
            .write_all(bytes)
            .map_err(ArchiveWriteError::Io)
    }

    fn write_bootstrap_sidecar(
        &mut self,
        bytes: &[u8],
    ) -> std::result::Result<(), ArchiveWriteError> {
        self.bootstrap_sidecar.extend_from_slice(bytes);
        Ok(())
    }
}

fn write_tar_stdin_archive_output(
    output: &str,
    key: &CreateKey,
    options: WriterOptions,
    root_auth: Option<RootAuthWriterConfig<'_>>,
    root_auth_profile: Option<&CreateRootAuthProfile>,
    force: bool,
) -> Result<(StreamingTarWriterSummary, Vec<u8>)> {
    let stdin = io::stdin();
    let mut stdin_lock = stdin.lock();
    if let (Some(profile), Some(root_auth)) = (root_auth_profile, root_auth) {
        let mut authenticator =
            |request: &RootAuthSigningRequest| root_auth_authenticator_value(profile, request);
        return write_tar_stdin_archive_output_from_reader(
            output,
            &mut stdin_lock,
            key,
            options,
            Some(root_auth),
            Some(&mut authenticator),
            force,
        );
    }
    write_tar_stdin_archive_output_from_reader(
        output,
        &mut stdin_lock,
        key,
        options,
        None,
        None,
        force,
    )
}

fn write_tar_stdin_archive_output_from_reader<R: Read>(
    output: &str,
    reader: &mut R,
    key: &CreateKey,
    options: WriterOptions,
    root_auth: Option<RootAuthWriterConfig<'_>>,
    authenticator: Option<&mut CliRootAuthAuthenticator<'_>>,
    force: bool,
) -> Result<(StreamingTarWriterSummary, Vec<u8>)> {
    let volume_count = options.stripe_width as usize;
    write_stdin_archive_output_with_sink(output, volume_count, force, |sink| {
        write_tar_stream_archive_to_sink_with_kdf_and_root_auth(
            reader,
            &key.master_key,
            options,
            &key.kdf_params,
            root_auth,
            authenticator,
            sink,
        )
    })
}

#[allow(clippy::too_many_arguments)]
fn write_raw_stdin_archive_output<R: Read>(
    output: &str,
    mut reader: R,
    archive_path: &str,
    input_size: u64,
    key: &CreateKey,
    options: WriterOptions,
    root_auth: Option<RootAuthWriterConfig<'_>>,
    root_auth_profile: Option<&CreateRootAuthProfile>,
    force: bool,
) -> Result<(StreamingRawWriterSummary, Vec<u8>)> {
    if let (Some(profile), Some(root_auth)) = (root_auth_profile, root_auth) {
        let mut authenticator =
            |request: &RootAuthSigningRequest| root_auth_authenticator_value(profile, request);
        return write_raw_stdin_archive_output_from_reader(
            output,
            &mut reader,
            archive_path,
            input_size,
            key,
            options,
            Some(root_auth),
            Some(&mut authenticator),
            force,
        );
    }
    write_raw_stdin_archive_output_from_reader(
        output,
        &mut reader,
        archive_path,
        input_size,
        key,
        options,
        None,
        None,
        force,
    )
}

fn write_file_inputs_ordered_parallel_to_memory(
    input_specs: &[InputSpec],
    key: &CreateKey,
    options: WriterOptions,
    root_auth: Option<RootAuthWriterConfig<'_>>,
    root_auth_profile: Option<&CreateRootAuthProfile>,
) -> Result<(WrittenArchiveSummary, MemoryArchiveSink)> {
    if let (Some(profile), Some(root_auth)) = (root_auth_profile, root_auth) {
        let mut authenticator =
            |request: &RootAuthSigningRequest| root_auth_authenticator_value(profile, request);
        return write_file_inputs_ordered_parallel_to_memory_with_authenticator(
            input_specs,
            key,
            options,
            Some(root_auth),
            Some(&mut authenticator),
        );
    }
    write_file_inputs_ordered_parallel_to_memory_with_authenticator(
        input_specs,
        key,
        options,
        None,
        None,
    )
}

fn write_file_inputs_ordered_parallel_to_memory_with_authenticator(
    input_specs: &[InputSpec],
    key: &CreateKey,
    options: WriterOptions,
    root_auth: Option<RootAuthWriterConfig<'_>>,
    authenticator: Option<&mut CliRootAuthAuthenticator<'_>>,
) -> Result<(WrittenArchiveSummary, MemoryArchiveSink)> {
    let mut sink = MemoryArchiveSink::default();
    let summary = write_archive_sources_to_sink_ordered_parallel(
        input_specs,
        &key.master_key,
        options,
        &key.kdf_params,
        root_auth,
        authenticator,
        &mut sink,
    )?;
    Ok((summary, sink))
}

#[allow(clippy::too_many_arguments)]
fn write_raw_stdin_archive_output_from_reader<R: Read>(
    output: &str,
    reader: &mut R,
    archive_path: &str,
    input_size: u64,
    key: &CreateKey,
    options: WriterOptions,
    root_auth: Option<RootAuthWriterConfig<'_>>,
    authenticator: Option<&mut CliRootAuthAuthenticator<'_>>,
    force: bool,
) -> Result<(StreamingRawWriterSummary, Vec<u8>)> {
    let volume_count = options.stripe_width as usize;
    write_stdin_archive_output_with_sink(output, volume_count, force, |sink| {
        write_sized_raw_member_archive_to_sink_with_kdf_and_root_auth(
            reader,
            archive_path,
            input_size,
            &key.master_key,
            options,
            &key.kdf_params,
            root_auth,
            authenticator,
            sink,
        )
    })
}

fn write_stdin_archive_output_with_sink<T>(
    output: &str,
    volume_count: usize,
    force: bool,
    write_archive: impl FnOnce(
        &mut PathBackedArchiveSink<'_>,
    ) -> std::result::Result<T, ArchiveWriteError>,
) -> Result<(T, Vec<u8>)> {
    if volume_count == 0 {
        bail!("writer returned no volumes");
    }
    let output_paths = create_output_paths(output, volume_count);
    let mut temps = create_archive_output_temps(&output_paths)?;
    let (summary, bootstrap_sidecar) = {
        let mut sink = PathBackedArchiveSink {
            temps: temps.as_mut_slice(),
            bootstrap_sidecar: Vec::new(),
        };
        let summary = write_archive(&mut sink)?;
        (summary, sink.bootstrap_sidecar)
    };
    flush_archive_output_temps(&mut temps, &output_paths)?;
    publish_archive_output_temps(temps, &output_paths, force)?;
    Ok((summary, bootstrap_sidecar))
}

fn create_archive_output_temps(output_paths: &[PathBuf]) -> Result<Vec<tempfile::NamedTempFile>> {
    output_paths
        .iter()
        .map(|output_path| {
            let parent = output_path
                .parent()
                .filter(|path| !path.as_os_str().is_empty())
                .unwrap_or_else(|| Path::new("."));
            tempfile::Builder::new()
                .prefix(".tzap-create-")
                .suffix(".partial")
                .tempfile_in(parent)
                .with_context(|| {
                    format!(
                        "failed to create temporary archive output in {}",
                        parent.display()
                    )
                })
        })
        .collect()
}

fn flush_archive_output_temps(
    temps: &mut [tempfile::NamedTempFile],
    output_paths: &[PathBuf],
) -> Result<()> {
    for (temp, output_path) in temps.iter_mut().zip(output_paths) {
        temp.as_file_mut().flush().with_context(|| {
            format!(
                "failed to flush temporary archive for {}",
                output_path.display()
            )
        })?;
        temp.as_file_mut().sync_all().with_context(|| {
            format!(
                "failed to sync temporary archive for {}",
                output_path.display()
            )
        })?;
    }
    Ok(())
}

fn publish_archive_output_temps(
    temps: Vec<tempfile::NamedTempFile>,
    output_paths: &[PathBuf],
    force: bool,
) -> Result<()> {
    let volume_count = output_paths.len();
    let publish_order = if volume_count == 1 {
        vec![0]
    } else {
        (1..volume_count).chain(std::iter::once(0)).collect()
    };
    let mut temp_slots = temps.into_iter().map(Some).collect::<Vec<_>>();
    let mut persisted_paths = Vec::new();
    for volume_index in publish_order {
        let temp = temp_slots[volume_index]
            .take()
            .ok_or_else(|| anyhow!("missing temporary archive volume {volume_index}"))?;
        let output_path = &output_paths[volume_index];
        let publish_result = if force {
            temp.persist(output_path)
        } else {
            temp.persist_noclobber(output_path)
        };
        if let Err(error) = publish_result {
            for path in &persisted_paths {
                let _ = fs::remove_file(path);
            }
            return Err(error.error)
                .with_context(|| format!("failed to publish archive {}", output_path.display()));
        }
        persisted_paths.push(output_path.clone());
    }
    Ok(())
}

fn write_bootstrap_output(path: &str, bytes: &[u8], force: bool) -> Result<()> {
    write_atomic_output_file("bootstrap output", Path::new(path), bytes, force)
}

fn reject_create_stdout_sentinels(output: &str, bootstrap_out: Option<&str>) -> Result<()> {
    if output == "-" {
        return Err(anyhow!(FormatError::WriterUnsupported(
            "--output - is not archive stdout; create output must be a file path",
        )));
    }
    if matches!(bootstrap_out, Some("-")) {
        return Err(anyhow!(FormatError::WriterUnsupported(
            "--bootstrap-out - is not sidecar stdout; sidecar output must be a file path",
        )));
    }
    Ok(())
}

fn validate_create_stdin_mode(args: CreateStdinArgs<'_>) -> Result<Option<CreateStdinMode>> {
    if args.tar_stdin && args.raw_stdin {
        return Err(anyhow!(FormatError::WriterUnsupported(
            "--tar-stdin and --raw-stdin cannot be used together",
        )));
    }
    if args.spool_stdin && !args.raw_stdin {
        return Err(anyhow!(FormatError::WriterUnsupported(
            "--spool-stdin requires --raw-stdin",
        )));
    }
    if args.stdin_name.is_some() && !args.raw_stdin {
        return Err(anyhow!(FormatError::WriterUnsupported(
            "--stdin-name requires --raw-stdin",
        )));
    }
    if args.stdin_size.is_some() && !args.raw_stdin {
        return Err(anyhow!(FormatError::WriterUnsupported(
            "--stdin-size requires --raw-stdin",
        )));
    }

    let Some(mode) = (if args.tar_stdin {
        Some(CreateStdinMode::Tar)
    } else if args.raw_stdin && args.spool_stdin {
        Some(CreateStdinMode::RawSpool)
    } else if args.raw_stdin && args.stdin_size.is_some() {
        Some(CreateStdinMode::RawKnownSize)
    } else if args.raw_stdin {
        Some(CreateStdinMode::RawUnknownSize)
    } else {
        None
    }) else {
        return Ok(None);
    };

    if args.paths != ["-"] {
        return Err(anyhow!(FormatError::WriterUnsupported(
            "stdin create modes require exactly one archive input path: -",
        )));
    }
    if args.password_stdin {
        return Err(anyhow!(FormatError::WriterUnsupported(
            "--password-stdin cannot be used when stdin carries archive payload bytes",
        )));
    }
    if args.password {
        return Err(anyhow!(FormatError::WriterUnsupported(
            "--password cannot be used when stdin carries archive payload bytes",
        )));
    }
    if args.has_dictionary {
        return Err(anyhow!(FormatError::WriterUnsupported(
            "--dictionary is not supported with stdin create modes",
        )));
    }
    if args.volume_size.is_some() {
        return Err(anyhow!(FormatError::WriterUnsupported(
            "--volume-size is not supported with stdin create modes",
        )));
    }
    if args.volume_loss_tolerance.unwrap_or(0) != 0 {
        return Err(anyhow!(FormatError::WriterUnsupported(
            "--volume-loss-tolerance > 0 is not supported with stdin create modes",
        )));
    }
    if matches!(args.volumes, Some(volumes) if volumes > 1)
        && !matches!(
            mode,
            CreateStdinMode::Tar | CreateStdinMode::RawKnownSize | CreateStdinMode::RawSpool
        )
    {
        return Err(anyhow!(FormatError::WriterUnsupported(
            "--volumes > 1 is supported only with --tar-stdin, known-size --raw-stdin, or --raw-stdin --spool-stdin",
        )));
    }

    match mode {
        CreateStdinMode::Tar => {
            if args.stdin_name.is_some() || args.stdin_size.is_some() || args.spool_stdin {
                return Err(anyhow!(FormatError::WriterUnsupported(
                    "--stdin-name, --stdin-size, and --spool-stdin require --raw-stdin",
                )));
            }
        }
        CreateStdinMode::RawUnknownSize => {
            if args.stdin_name.is_none() {
                return Err(anyhow!(FormatError::WriterUnsupported(
                    "--raw-stdin requires --stdin-name PATH",
                )));
            }
        }
        CreateStdinMode::RawKnownSize => {
            if args.stdin_name.is_none() {
                return Err(anyhow!(FormatError::WriterUnsupported(
                    "--raw-stdin requires --stdin-name PATH",
                )));
            }
            parse_size(args.stdin_size.expect("checked raw known-size stdin"))
                .with_context(|| UsageError("invalid stdin-size"))?;
        }
        CreateStdinMode::RawSpool => {
            if args.stdin_name.is_none() {
                return Err(anyhow!(FormatError::WriterUnsupported(
                    "--raw-stdin requires --stdin-name PATH",
                )));
            }
            if args.stdin_size.is_some() {
                return Err(anyhow!(FormatError::WriterUnsupported(
                    "--spool-stdin is for unknown-size raw stdin; omit --stdin-size",
                )));
            }
        }
    }

    Ok(Some(mode))
}

fn ensure_create_output_paths_can_be_written(
    output: &str,
    volumes: Option<u32>,
    has_volume_size: bool,
    bootstrap_out: Option<&str>,
    force: bool,
) -> Result<()> {
    if let Some(volumes) = volumes {
        if volumes == 0 {
            bail!("--volumes must be at least 1");
        }
        if !force && volumes == 1 {
            check_output_path_free("archive output", Path::new(output))?;
        }
        if !force && volumes > 1 {
            let paths = create_output_paths(output, volumes as usize);
            check_archive_paths_free_for_write(&paths)?;
        }
        if let Some(path) = bootstrap_out {
            if !force {
                check_output_path_free("bootstrap output", Path::new(path))?;
            }
        }
        return Ok(());
    }
    if has_volume_size {
        if !force {
            check_output_path_collisions_for_volume_size_output(output)?;
            if let Some(path) = bootstrap_out {
                check_output_path_free("bootstrap output", Path::new(path))?;
            }
        }
        return Ok(());
    }
    if !force {
        check_output_path_free("archive output", Path::new(output))?;
        if let Some(path) = bootstrap_out {
            check_output_path_free("bootstrap output", Path::new(path))?;
        }
    }
    Ok(())
}

fn check_output_path_collisions_for_volume_size_output(output: &str) -> Result<()> {
    check_output_path_free("archive output", Path::new(output))?;
    let output_path = Path::new(output);
    let parent = output_path.parent().unwrap_or_else(|| Path::new("."));
    let base = multi_volume_base_name(output)?;
    let entries = match fs::read_dir(parent) {
        Ok(entries) => entries,
        Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
        Err(err) => {
            return Err(err).with_context(|| {
                format!("failed to inspect output directory {}", parent.display())
            })
        }
    };
    for entry in entries.filter_map(|entry| entry.ok()) {
        let name = entry.file_name();
        let name = name.to_string_lossy();
        if looks_like_tzap_volume(&name, &base) {
            bail!(
                "output path collision: {base}.volNNN.tzap already exists; use --force to overwrite"
            );
        }
    }
    Ok(())
}

fn looks_like_tzap_volume(path_name: &str, base: &str) -> bool {
    let Some(rest) = path_name.strip_prefix(base) else {
        return false;
    };
    let Some(digits) = rest
        .strip_prefix(".vol")
        .and_then(|rest| rest.strip_suffix(".tzap"))
    else {
        return false;
    };
    !digits.is_empty() && digits.bytes().all(|byte| byte.is_ascii_digit())
}

fn check_archive_paths_free_for_write(paths: &[PathBuf]) -> Result<()> {
    for path in paths {
        check_output_path_free("archive output", path)?;
    }
    Ok(())
}

fn resolve_create_volume_loss_tolerance(
    explicit: Option<u8>,
    volumes: Option<u32>,
    volume_size: Option<&str>,
    stdin_payload_mode_requested: bool,
) -> u8 {
    explicit.unwrap_or_else(|| {
        if stdin_payload_mode_requested || (volumes.unwrap_or(1) <= 1 && volume_size.is_none()) {
            0
        } else {
            1
        }
    })
}

fn validate_create_writer_options(options: &WriterOptions) -> Result<()> {
    if options.block_size < 4096 || options.block_size % 2 != 0 {
        return Err(anyhow!(FormatError::WriterUnsupported(
            "writer requires an even block size of at least 4096",
        )));
    }
    if options.stripe_width == 0 {
        return Err(anyhow!(FormatError::WriterUnsupported(
            "stripe_width must be non-zero",
        )));
    }
    let effective_stripe_width = if options.target_volume_size.is_some() {
        options
            .stripe_width
            .max(options.volume_loss_tolerance as u32 + 1)
    } else {
        options.stripe_width
    };
    if options.volume_loss_tolerance as u32 >= effective_stripe_width {
        return Err(anyhow!(FormatError::WriterUnsupported(
            "volume_loss_tolerance must be less than stripe_width",
        )));
    }
    if effective_stripe_width == 1 && options.volume_loss_tolerance != 0 {
        return Err(anyhow!(FormatError::WriterUnsupported(
            "single-volume archives cannot tolerate volume loss",
        )));
    }
    if matches!(options.target_volume_size, Some(0)) {
        return Err(anyhow!(FormatError::WriterUnsupported(
            "target_volume_size must be non-zero",
        )));
    }
    if options.bit_rot_buffer_pct > 100 {
        return Err(anyhow!(FormatError::WriterUnsupported(
            "bit_rot_buffer_pct must be at most 100",
        )));
    }
    if options.chunk_size == 0 || options.chunk_size > options.envelope_target_size {
        return Err(anyhow!(FormatError::WriterUnsupported(
            "chunk_size must be non-zero and no larger than envelope_target_size",
        )));
    }
    Ok(())
}

fn check_output_path_free(label: &str, path: &Path) -> Result<()> {
    if path.as_os_str().is_empty() {
        return Ok(());
    }
    if path.exists() {
        bail!(
            "{label} already exists: {}; use --force to overwrite",
            path.display()
        );
    }
    Ok(())
}

fn create_output_paths(output: &str, volume_count: usize) -> Vec<PathBuf> {
    if volume_count == 1 {
        vec![PathBuf::from(output)]
    } else {
        (0..volume_count)
            .map(|index| create_volume_output_path(output, index))
            .collect()
    }
}

fn create_volume_output_path(output: &str, index: usize) -> PathBuf {
    let output_path = Path::new(output);
    let base = multi_volume_base_name(output).unwrap_or_else(|_| output.to_owned());
    let file_name = format!("{base}.vol{index:03}.tzap");
    match output_path.parent() {
        Some(parent) if !parent.as_os_str().is_empty() => parent.join(file_name),
        _ => PathBuf::from(file_name),
    }
}

fn multi_volume_base_name(output: &str) -> Result<String> {
    let file_name = Path::new(output)
        .file_name()
        .and_then(|name| name.to_str())
        .ok_or_else(|| anyhow!("output path has invalid UTF-8: {output}"))?;
    Ok(file_name
        .strip_suffix(".tzap")
        .unwrap_or(file_name)
        .to_owned())
}

fn create_dry_run_output_paths(
    output: &str,
    volumes: Option<u32>,
    has_volume_size: bool,
) -> Vec<String> {
    if let Some(volumes) = volumes {
        return create_output_paths(output, volumes as usize)
            .into_iter()
            .map(|path| path.display().to_string())
            .collect();
    }
    if has_volume_size {
        let first = create_volume_output_path(output, 0);
        let second = create_volume_output_path(output, 1);
        return vec![
            format!("{output} (if one volume is emitted)"),
            format!("{}, {}, ... (if split)", first.display(), second.display()),
        ];
    }
    vec![output.to_owned()]
}

fn describe_planned_volume_mode(volumes: Option<u32>, volume_size: Option<&str>) -> String {
    if let Some(volumes) = volumes {
        return format!("{volumes} explicit volume(s) requested");
    }
    if let Some(size) = volume_size {
        return format!("volume-size mode, target size {size}");
    }
    "single volume".to_string()
}

fn create_key_mode_label(
    keyfile: Option<&str>,
    password_stdin: bool,
    password: bool,
    no_encryption: bool,
    insecure_zero_key: bool,
) -> String {
    if password_stdin {
        return "password-stdin".to_string();
    }
    if password {
        return "password".to_string();
    }
    if keyfile.is_some() {
        return "keyfile".to_string();
    }
    if no_encryption {
        return "no-encryption".to_string();
    }
    if insecure_zero_key {
        return "insecure-zero-key".to_string();
    }
    "unknown".to_string()
}

fn removed_insecure_zero_key_error() -> UsageError {
    UsageError("--insecure-zero-key was removed in v43; use --no-encryption for plaintext archives")
}

fn validate_create_key_source(
    keyfile: Option<&str>,
    password_stdin: bool,
    password: bool,
    no_encryption: bool,
    insecure_zero_key: bool,
) -> Result<()> {
    if insecure_zero_key {
        return Err(removed_insecure_zero_key_error().into());
    }
    let count = usize::from(keyfile.is_some())
        + usize::from(password_stdin)
        + usize::from(password)
        + usize::from(no_encryption);
    if count == 0 {
        return Err(UsageError(
            "no key source provided; use --password-stdin, --password, --keyfile PATH, or --no-encryption",
        )
        .into());
    }
    if count > 1 {
        return Err(UsageError(
            "create accepts exactly one protection mode: --keyfile, --password, --password-stdin, or --no-encryption",
        )
        .into());
    }
    Ok(())
}

fn create_root_auth_mode_label(signing_key: Option<&str>, signing_cert: Option<&str>) -> String {
    if signing_key.is_some() {
        return "ed25519".to_string();
    }
    if signing_cert.is_some() {
        return "x509".to_string();
    }
    "unsigned".to_string()
}

impl CreateRootAuthProfile {
    fn label(&self) -> &'static str {
        match self {
            Self::Ed25519 { .. } => "ed25519",
            Self::X509(_) => "x509",
        }
    }

    fn root_auth_writer_config(&self) -> Result<RootAuthWriterConfig<'_>> {
        match self {
            Self::Ed25519 {
                signer_identity, ..
            } => Ok(RootAuthWriterConfig {
                authenticator_id: ED25519_AUTHENTICATOR_ID,
                signer_identity_type: 1,
                signer_identity,
                authenticator_value_length: ED25519_AUTHENTICATOR_VALUE_LEN,
            }),
            Self::X509(signer) => signer.root_auth_writer_config().map_err(Into::into),
        }
    }
}

fn root_auth_authenticator_value(
    profile: &CreateRootAuthProfile,
    request: &RootAuthSigningRequest,
) -> Result<Vec<u8>, FormatError> {
    match profile {
        CreateRootAuthProfile::Ed25519 { signing_key, .. } => {
            Ok(ed25519_raw::authenticator_value_for_request(signing_key, request).to_vec())
        }
        CreateRootAuthProfile::X509(signer) => signer
            .authenticator_value_for_request(request)
            .map_err(|_| FormatError::WriterUnsupported("X.509 RootAuth signing failed")),
    }
}

#[derive(Debug, Clone)]
struct ArchiveInputSelection {
    paths: Vec<String>,
    autodiscovered: bool,
}

struct OpenedArchiveSelection {
    paths: Vec<String>,
    opened: OpenedArchive,
}

struct RepairedArchiveOutput {
    path: String,
    volume_index: u32,
    repaired_block_count: usize,
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct VolumePathPattern {
    base: String,
    volume_index: u32,
}

fn resolve_archive_input_paths(
    primary: &str,
    additional: &[String],
    allow_autodiscovery: bool,
) -> Result<ArchiveInputSelection> {
    let mut paths = Vec::with_capacity(additional.len() + 1);
    paths.push(primary.to_owned());
    paths.extend(additional.iter().cloned());
    if !allow_autodiscovery || !additional.is_empty() || primary == "-" {
        return Ok(ArchiveInputSelection {
            paths,
            autodiscovered: false,
        });
    }

    let Some(pattern) = parse_volume_path_pattern(Path::new(primary)) else {
        return Ok(ArchiveInputSelection {
            paths,
            autodiscovered: false,
        });
    };
    let discovered = discover_volume_siblings(Path::new(primary), &pattern)?;
    if discovered.is_empty() {
        return Ok(ArchiveInputSelection {
            paths,
            autodiscovered: false,
        });
    }
    Ok(ArchiveInputSelection {
        paths: discovered,
        autodiscovered: true,
    })
}

fn read_volume_inputs_from_paths(paths: &[String]) -> Result<Vec<Vec<u8>>> {
    paths
        .iter()
        .map(|path| fs::read(path).with_context(|| format!("failed to read archive {path}")))
        .collect()
}

fn open_volume_inputs_from_paths(paths: &[String]) -> Result<Vec<File>> {
    paths
        .iter()
        .map(|path| File::open(path).with_context(|| format!("failed to read archive {path}")))
        .collect()
}

fn write_repaired_archive_copies(
    paths: &[String],
    opened: &OpenedArchive,
) -> Result<Vec<RepairedArchiveOutput>> {
    let patches = opened
        .repair_patches()
        .context("failed to prepare repaired archive output")?;
    if patches.is_empty() {
        return Ok(Vec::new());
    }

    let mut path_by_volume = BTreeMap::<u32, String>::new();
    for path in paths {
        let volume_index = read_volume_index_from_path(path)?;
        if path_by_volume.insert(volume_index, path.clone()).is_some() {
            bail!("duplicate archive input for volume index {volume_index}");
        }
    }

    let mut patches_by_volume = BTreeMap::<u32, Vec<&ArchiveRepairPatch>>::new();
    for patch in &patches {
        patches_by_volume
            .entry(patch.volume_index)
            .or_default()
            .push(patch);
    }

    let mut outputs = Vec::new();
    for (volume_index, volume_patches) in patches_by_volume {
        let input_path = path_by_volume.get(&volume_index).ok_or_else(|| {
            anyhow!("repair output references unavailable volume index {volume_index}")
        })?;
        let output_path = repaired_archive_output_path(input_path)?;
        if output_path.exists() {
            return Err(io::Error::new(
                io::ErrorKind::AlreadyExists,
                format!("repaired output already exists: {}", output_path.display()),
            )
            .into());
        }
        fs::copy(input_path, &output_path).with_context(|| {
            format!(
                "failed to copy archive volume {} to {}",
                input_path,
                output_path.display()
            )
        })?;

        let mut output = OpenOptions::new()
            .write(true)
            .open(&output_path)
            .with_context(|| format!("failed to open repaired output {}", output_path.display()))?;
        for patch in &volume_patches {
            output
                .seek(SeekFrom::Start(patch.record_offset))
                .with_context(|| {
                    format!(
                        "failed to seek repaired output {} to offset {}",
                        output_path.display(),
                        patch.record_offset
                    )
                })?;
            output.write_all(&patch.record_bytes).with_context(|| {
                format!(
                    "failed to write repaired block {} to {}",
                    patch.block_index,
                    output_path.display()
                )
            })?;
        }
        output.flush().with_context(|| {
            format!("failed to flush repaired output {}", output_path.display())
        })?;
        outputs.push(RepairedArchiveOutput {
            path: output_path.to_string_lossy().into_owned(),
            volume_index,
            repaired_block_count: volume_patches.len(),
        });
    }

    Ok(outputs)
}

fn read_volume_index_from_path(path: &str) -> Result<u32> {
    let mut file = File::open(path).with_context(|| format!("failed to read archive {path}"))?;
    let mut header = [0u8; VOLUME_HEADER_LEN];
    file.read_exact(&mut header)
        .with_context(|| format!("failed to read archive header {path}"))?;
    Ok(VolumeHeader::parse(&header)
        .with_context(|| format!("failed to parse archive header {path}"))?
        .volume_index)
}

fn repaired_archive_output_path(input: &str) -> Result<PathBuf> {
    let path = Path::new(input);
    let file_name = path
        .file_name()
        .and_then(|file_name| file_name.to_str())
        .ok_or_else(|| anyhow!("archive path has no UTF-8 file name: {input}"))?;
    let repaired_name = if let Some(pattern) = parse_volume_file_name(file_name) {
        format!(
            "{}.repaired.vol{:03}.tzap",
            pattern.base, pattern.volume_index
        )
    } else if let Some(stem) = file_name.strip_suffix(".tzap") {
        format!("{stem}.repaired.tzap")
    } else {
        format!("{file_name}.repaired")
    };
    Ok(path.with_file_name(repaired_name))
}

fn parse_volume_path_pattern(path: &Path) -> Option<VolumePathPattern> {
    let file_name = path.file_name()?.to_str()?;
    parse_volume_file_name(file_name)
}

fn parse_volume_file_name(file_name: &str) -> Option<VolumePathPattern> {
    let stem = file_name.strip_suffix(".tzap")?;
    let (base, digits) = stem.rsplit_once(".vol")?;
    if base.is_empty() || digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
        return None;
    }
    Some(VolumePathPattern {
        base: base.to_owned(),
        volume_index: digits.parse().ok()?,
    })
}

fn discover_volume_siblings(primary: &Path, pattern: &VolumePathPattern) -> Result<Vec<String>> {
    let parent = primary.parent().unwrap_or_else(|| Path::new("."));
    let entries = match fs::read_dir(parent) {
        Ok(entries) => entries,
        Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
        Err(err) => {
            return Err(err).with_context(|| {
                format!("failed to inspect archive directory {}", parent.display())
            })
        }
    };
    let mut discovered = Vec::new();
    for entry in entries.filter_map(|entry| entry.ok()) {
        let file_name = entry.file_name();
        let Some(file_name) = file_name.to_str() else {
            continue;
        };
        let Some(candidate) = parse_volume_file_name(file_name) else {
            continue;
        };
        if candidate.base != pattern.base {
            continue;
        }
        discovered.push((candidate.volume_index, entry.path()));
    }
    discovered.sort_by(|left, right| left.0.cmp(&right.0).then_with(|| left.1.cmp(&right.1)));
    Ok(discovered
        .into_iter()
        .map(|(_, path)| path.to_string_lossy().into_owned())
        .collect())
}

fn reject_multi_volume_bootstrap(volume_count: usize, bootstrap: Option<&str>) -> Result<()> {
    if volume_count > 1 && bootstrap.is_some() {
        return Err(anyhow!(FormatError::ReaderUnsupported(
            "multi-volume inputs with --bootstrap are not supported; pass volume files without --bootstrap",
        )));
    }
    Ok(())
}

fn reject_stdout_extract_shape(stdout: bool, path_count: usize) -> Result<()> {
    if stdout && path_count != 1 {
        return Err(anyhow!(FormatError::ReaderUnsupported(
            "--stdout requires exactly one archive path",
        )));
    }
    Ok(())
}

struct ArchiveStdinOpenOptions<'a> {
    paths: &'a [String],
    stdout: bool,
    volumes: &'a [String],
    password_stdin: bool,
    password: bool,
    keyfile: Option<&'a str>,
    insecure_zero_key: bool,
}

fn reject_archive_stdin_open_options(options: ArchiveStdinOpenOptions<'_>) -> Result<()> {
    if !options.volumes.is_empty() {
        return Err(anyhow!(FormatError::ReaderUnsupported(
            "archive stdin must be the only archive input",
        )));
    }
    if options.stdout {
        return Err(anyhow!(FormatError::ReaderUnsupported(
            "--stdout is not supported for archive stdin extraction",
        )));
    }
    if !options.paths.is_empty() {
        return Err(anyhow!(FormatError::ReaderUnsupported(
            "selected-path extraction is not supported for archive stdin",
        )));
    }
    reject_archive_stdin_key_options(
        options.password_stdin,
        options.password,
        options.keyfile,
        options.insecure_zero_key,
    )
}

fn reject_archive_stdin_list_options(
    volumes: &[String],
    password_stdin: bool,
    password: bool,
    keyfile: Option<&str>,
    insecure_zero_key: bool,
) -> Result<()> {
    if !volumes.is_empty() {
        return Err(anyhow!(FormatError::ReaderUnsupported(
            "archive stdin must be the only archive input",
        )));
    }
    reject_archive_stdin_key_options(password_stdin, password, keyfile, insecure_zero_key)
}

fn reject_archive_stdin_key_options(
    password_stdin: bool,
    password: bool,
    _keyfile: Option<&str>,
    insecure_zero_key: bool,
) -> Result<()> {
    if insecure_zero_key {
        return Err(removed_insecure_zero_key_error().into());
    }
    if password_stdin || password {
        return Err(anyhow!(FormatError::ReaderUnsupported(
            "archive stdin currently supports raw --keyfile or no-key unencrypted archives only",
        )));
    }
    Ok(())
}

fn load_archive_stdin_key(
    keyfile: Option<&str>,
    password_stdin: bool,
    password: bool,
    insecure_zero_key: bool,
) -> Result<MasterKey> {
    reject_archive_stdin_key_options(password_stdin, password, keyfile, insecure_zero_key)?;
    if keyfile.is_some() {
        return load_raw_master_key(keyfile);
    }
    Err(anyhow!(FormatError::KeyMaterialMismatch).context(
        "encrypted archive stdin requires --keyfile; unencrypted archive stdin uses no key source",
    ))
}

fn read_optional_bootstrap_sidecar(path: Option<&str>) -> Result<Option<Vec<u8>>> {
    path.map(|path| {
        fs::read(path).with_context(|| format!("failed to read bootstrap sidecar {path}"))
    })
    .transpose()
}

fn open_inputs_maybe_bootstrap(
    volume_files: Vec<File>,
    master_key: &MasterKey,
    bootstrap: Option<&str>,
    options: ReaderOptions,
) -> Result<OpenedArchive> {
    if volume_files.len() > 1 {
        reject_multi_volume_bootstrap(volume_files.len(), bootstrap)?;
        return OpenedArchive::open_seekable_volumes_with_options(
            volume_files,
            master_key,
            options,
        )
        .map_err(Into::into);
    }
    let volume_file = volume_files
        .into_iter()
        .next()
        .ok_or_else(|| anyhow!("at least one archive volume is required"))?;
    if let Some(path) = bootstrap {
        let sidecar =
            fs::read(path).with_context(|| format!("failed to read bootstrap sidecar {path}"))?;
        open_seekable_archive_with_bootstrap_sidecar_options(
            volume_file,
            &sidecar,
            master_key,
            options,
        )
        .map_err(Into::into)
    } else {
        OpenedArchive::open_seekable_volumes_with_options(vec![volume_file], master_key, options)
            .map_err(Into::into)
    }
}

fn open_selection_maybe_bootstrap(
    selection: &ArchiveInputSelection,
    master_key: &MasterKey,
    bootstrap: Option<&str>,
    options: ReaderOptions,
) -> Result<OpenedArchive> {
    Ok(open_selection_maybe_bootstrap_resolved(selection, master_key, bootstrap, options)?.opened)
}

fn open_selection_maybe_bootstrap_resolved(
    selection: &ArchiveInputSelection,
    master_key: &MasterKey,
    bootstrap: Option<&str>,
    options: ReaderOptions,
) -> Result<OpenedArchiveSelection> {
    let volume_files = open_volume_inputs_from_paths(&selection.paths)?;
    match open_inputs_maybe_bootstrap(volume_files, master_key, bootstrap, options) {
        Ok(opened) => Ok(OpenedArchiveSelection {
            paths: selection.paths.clone(),
            opened,
        }),
        Err(err)
            if selection.autodiscovered && bootstrap.is_none() && selection.paths.len() > 1 =>
        {
            let usable_paths =
                filter_usable_autodiscovered_volume_paths(&selection.paths, master_key)
                    .with_context(|| "failed to filter autodiscovered archive volumes")?;
            if usable_paths == selection.paths {
                return Err(err);
            }
            let volume_files = open_volume_inputs_from_paths(&usable_paths)?;
            let opened = open_inputs_maybe_bootstrap(volume_files, master_key, bootstrap, options)?;
            Ok(OpenedArchiveSelection {
                paths: usable_paths,
                opened,
            })
        }
        Err(err) => Err(err),
    }
}

fn filter_usable_autodiscovered_volume_paths(
    paths: &[String],
    master_key: &MasterKey,
) -> Result<Vec<String>> {
    let mut usable = Vec::new();
    let mut first_error = None;
    for path in paths {
        let file = match File::open(path) {
            Ok(file) => file,
            Err(err) => {
                if first_error.is_none() {
                    first_error =
                        Some(anyhow!(err).context(format!("failed to read archive {path}")));
                }
                continue;
            }
        };
        match open_seekable_archive(file, master_key) {
            Ok(_) => usable.push(path.clone()),
            Err(err) if is_single_volume_candidate_usable_error(&err) => usable.push(path.clone()),
            Err(err) => {
                if first_error.is_none() {
                    first_error =
                        Some(anyhow!(err).context(format!("failed to open archive {path}")));
                }
            }
        }
    }
    if usable.is_empty() {
        return Err(
            first_error.unwrap_or_else(|| anyhow!("no autodiscovered archive volumes found"))
        );
    }
    Ok(usable)
}

fn is_single_volume_candidate_usable_error(err: &FormatError) -> bool {
    matches!(err, FormatError::FecTooFewAvailableShards)
        || matches!(
            err,
            FormatError::InvalidArchive(message)
                if *message == "missing volume count exceeds volume_loss_tolerance"
        )
}

fn validate_verify_key_holding_key_source(
    keyfile: Option<&str>,
    password_stdin: bool,
    password: bool,
    insecure_zero_key: bool,
) -> Result<()> {
    if insecure_zero_key {
        return Err(removed_insecure_zero_key_error().into());
    }
    let count =
        usize::from(keyfile.is_some()) + usize::from(password_stdin) + usize::from(password);
    if count > 1 {
        return Err(UsageError(
            "verify accepts at most one key source: --keyfile, --password, or --password-stdin",
        )
        .into());
    }
    Ok(())
}

struct PublicNoKeyVerifyRequest<'a> {
    archive_paths: &'a [String],
    trusted_public_key: Option<&'a str>,
    trusted_ca_cert: &'a [String],
    trusted_system_roots: bool,
    password_stdin: bool,
    password: bool,
    keyfile: Option<&'a str>,
    insecure_zero_key: bool,
    bootstrap: Option<&'a str>,
    reader_options: ReaderOptions,
    quiet: bool,
    json: bool,
}

fn run_public_no_key_verify(request: PublicNoKeyVerifyRequest<'_>) -> Result<()> {
    let trust = match load_public_no_key_trust(&request) {
        Ok(trust) => trust,
        Err(err) => {
            if request.json {
                emit_verify_json_error(request.archive_paths, None, None, &err)?;
            }
            return Err(err);
        }
    };
    let first = request
        .archive_paths
        .first()
        .ok_or(UsageError("at least one archive volume is required"))?;
    let selection = match resolve_archive_input_paths(first, &request.archive_paths[1..], true) {
        Ok(selection) => selection,
        Err(err) => {
            if request.json {
                emit_verify_json_error(request.archive_paths, None, None, &err)?;
            }
            return Err(err);
        }
    };
    let archive_paths = selection.paths;
    let volume_bytes = match read_volume_inputs_from_paths(&archive_paths) {
        Ok(volume_bytes) => volume_bytes,
        Err(err) => {
            if request.json {
                emit_verify_json_error(&archive_paths, None, None, &err)?;
            }
            return Err(err);
        }
    };
    let borrowed = volume_bytes.iter().map(Vec::as_slice).collect::<Vec<_>>();
    let mut x509_report = None;
    let mut x509_error = None;
    let verification = match public_no_key_verify_volumes_with_options(
        &borrowed,
        |footer, archive_root| match &trust {
            PublicNoKeyTrust::Ed25519 { public_key } => {
                if footer.authenticator_id != ED25519_AUTHENTICATOR_ID {
                    return Err(FormatError::ReaderUnsupported(
                        "trusted public key can only verify Ed25519 RootAuth",
                    ));
                }
                Ok(matches!(
                    ed25519_raw::verify_root_auth_footer(
                        footer,
                        archive_root,
                        Some(*public_key),
                        Ed25519VerificationMode::PublicNoKey,
                    ),
                    Ed25519RootAuthOutcome::PublicDataBlockCommitmentVerified { .. }
                ))
            }
            PublicNoKeyTrust::X509 {
                trusted_roots_der,
                trusted_system_roots,
            } => {
                if footer.authenticator_id != X509_AUTHENTICATOR_ID {
                    return Err(FormatError::ReaderUnsupported(
                        "X.509 trust can only verify X.509 RootAuth",
                    ));
                }
                match x509_chain::verify_root_auth_footer(
                    footer,
                    archive_root,
                    trusted_roots_der,
                    *trusted_system_roots,
                ) {
                    Ok(report) => {
                        x509_report = Some(report);
                        Ok(true)
                    }
                    Err(err) => {
                        x509_error = Some(err.to_string());
                        Ok(false)
                    }
                }
            }
        },
        request.reader_options,
    )
    .map_err(|err| {
        if let Some(detail) = x509_error.take() {
            anyhow!("{err}: {detail}")
        } else {
            anyhow!(err)
        }
    })
    .with_context(|| format!("failed to verify public RootAuth for {first}"))
    {
        Ok(verification) => verification,
        Err(err) => {
            if request.json {
                emit_verify_json_error(&archive_paths, None, None, &err)?;
            }
            return Err(err);
        }
    };
    let root_auth = match trust {
        PublicNoKeyTrust::Ed25519 { .. } => VerifiedPublicNoKeyRootAuth::Ed25519(verification),
        PublicNoKeyTrust::X509 { .. } => {
            let report = x509_report.ok_or(FormatError::InvalidArchive(
                "missing X.509 public no-key verification report",
            ))?;
            VerifiedPublicNoKeyRootAuth::X509 {
                verification,
                report,
            }
        }
    };
    if request.json {
        println!(
            "{}",
            serde_json::to_string(&json!({
                "ok": true,
                "archives": &archive_paths,
                "verification_mode": "public-no-key",
                "volume_count": archive_paths.len(),
                "root_auth": public_no_key_root_auth_json(&root_auth),
                "public_diagnostics": public_no_key_diagnostic_labels_for_root_auth(&root_auth),
            }))
            .context("failed to encode verify output as JSON")?
        );
        return Ok(());
    }
    emit_success_stdout(
        request.quiet,
        &format!(
            "{}: OK public no-key ({} volume(s), {} data block(s))",
            first,
            archive_paths.len(),
            public_no_key_total_data_block_count(&root_auth)
        ),
    )?;
    emit_public_no_key_root_auth_stdout(request.quiet, &root_auth)?;
    for diagnostic in public_no_key_diagnostic_labels_for_root_auth(&root_auth) {
        emit_success_stdout(request.quiet, &format!("public-no-key: {diagnostic}"))?;
    }
    Ok(())
}

fn load_public_no_key_trust(request: &PublicNoKeyVerifyRequest<'_>) -> Result<PublicNoKeyTrust> {
    let wants_ed25519 = request.trusted_public_key.is_some();
    let wants_x509 = !request.trusted_ca_cert.is_empty() || request.trusted_system_roots;
    if !wants_ed25519 && !wants_x509 {
        return Err(UsageError(
            "--public-no-key requires --trusted-public-key FILE, --trusted-ca-cert FILE, or --trusted-system-roots",
        )
        .into());
    }
    if wants_ed25519 && wants_x509 {
        return Err(UsageError(
            "use either --trusted-public-key or X.509 trust options with --public-no-key, not both",
        )
        .into());
    }
    if request.insecure_zero_key {
        return Err(removed_insecure_zero_key_error().into());
    }
    if request.password_stdin || request.password || request.keyfile.is_some() {
        return Err(UsageError(
            "--public-no-key cannot be combined with --keyfile, --password, or --password-stdin",
        )
        .into());
    }
    if request.bootstrap.is_some() {
        return Err(UsageError("--public-no-key does not use --bootstrap sidecars").into());
    }
    if let Some(path) = request.trusted_public_key {
        return Ok(PublicNoKeyTrust::Ed25519 {
            public_key: load_ed25519_public_key(path)?,
        });
    }
    Ok(PublicNoKeyTrust::X509 {
        trusted_roots_der: load_x509_certificate_files(request.trusted_ca_cert)?,
        trusted_system_roots: request.trusted_system_roots,
    })
}

fn verify_opened_root_auth_ed25519(
    opened: &OpenedArchive,
    content_verification: &ArchiveContentVerification<'_>,
    trusted_public_key: &str,
) -> Result<RootAuthVerification> {
    let public_key = load_ed25519_public_key(trusted_public_key)?;
    opened
        .verify_root_auth_with_verified_content(content_verification, |footer, archive_root| {
            Ok(matches!(
                ed25519_raw::verify_root_auth_footer(
                    footer,
                    archive_root,
                    Some(public_key),
                    Ed25519VerificationMode::KeyHoldingRootAuth,
                ),
                Ed25519RootAuthOutcome::RootAuthContentVerified { .. }
            ))
        })
        .map_err(Into::into)
}

fn verify_opened_root_auth(
    opened: &OpenedArchive,
    content_verification: &ArchiveContentVerification<'_>,
    trusted_public_key: Option<&str>,
    trusted_ca_cert: &[String],
    trusted_system_roots: bool,
) -> Result<Option<VerifiedRootAuth>> {
    let wants_ed25519 = trusted_public_key.is_some();
    let wants_x509 = !trusted_ca_cert.is_empty() || trusted_system_roots;
    if !wants_ed25519 && !wants_x509 {
        return Ok(None);
    }
    if wants_ed25519 && wants_x509 {
        return Err(
            UsageError("use either --trusted-public-key or X.509 trust options, not both").into(),
        );
    }
    let footer = opened
        .root_auth_footer
        .as_ref()
        .ok_or(FormatError::InvalidArchive("missing RootAuthFooter"))?;
    match footer.authenticator_id {
        ED25519_AUTHENTICATOR_ID if wants_ed25519 => {
            let public_key = trusted_public_key.expect("checked Ed25519 trust request");
            Ok(Some(VerifiedRootAuth::Ed25519(
                verify_opened_root_auth_ed25519(opened, content_verification, public_key)?,
            )))
        }
        X509_AUTHENTICATOR_ID if wants_x509 => {
            let trusted_roots_der = load_x509_certificate_files(trusted_ca_cert)?;
            Ok(Some(verify_opened_root_auth_x509(
                opened,
                content_verification,
                &trusted_roots_der,
                trusted_system_roots,
            )?))
        }
        ED25519_AUTHENTICATOR_ID => {
            Err(UsageError("Ed25519 RootAuth requires --trusted-public-key FILE").into())
        }
        X509_AUTHENTICATOR_ID => Err(UsageError(
            "X.509 RootAuth requires --trusted-ca-cert FILE or --trusted-system-roots",
        )
        .into()),
        _ => Err(FormatError::ReaderUnsupported("unsupported RootAuth authenticator id").into()),
    }
}

fn verify_opened_root_auth_x509(
    opened: &OpenedArchive,
    content_verification: &ArchiveContentVerification<'_>,
    trusted_roots_der: &[Vec<u8>],
    trusted_system_roots: bool,
) -> Result<VerifiedRootAuth> {
    let mut report = None;
    let mut x509_error = None;
    let verification = opened
        .verify_root_auth_with_verified_content(content_verification, |footer, archive_root| {
            match x509_chain::verify_root_auth_footer(
                footer,
                archive_root,
                trusted_roots_der,
                trusted_system_roots,
            ) {
                Ok(value) => {
                    report = Some(value);
                    Ok(true)
                }
                Err(err) => {
                    x509_error = Some(err.to_string());
                    Ok(false)
                }
            }
        })
        .map_err(|err| {
            if let Some(detail) = x509_error {
                anyhow!("{err}: {detail}")
            } else {
                anyhow!(err)
            }
        })?;
    let report = report.ok_or(FormatError::InvalidArchive(
        "missing X.509 RootAuth verification report",
    ))?;
    Ok(VerifiedRootAuth::X509 {
        verification,
        report,
    })
}

fn root_auth_json(root_auth: &VerifiedRootAuth) -> serde_json::Value {
    match root_auth {
        VerifiedRootAuth::Ed25519(root_auth) => {
            let mut payload = json!({
                "status": root_auth_status(root_auth),
                "diagnostics": root_auth_diagnostic_labels(root_auth),
                "authenticator": "ed25519",
                "archive_root": encode_hex(&root_auth.archive_root),
                "authenticator_id": root_auth.authenticator_id,
                "signer_identity_type": root_auth.signer_identity_type,
                "signer_identity": encode_hex(&root_auth.signer_identity_bytes),
                "total_data_block_count": root_auth.total_data_block_count,
            });
            if root_auth.signer_identity_type == 1 && root_auth.signer_identity_bytes.len() == 32 {
                payload["key_id"] = json!(encode_hex(&root_auth.signer_identity_bytes));
            }
            payload
        }
        VerifiedRootAuth::X509 {
            verification,
            report,
        } => json!({
            "status": root_auth_status(verification),
            "diagnostics": root_auth_diagnostic_labels(verification),
            "authenticator": "x509",
            "archive_root": encode_hex(&verification.archive_root),
            "authenticator_id": verification.authenticator_id,
            "signer_identity_type": verification.signer_identity_type,
            "signer_identity": encode_hex(&verification.signer_identity_bytes),
            "total_data_block_count": verification.total_data_block_count,
            "subject": &report.subject,
            "issuer": &report.issuer,
            "serial_number": &report.serial_number_hex,
            "certificate_sha256": encode_hex(&report.certificate_sha256),
            "signed_at_unix_seconds": report.signed_at_unix_seconds,
            "signed_at": format_unix_timestamp(report.signed_at_unix_seconds),
            "time_source": "signer_claimed",
            "verified_chain_subjects": &report.verified_chain_subjects,
            "trust_anchor_subject": &report.trust_anchor_subject,
        }),
    }
}

fn root_auth_status(root_auth: &RootAuthVerification) -> &'static str {
    root_auth
        .diagnostics
        .first()
        .map(|diagnostic| diagnostic.label())
        .unwrap_or("root_auth_content_verified")
}

fn root_auth_diagnostic_labels(root_auth: &RootAuthVerification) -> Vec<&'static str> {
    root_auth
        .diagnostics
        .iter()
        .map(|diagnostic| diagnostic.label())
        .collect()
}

fn emit_root_auth_stdout(quiet: bool, root_auth: &VerifiedRootAuth) -> io::Result<()> {
    match root_auth {
        VerifiedRootAuth::Ed25519(verification) => {
            emit_success_stdout(
                quiet,
                &format!(
                    "root-auth: OK ed25519 {}",
                    encode_hex(&verification.archive_root)
                ),
            )?;
            emit_root_auth_diagnostics_stdout(quiet, verification)
        }
        VerifiedRootAuth::X509 {
            verification,
            report,
        } => {
            emit_success_stdout(
                quiet,
                &format!(
                    "root-auth: OK x509 {}",
                    encode_hex(&verification.archive_root)
                ),
            )?;
            emit_success_stdout(quiet, &format!("root-auth signer: {}", report.subject))?;
            emit_success_stdout(quiet, &format!("root-auth issuer: {}", report.issuer))?;
            if let Some(trust_anchor) = &report.trust_anchor_subject {
                emit_success_stdout(quiet, &format!("root-auth trust-anchor: {trust_anchor}"))?;
            }
            emit_success_stdout(
                quiet,
                &format!(
                    "root-auth signed-at: {} (signer-claimed)",
                    format_unix_timestamp(report.signed_at_unix_seconds)
                ),
            )?;
            emit_success_stdout(
                quiet,
                &format!(
                    "root-auth certificate-sha256: {}",
                    encode_hex(&report.certificate_sha256)
                ),
            )?;
            emit_root_auth_diagnostics_stdout(quiet, verification)
        }
    }
}

fn emit_root_auth_diagnostics_stdout(
    quiet: bool,
    verification: &RootAuthVerification,
) -> io::Result<()> {
    for diagnostic in &verification.diagnostics {
        emit_success_stdout(quiet, &format!("root-auth: {}", diagnostic.label()))?;
    }
    Ok(())
}

fn public_no_key_root_auth_json(root_auth: &VerifiedPublicNoKeyRootAuth) -> serde_json::Value {
    match root_auth {
        VerifiedPublicNoKeyRootAuth::Ed25519(verification) => {
            let mut payload = json!({
                "status": public_no_key_status(verification),
                "diagnostics": public_no_key_diagnostic_labels(verification),
                "authenticator": "ed25519",
                "archive_root": encode_hex(&verification.archive_root),
                "authenticator_id": verification.authenticator_id,
                "signer_identity_type": verification.signer_identity_type,
                "signer_identity": encode_hex(&verification.signer_identity_bytes),
                "total_data_block_count": verification.total_data_block_count,
            });
            if verification.signer_identity_type == 1
                && verification.signer_identity_bytes.len() == 32
            {
                payload["key_id"] = json!(encode_hex(&verification.signer_identity_bytes));
            }
            payload
        }
        VerifiedPublicNoKeyRootAuth::X509 {
            verification,
            report,
        } => json!({
            "status": public_no_key_status(verification),
            "diagnostics": public_no_key_diagnostic_labels(verification),
            "authenticator": "x509",
            "archive_root": encode_hex(&verification.archive_root),
            "authenticator_id": verification.authenticator_id,
            "signer_identity_type": verification.signer_identity_type,
            "signer_identity": encode_hex(&verification.signer_identity_bytes),
            "total_data_block_count": verification.total_data_block_count,
            "subject": &report.subject,
            "issuer": &report.issuer,
            "serial_number": &report.serial_number_hex,
            "certificate_sha256": encode_hex(&report.certificate_sha256),
            "signed_at_unix_seconds": report.signed_at_unix_seconds,
            "signed_at": format_unix_timestamp(report.signed_at_unix_seconds),
            "time_source": "signer_claimed",
            "verified_chain_subjects": &report.verified_chain_subjects,
            "trust_anchor_subject": &report.trust_anchor_subject,
        }),
    }
}

fn public_no_key_status(verification: &PublicNoKeyVerification) -> &'static str {
    verification
        .diagnostics
        .first()
        .map(|diagnostic| diagnostic.label())
        .unwrap_or("public_data_block_commitment_verified")
}

fn public_no_key_diagnostic_labels(verification: &PublicNoKeyVerification) -> Vec<&'static str> {
    verification
        .diagnostics
        .iter()
        .map(|diagnostic| diagnostic.label())
        .collect()
}

fn public_no_key_diagnostic_labels_for_root_auth(
    root_auth: &VerifiedPublicNoKeyRootAuth,
) -> Vec<&'static str> {
    match root_auth {
        VerifiedPublicNoKeyRootAuth::Ed25519(verification) => {
            public_no_key_diagnostic_labels(verification)
        }
        VerifiedPublicNoKeyRootAuth::X509 { verification, .. } => {
            public_no_key_diagnostic_labels(verification)
        }
    }
}

fn emit_public_no_key_root_auth_stdout(
    quiet: bool,
    root_auth: &VerifiedPublicNoKeyRootAuth,
) -> io::Result<()> {
    match root_auth {
        VerifiedPublicNoKeyRootAuth::Ed25519(verification) => emit_success_stdout(
            quiet,
            &format!(
                "root-auth: OK public-no-key ed25519 {}",
                encode_hex(&verification.archive_root)
            ),
        ),
        VerifiedPublicNoKeyRootAuth::X509 {
            verification,
            report,
        } => {
            emit_success_stdout(
                quiet,
                &format!(
                    "root-auth: OK public-no-key x509 {}",
                    encode_hex(&verification.archive_root)
                ),
            )?;
            emit_success_stdout(quiet, &format!("root-auth signer: {}", report.subject))?;
            emit_success_stdout(quiet, &format!("root-auth issuer: {}", report.issuer))?;
            if let Some(trust_anchor) = &report.trust_anchor_subject {
                emit_success_stdout(quiet, &format!("root-auth trust-anchor: {trust_anchor}"))?;
            }
            emit_success_stdout(
                quiet,
                &format!(
                    "root-auth signed-at: {} (signer-claimed)",
                    format_unix_timestamp(report.signed_at_unix_seconds)
                ),
            )
        }
    }
}

fn public_no_key_total_data_block_count(root_auth: &VerifiedPublicNoKeyRootAuth) -> u64 {
    match root_auth {
        VerifiedPublicNoKeyRootAuth::Ed25519(verification) => verification.total_data_block_count,
        VerifiedPublicNoKeyRootAuth::X509 { verification, .. } => {
            verification.total_data_block_count
        }
    }
}

fn format_unix_timestamp(unix_seconds: i64) -> String {
    match OffsetDateTime::from_unix_timestamp(unix_seconds) {
        Ok(date_time) => date_time
            .format(&Rfc3339)
            .unwrap_or_else(|_| unix_seconds.to_string()),
        Err(_) => unix_seconds.to_string(),
    }
}

fn generate_random_key_material() -> Result<[u8; 32]> {
    let mut bytes = [0u8; 32];
    rand::thread_rng().fill_bytes(&mut bytes);
    Ok(bytes)
}

fn generate_ed25519_signing_key() -> SigningKey {
    SigningKey::generate(&mut rand::rngs::OsRng)
}

fn write_keyfile(path: &str, key_hex: &str, force: bool) -> Result<()> {
    write_atomic_output_file("keyfile", Path::new(path), key_hex.as_bytes(), force)
}

fn write_atomic_output_file(label: &str, path: &Path, bytes: &[u8], force: bool) -> Result<()> {
    if !force && path.exists() {
        bail!(
            "{label} already exists: {}; use --force to overwrite",
            path.display()
        );
    }
    let parent = path
        .parent()
        .filter(|path| !path.as_os_str().is_empty())
        .unwrap_or_else(|| Path::new("."));
    let mut temp = tempfile::Builder::new()
        .prefix(".tzap-write-")
        .suffix(".partial")
        .tempfile_in(parent)
        .with_context(|| format!("failed to create temporary {label} in {}", parent.display()))?;
    temp.as_file_mut()
        .write_all(bytes)
        .with_context(|| format!("failed to write temporary {label} {}", path.display()))?;
    temp.as_file_mut()
        .flush()
        .with_context(|| format!("failed to flush temporary {label} {}", path.display()))?;
    temp.as_file_mut()
        .sync_all()
        .with_context(|| format!("failed to sync temporary {label} {}", path.display()))?;

    let publish_result = if force {
        temp.persist(path)
    } else {
        temp.persist_noclobber(path)
    };
    match publish_result {
        Ok(_) => Ok(()),
        Err(error) if !force && error.error.kind() == io::ErrorKind::AlreadyExists => bail!(
            "{label} already exists: {}; use --force to overwrite",
            path.display()
        ),
        Err(error) => Err(error.error)
            .with_context(|| format!("failed to publish {label} {}", path.display())),
    }
}

fn load_ed25519_signing_key(path: &str) -> Result<SigningKey> {
    let seed = load_32_byte_key_file("Ed25519 signing key seed", path)?;
    Ok(SigningKey::from_bytes(&seed))
}

fn load_create_root_auth_profile(
    signing_key: Option<&str>,
    signing_cert: Option<&str>,
    signing_private_key: Option<&str>,
    signing_chain: &[String],
) -> Result<Option<CreateRootAuthProfile>> {
    match (signing_key, signing_cert, signing_private_key) {
        (Some(path), None, None) => {
            let signing_key = load_ed25519_signing_key(path)?;
            let signer_identity = signing_key.verifying_key().to_bytes();
            Ok(Some(CreateRootAuthProfile::Ed25519 {
                signing_key,
                signer_identity,
            }))
        }
        (None, Some(cert_path), Some(private_key_path)) => {
            let cert = fs::read(cert_path)
                .with_context(|| format!("failed to read signing certificate {cert_path}"))?;
            let private_key = fs::read(private_key_path).with_context(|| {
                format!("failed to read signing private key {private_key_path}")
            })?;
            let chain_der = load_x509_certificate_files(signing_chain)?;
            let signer = X509RootAuthSigner::from_pem_or_der(
                &cert,
                &private_key,
                chain_der,
                current_unix_seconds()?,
            )
            .with_context(|| format!("failed to load X.509 signing profile from {cert_path}"))?;
            Ok(Some(CreateRootAuthProfile::X509(signer)))
        }
        (None, None, None) => {
            if !signing_chain.is_empty() {
                return Err(UsageError("--signing-chain requires --signing-cert").into());
            }
            Ok(None)
        }
        _ => Err(UsageError(
            "create requires either --signing-key or --signing-cert with --signing-private-key",
        )
        .into()),
    }
}

fn load_ed25519_public_key(path: &str) -> Result<[u8; 32]> {
    load_32_byte_key_file("Ed25519 public key", path)
}

fn load_x509_certificate_files(paths: &[String]) -> Result<Vec<Vec<u8>>> {
    let mut certificates = Vec::new();
    for path in paths {
        let bytes = fs::read(path).with_context(|| format!("failed to read certificate {path}"))?;
        certificates.extend(
            x509_chain::certificates_der_from_pem_or_der(&bytes)
                .with_context(|| format!("failed to parse certificate {path}"))?,
        );
    }
    Ok(certificates)
}

fn current_unix_seconds() -> Result<i64> {
    let seconds = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .context("system clock is before the Unix epoch")?
        .as_secs();
    i64::try_from(seconds).context("current Unix timestamp exceeds i64")
}

fn load_32_byte_key_file(label: &'static str, path: &str) -> Result<[u8; 32]> {
    let bytes = fs::read(path).with_context(|| format!("failed to read {label} {path}"))?;
    if bytes.len() == 32 {
        let mut out = [0u8; 32];
        out.copy_from_slice(&bytes);
        return Ok(out);
    }

    let hex = std::str::from_utf8(&bytes)
        .with_context(|| format!("{label} must contain either 32 raw bytes or 64 hex characters"))?
        .trim();
    if hex.len() != 64 {
        bail!("{label} must contain either 32 raw bytes or 64 hex characters");
    }
    let mut out = [0u8; 32];
    for (idx, chunk) in hex.as_bytes().chunks_exact(2).enumerate() {
        out[idx] = decode_hex_byte(chunk)?;
    }
    Ok(out)
}

fn encode_hex(bytes: &[u8]) -> String {
    let mut output = String::with_capacity(bytes.len() * 2);
    for byte in bytes {
        let _ = std::fmt::Write::write_fmt(&mut output, format_args!("{:02x}", byte));
    }
    output
}

#[allow(clippy::too_many_arguments)]
fn load_create_key(
    keyfile: Option<&str>,
    password_stdin: bool,
    password: bool,
    no_encryption: bool,
    insecure_zero_key: bool,
    t_cost: u32,
    m_cost_kib: u32,
    parallelism: u32,
) -> Result<CreateKey> {
    if password_stdin {
        let passphrase = read_passphrase_stdin()?;
        validate_argon2_params(t_cost, m_cost_kib, parallelism)?;
        let mut salt = vec![0u8; DEFAULT_ARGON2_SALT_LEN];
        rand::thread_rng().fill_bytes(&mut salt);
        let kdf_params = KdfParams::Argon2id {
            t_cost,
            m_cost_kib,
            parallelism,
            salt,
        };
        let master_key = MasterKey::derive_from_passphrase(&kdf_params, &passphrase)?;
        return Ok(CreateKey {
            master_key,
            kdf_params,
        });
    }
    if password {
        let passphrase = read_passphrase_interactive_create()?;
        validate_argon2_params(t_cost, m_cost_kib, parallelism)?;
        let mut salt = vec![0u8; DEFAULT_ARGON2_SALT_LEN];
        rand::thread_rng().fill_bytes(&mut salt);
        let kdf_params = KdfParams::Argon2id {
            t_cost,
            m_cost_kib,
            parallelism,
            salt,
        };
        let master_key = MasterKey::derive_from_passphrase(&kdf_params, &passphrase)?;
        return Ok(CreateKey {
            master_key,
            kdf_params,
        });
    }
    if no_encryption {
        return Ok(CreateKey {
            master_key: insecure_zero_master_key()?,
            kdf_params: KdfParams::None,
        });
    }
    if insecure_zero_key {
        return Err(removed_insecure_zero_key_error().into());
    }
    Ok(CreateKey {
        master_key: load_raw_master_key(keyfile)?,
        kdf_params: KdfParams::Raw,
    })
}

fn load_open_key_from_paths(
    keyfile: Option<&str>,
    password_stdin: bool,
    password: bool,
    insecure_zero_key: bool,
    volume_paths: &[String],
) -> Result<MasterKey> {
    if password_stdin {
        let passphrase = read_passphrase_stdin()?;
        let kdf_params = read_kdf_params_from_any_volume_path(volume_paths)?;
        return derive_key_from_passphrase(&kdf_params, &passphrase);
    }
    if password {
        let passphrase = read_passphrase_interactive_open()?;
        let kdf_params = read_kdf_params_from_any_volume_path(volume_paths)?;
        return derive_key_from_passphrase(&kdf_params, &passphrase);
    }
    if insecure_zero_key {
        return Err(removed_insecure_zero_key_error().into());
    }
    if keyfile.is_some() {
        return load_raw_master_key(keyfile);
    }
    let protection = read_archive_protection_from_any_volume_path(volume_paths)?;
    if protection.aead_algo == AeadAlgo::None && protection.kdf_algo == KdfAlgo::None {
        return insecure_zero_master_key();
    }
    Err(anyhow!(FormatError::KeyMaterialMismatch)
        .context("encrypted archives require --keyfile, --password, or --password-stdin"))
}

fn insecure_zero_master_key() -> Result<MasterKey> {
    MasterKey::from_raw_key(&INSECURE_ZERO_KEY).map_err(Into::into)
}

fn derive_key_from_passphrase(kdf_params: &KdfParams, passphrase: &str) -> Result<MasterKey> {
    match kdf_params {
        KdfParams::Argon2id { .. } => {
            MasterKey::derive_from_passphrase(kdf_params, passphrase).map_err(Into::into)
        }
        KdfParams::Raw => Err(anyhow!(FormatError::KeyMaterialMismatch)
            .context("raw-key archives require --keyfile, not passphrase input")),
        KdfParams::None => Err(anyhow!(FormatError::KeyMaterialMismatch)
            .context("unencrypted archives do not use passphrase input")),
    }
}

fn validate_argon2_params(t_cost: u32, m_cost_kib: u32, parallelism: u32) -> Result<()> {
    if t_cost == 0 {
        return Err(anyhow!(FormatError::InvalidKdfParams(
            "argon2 t_cost must be at least 1",
        )));
    }
    if t_cost > READER_MAX_ARGON2ID_T_COST {
        return Err(anyhow!(FormatError::InvalidKdfParams(
            "argon2 t_cost exceeds reader maximum",
        )));
    }
    if parallelism == 0 {
        return Err(anyhow!(FormatError::InvalidKdfParams(
            "argon2 parallelism must be at least 1",
        )));
    }
    if parallelism > READER_MAX_ARGON2ID_PARALLELISM {
        return Err(anyhow!(FormatError::InvalidKdfParams(
            "argon2 parallelism exceeds reader maximum",
        )));
    }
    if m_cost_kib > READER_MAX_ARGON2ID_M_COST_KIB {
        return Err(anyhow!(FormatError::InvalidKdfParams(
            "argon2 memory cost exceeds reader maximum",
        )));
    }
    let min_memory = parallelism.checked_mul(8).ok_or_else(|| {
        anyhow!(FormatError::InvalidKdfParams(
            "argon2 memory per lane computation overflows",
        ))
    })?;
    if m_cost_kib < min_memory {
        return Err(anyhow!(FormatError::InvalidKdfParams(
            "argon2 memory must be at least 8 KiB per lane",
        )));
    }
    Ok(())
}

fn load_raw_master_key(keyfile: Option<&str>) -> Result<MasterKey> {
    let keyfile = keyfile.ok_or_else(|| {
        anyhow!(
            "no key source provided; use --password-stdin, --password, --keyfile PATH, or --no-encryption for create"
        )
    })?;
    let bytes = fs::read(keyfile).with_context(|| format!("failed to read keyfile {keyfile}"))?;
    if bytes.len() == 32 {
        return MasterKey::from_raw_key(&bytes).map_err(Into::into);
    }

    let hex = std::str::from_utf8(&bytes)
        .context("keyfile must contain either 32 raw bytes or 64 hex characters")?
        .trim();
    if hex.len() != 64 {
        bail!("keyfile must contain either 32 raw bytes or 64 hex characters");
    }
    let mut raw = [0u8; 32];
    for (idx, chunk) in hex.as_bytes().chunks_exact(2).enumerate() {
        raw[idx] = decode_hex_byte(chunk)?;
    }
    MasterKey::from_raw_key(&raw).map_err(Into::into)
}

fn read_passphrase_stdin() -> Result<String> {
    let mut passphrase = String::new();
    io::stdin()
        .read_to_string(&mut passphrase)
        .context("failed to read passphrase from stdin")?;
    if passphrase.ends_with('\n') {
        passphrase.pop();
        if passphrase.ends_with('\r') {
            passphrase.pop();
        }
    }
    if passphrase.is_empty() {
        bail!("passphrase must not be empty");
    }
    Ok(passphrase)
}

fn read_passphrase_interactive_create() -> Result<String> {
    loop {
        let first = read_passphrase_interactive("Passphrase: ")?;
        let second = read_passphrase_interactive("Confirm passphrase: ")?;
        if first == second {
            return Ok(first);
        }
        eprintln!("Passphrases do not match; try again.");
    }
}

fn read_passphrase_interactive_open() -> Result<String> {
    read_passphrase_interactive("Passphrase: ")
}

fn read_passphrase_interactive(prompt: &str) -> Result<String> {
    if !io::stdin().is_terminal() {
        eprint!("{prompt}");
        io::stderr().flush()?;
        return read_non_empty_passphrase(read_passphrase_stdin_fallback()?);
    }

    let passphrase = match read_passphrase_hidden(prompt) {
        Ok(passphrase) => passphrase,
        Err(err) => {
            let _ = err;
            eprint!("{prompt}");
            io::stderr().flush()?;
            read_passphrase_stdin_fallback()?
        }
    };
    read_non_empty_passphrase(passphrase)
}

fn read_non_empty_passphrase(passphrase: String) -> Result<String> {
    if passphrase.is_empty() {
        bail!("passphrase must not be empty");
    }
    Ok(passphrase)
}

fn read_passphrase_hidden(prompt: &str) -> Result<String> {
    Ok(rpassword::prompt_password(prompt)?)
}

fn read_passphrase_stdin_fallback() -> Result<String> {
    let mut passphrase = String::new();
    io::stdin()
        .read_line(&mut passphrase)
        .context("failed to read passphrase from stdin")?;
    if passphrase.ends_with('\n') {
        passphrase.pop();
        if passphrase.ends_with('\r') {
            passphrase.pop();
        }
    }
    Ok(passphrase)
}

#[cfg(test)]
fn read_kdf_params_from_volume(bytes: &[u8]) -> Result<KdfParams> {
    let header_bytes = bytes.get(..VOLUME_HEADER_LEN).ok_or_else(|| {
        anyhow!(FormatError::InvalidArchive(
            "volume is too short for VolumeHeader"
        ))
    })?;
    let volume_header = VolumeHeader::parse(header_bytes)?;
    let offset = volume_header.crypto_header_offset as usize;
    let length = volume_header.crypto_header_length as usize;
    let end = offset
        .checked_add(length)
        .ok_or_else(|| anyhow!(FormatError::InvalidArchive("CryptoHeader range overflow")))?;
    let crypto_header_bytes = bytes.get(offset..end).ok_or_else(|| {
        anyhow!(FormatError::InvalidArchive(
            "volume is too short for CryptoHeader"
        ))
    })?;
    Ok(read_archive_protection_from_headers(header_bytes, crypto_header_bytes)?.kdf_params)
}

fn read_kdf_params_from_volume_path(path: &str) -> Result<KdfParams> {
    Ok(read_archive_protection_from_volume_path(path)?.kdf_params)
}

#[derive(Debug)]
struct ArchiveProtection {
    aead_algo: AeadAlgo,
    kdf_algo: KdfAlgo,
    kdf_params: KdfParams,
}

fn read_archive_protection_from_volume_path(path: &str) -> Result<ArchiveProtection> {
    let mut file = File::open(path).with_context(|| format!("failed to open archive {path}"))?;
    let mut header_bytes = vec![0u8; VOLUME_HEADER_LEN];
    file.read_exact(&mut header_bytes)
        .with_context(|| format!("failed to read VolumeHeader from {path}"))?;
    let volume_header = VolumeHeader::parse(&header_bytes)?;
    let offset = volume_header.crypto_header_offset as u64;
    let length = volume_header.crypto_header_length as usize;
    file.seek(SeekFrom::Start(offset))
        .with_context(|| format!("failed to seek to CryptoHeader in {path}"))?;
    let mut crypto_header_bytes = vec![0u8; length];
    file.read_exact(&mut crypto_header_bytes)
        .with_context(|| format!("failed to read CryptoHeader from {path}"))?;
    read_archive_protection_from_headers(&header_bytes, &crypto_header_bytes)
}

fn read_kdf_params_from_any_volume_path(paths: &[String]) -> Result<KdfParams> {
    let mut first_error = None;
    for path in paths {
        match read_kdf_params_from_volume_path(path) {
            Ok(params) => return Ok(params),
            Err(err) => {
                if first_error.is_none() {
                    first_error = Some(err);
                }
            }
        }
    }
    Err(first_error.unwrap_or_else(|| anyhow!("at least one archive volume is required")))
        .context("failed to read KDF parameters from any archive volume")
}

fn read_archive_protection_from_any_volume_path(paths: &[String]) -> Result<ArchiveProtection> {
    let mut first_error = None;
    for path in paths {
        match read_archive_protection_from_volume_path(path) {
            Ok(protection) => return Ok(protection),
            Err(err) => {
                if first_error.is_none() {
                    first_error = Some(err);
                }
            }
        }
    }
    Err(first_error.unwrap_or_else(|| anyhow!("at least one archive volume is required")))
        .context("failed to read protection mode from any archive volume")
}

fn read_archive_protection_from_headers(
    header_bytes: &[u8],
    crypto_header_bytes: &[u8],
) -> Result<ArchiveProtection> {
    let volume_header = VolumeHeader::parse(header_bytes)?;
    let fixed_bytes = crypto_header_bytes
        .get(..CRYPTO_HEADER_FIXED_LEN)
        .ok_or_else(|| {
            anyhow!(FormatError::InvalidLength {
                structure: "CryptoHeaderFixed",
                expected: CRYPTO_HEADER_FIXED_LEN,
                actual: crypto_header_bytes.len(),
            })
        })?;
    let fixed = CryptoHeaderFixed::parse(fixed_bytes, volume_header.crypto_header_length)?;
    if fixed.stripe_width != volume_header.stripe_width {
        return Err(anyhow!(FormatError::InvalidArchive(
            "VolumeHeader and CryptoHeader stripe_width differ"
        )));
    }
    let crypto_header =
        CryptoHeader::parse(crypto_header_bytes, volume_header.crypto_header_length)?;
    Ok(ArchiveProtection {
        aead_algo: fixed.aead_algo,
        kdf_algo: fixed.kdf_algo,
        kdf_params: crypto_header.kdf_params,
    })
}

fn parse_size_u32(value: &str, name: &'static str) -> Result<u32> {
    let size = parse_size(value).with_context(|| format!("invalid {name}: {value}"))?;
    u32::try_from(size).with_context(|| format!("{name} exceeds u32"))
}

fn parse_size(value: &str) -> Result<u64> {
    let trimmed = value.trim();
    if trimmed.is_empty() {
        bail!("size is empty");
    }
    let split_at = trimmed
        .find(|ch: char| !ch.is_ascii_digit())
        .unwrap_or(trimmed.len());
    let (digits, suffix) = trimmed.split_at(split_at);
    if digits.is_empty() {
        bail!("invalid size '{value}': missing size digits");
    }
    let number = digits
        .parse::<u64>()
        .with_context(|| format!("invalid size '{trimmed}': bad digit sequence"))?;
    let multiplier = match suffix.to_ascii_lowercase().as_str() {
        "" => 1,
        "k" | "kb" | "kib" => 1024,
        "m" | "mb" | "mib" => 1024 * 1024,
        "g" | "gb" | "gib" => 1024 * 1024 * 1024,
        _ => bail!(
            "invalid size '{trimmed}': unsupported suffix '{suffix}'; supported: K/KB/KiB, M/MB/MiB, G/GB/GiB"
        ),
    };
    number
        .checked_mul(multiplier)
        .ok_or_else(|| anyhow!("size overflow"))
}

fn decode_hex_byte(bytes: &[u8]) -> Result<u8> {
    Ok((decode_hex_nibble(bytes[0])? << 4) | decode_hex_nibble(bytes[1])?)
}

fn decode_hex_nibble(byte: u8) -> Result<u8> {
    match byte {
        b'0'..=b'9' => Ok(byte - b'0'),
        b'a'..=b'f' => Ok(byte - b'a' + 10),
        b'A'..=b'F' => Ok(byte - b'A' + 10),
        _ => bail!("keyfile contains non-hex characters"),
    }
}

fn classify_error(err: &anyhow::Error) -> Diagnostic {
    if err.downcast_ref::<UsageError>().is_some() {
        return Diagnostic {
            label: "invalid-arguments",
            exit_code: EXIT_USAGE,
            action: "check command arguments",
        };
    }
    for cause in err.chain() {
        if let Some(usage) = cause.downcast_ref::<UsageError>() {
            let _ = usage;
            return Diagnostic {
                label: "invalid-arguments",
                exit_code: EXIT_USAGE,
                action: "check command arguments",
            };
        }
        if let Some(write_error) = cause.downcast_ref::<ArchiveWriteError>() {
            return match write_error {
                ArchiveWriteError::Format(format) => classify_format_error(format),
                ArchiveWriteError::Io(io_error) => classify_io_error(io_error),
            };
        }
        if let Some(extract_error) = cause.downcast_ref::<ExtractError>() {
            return match extract_error {
                ExtractError::Format(format) => classify_format_error(format),
                ExtractError::Output(io_error) => classify_io_error(io_error),
            };
        }
        if let Some(format) = cause.downcast_ref::<FormatError>() {
            return classify_format_error(format);
        }
        if let Some(io_error) = cause.downcast_ref::<io::Error>() {
            return classify_io_error(io_error);
        }
    }
    Diagnostic {
        label: "error",
        exit_code: EXIT_GENERIC,
        action: "",
    }
}

fn classify_io_error(err: &io::Error) -> Diagnostic {
    match err.kind() {
        io::ErrorKind::PermissionDenied
        | io::ErrorKind::NotFound
        | io::ErrorKind::AlreadyExists => Diagnostic {
            label: "io-error",
            exit_code: EXIT_IO,
            action: "check file paths and permissions",
        },
        _ => Diagnostic {
            label: "io-error",
            exit_code: EXIT_IO,
            action: "check filesystem state",
        },
    }
}

fn classify_format_error(err: &FormatError) -> Diagnostic {
    match err {
        FormatError::UnsupportedFormatVersion(_)
        | FormatError::UnsupportedVolumeFormatRevision(_)
        | FormatError::UnknownCompressionAlgo(_)
        | FormatError::UnknownAeadAlgo(_)
        | FormatError::UnknownFecAlgo(_)
        | FormatError::UnknownKdfAlgo(_)
        | FormatError::UnsupportedCompression(_)
        | FormatError::UnsupportedFec(_)
        | FormatError::UnsupportedBootstrapSidecarVersion(_) => Diagnostic {
            label: "unsupported-revision",
            exit_code: EXIT_UNSUPPORTED_REVISION,
            action: "use the matching tzap version for this archive",
        },
        FormatError::BadMagic {
            structure: "VolumeHeader",
        }
        | FormatError::BadMagic {
            structure: "VolumeTrailer",
        }
        | FormatError::BadMagic {
            structure: "ManifestFooter",
        } => Diagnostic {
            label: "corrupt-header",
            exit_code: EXIT_CORRUPT_ARCHIVE,
            action: "verify the archive header/trailer bytes and source file path",
        },
        FormatError::HmacMismatch {
            structure: "CryptoHeader",
        }
        | FormatError::KeyMaterialMismatch
        | FormatError::InvalidRawMasterKeyLength => Diagnostic {
            label: "wrong-key",
            exit_code: EXIT_WRONG_KEY,
            action: "confirm the archive key source (passphrase/raw key)",
        },
        FormatError::IntegrityDigestMismatch { .. } => Diagnostic {
            label: "corrupt-archive",
            exit_code: EXIT_CORRUPT_ARCHIVE,
            action: "verify the archive bytes and source file path",
        },
        FormatError::FecTooFewAvailableShards => Diagnostic {
            label: "missing-volume",
            exit_code: EXIT_CORRUPT_ARCHIVE,
            action: "add the missing archive volume(s) or confirm volume-loss tolerance",
        },
        FormatError::InvalidArchive(message)
            if *message == "complete volume set has missing global blocks" =>
        {
            Diagnostic {
                label: "missing-volume",
                exit_code: EXIT_CORRUPT_ARCHIVE,
                action: "add the missing archive volume(s) or confirm volume-loss tolerance",
            }
        }
        FormatError::InvalidArchive(message)
            if *message == "missing volume count exceeds volume_loss_tolerance" =>
        {
            Diagnostic {
                label: "missing-volume",
                exit_code: EXIT_CORRUPT_ARCHIVE,
                action: "add the missing archive volume(s) or confirm volume-loss tolerance",
            }
        }
        FormatError::HmacMismatch { .. } | FormatError::AeadFailure => Diagnostic {
            label: "corrupt-payload",
            exit_code: EXIT_CORRUPT_ARCHIVE,
            action: "verify archive payload integrity",
        },
        FormatError::BadCrc {
            structure: "VolumeHeader",
        }
        | FormatError::BadCrc {
            structure: "VolumeTrailer",
        }
        | FormatError::BadCrc {
            structure: "ManifestFooter",
        }
        | FormatError::InvalidMetadata {
            structure: "ManifestFooter",
            ..
        }
        | FormatError::InvalidMetadata {
            structure: "VolumeHeader",
            ..
        } => Diagnostic {
            label: "corrupt-header",
            exit_code: EXIT_CORRUPT_ARCHIVE,
            action: "inspect archive metadata and source file path",
        },
        FormatError::BadCrc { structure: _ } => Diagnostic {
            label: "corrupt-payload",
            exit_code: EXIT_CORRUPT_ARCHIVE,
            action: "verify payload integrity",
        },
        FormatError::InvalidKdfParams(message) => Diagnostic {
            label: "invalid-arguments",
            exit_code: EXIT_USAGE,
            action: message,
        },
        FormatError::InvalidMetadata { structure, .. } => Diagnostic {
            label: if *structure == "IndexRoot"
                || *structure == "FrameEntry"
                || *structure == "EnvelopeEntry"
            {
                "corrupt-payload"
            } else {
                "corrupt-header"
            },
            exit_code: EXIT_CORRUPT_ARCHIVE,
            action: if *structure == "IndexRoot"
                || *structure == "FrameEntry"
                || *structure == "EnvelopeEntry"
            {
                "inspect archive metadata tables and payload"
            } else {
                "inspect archive header metadata"
            },
        },
        FormatError::ReaderResourceLimitExceeded { .. } => Diagnostic {
            label: "invalid-arguments",
            exit_code: EXIT_USAGE,
            action:
                "check argon2 flags (--argon2-t-cost, --argon2-m-cost-kib, --argon2-parallelism)",
        },
        FormatError::UnsafeArchivePath => Diagnostic {
            label: "unsafe-path",
            exit_code: EXIT_UNSAFE_PATH,
            action: "archive contains unsafe paths; extract paths should be reviewed first",
        },
        FormatError::UnsafeOverwrite => Diagnostic {
            label: "unsafe-path",
            exit_code: EXIT_UNSAFE_PATH,
            action: "add --overwrite if overwriting existing files is intended",
        },
        FormatError::ReaderUnsupported(message) | FormatError::WriterUnsupported(message)
            if message.contains("bootstrap sidecar")
                || message.contains("dictionary bootstrap required") =>
        {
            Diagnostic {
                label: "missing-bootstrap",
                exit_code: EXIT_MISSING_BOOTSTRAP,
                action: "use --bootstrap with a matching sidecar",
            }
        }
        FormatError::ReaderUnsupported(_) | FormatError::WriterUnsupported(_) => Diagnostic {
            label: "unsupported-feature",
            exit_code: EXIT_UNSUPPORTED_FEATURE,
            action: "use a supported archive shape or upgrade tzap",
        },
        _ => Diagnostic {
            label: "corrupt-archive",
            exit_code: EXIT_CORRUPT_ARCHIVE,
            action: "verify archive integrity and source",
        },
    }
}

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

    use std::io::Cursor;

    use tzap_core::format::MASTER_KEY_LEN;

    fn test_master_key() -> MasterKey {
        MasterKey::from_raw_key(&[0x42; MASTER_KEY_LEN]).unwrap()
    }

    fn test_tar_stream(entries: &[(&str, &[u8])]) -> Vec<u8> {
        let mut out = Vec::new();
        for (path, data) in entries {
            out.extend_from_slice(&test_tar_header(path.as_bytes(), b'0', data.len() as u64));
            out.extend_from_slice(data);
            out.resize(out.len() + test_tar_padding(data.len()), 0);
        }
        out.extend_from_slice(&[0u8; 1024]);
        out
    }

    fn test_tar_header(path: &[u8], kind: u8, size: u64) -> [u8; 512] {
        let mut header = [0u8; 512];
        header[..path.len()].copy_from_slice(path);
        test_tar_octal(&mut header[100..108], 0o644);
        test_tar_octal(&mut header[108..116], 0);
        test_tar_octal(&mut header[116..124], 0);
        test_tar_octal(&mut header[124..136], size);
        test_tar_octal(&mut header[136..148], 0);
        header[148..156].fill(b' ');
        header[156] = kind;
        header[257..263].copy_from_slice(b"ustar\0");
        header[263..265].copy_from_slice(b"00");
        let checksum = header.iter().map(|byte| *byte as u64).sum::<u64>();
        test_tar_checksum(&mut header[148..156], checksum);
        header
    }

    fn test_tar_octal(field: &mut [u8], value: u64) {
        let digits = format!("{value:o}");
        field.fill(0);
        let start = field.len() - 1 - digits.len();
        field[..start].fill(b'0');
        field[start..start + digits.len()].copy_from_slice(digits.as_bytes());
    }

    fn test_tar_checksum(field: &mut [u8], value: u64) {
        let digits = format!("{value:06o}");
        field[0..6].copy_from_slice(digits.as_bytes());
        field[6] = 0;
        field[7] = b' ';
    }

    fn test_tar_padding(len: usize) -> usize {
        let remainder = len % 512;
        if remainder == 0 {
            0
        } else {
            512 - remainder
        }
    }

    #[test]
    fn create_layout_defaults_scale_by_input_size() {
        assert_eq!(
            default_create_layout(Some(LARGE_CREATE_LAYOUT_THRESHOLD)),
            CreateLayout {
                block_size: 64 * 1024,
                chunk_size: 256 * 1024,
                envelope_target_size: 1024 * 1024,
            }
        );
        assert_eq!(
            default_create_layout(Some(LARGE_CREATE_LAYOUT_THRESHOLD + 1)),
            CreateLayout {
                block_size: 1024 * 1024,
                chunk_size: 32 * 1024 * 1024,
                envelope_target_size: 64 * 1024 * 1024,
            }
        );
        assert_eq!(
            default_create_layout(None),
            default_create_layout(Some(LARGE_CREATE_LAYOUT_THRESHOLD + 1))
        );
    }

    #[test]
    fn create_layout_chunk_override_grows_implicit_envelope() {
        let layout = resolve_create_layout(
            CreateLayoutOverrides {
                chunk_size: Some("4M"),
                envelope_size: None,
                block_size: None,
            },
            Some(1024),
        )
        .unwrap();

        assert_eq!(layout.chunk_size, 4 * 1024 * 1024);
        assert_eq!(layout.envelope_target_size, 4 * 1024 * 1024);
        assert_eq!(layout.block_size, 64 * 1024);
    }

    #[test]
    fn tar_stdin_signer_failure_removes_temporary_archive_output() {
        let temp = tempfile::tempdir().unwrap();
        let output = temp.path().join("failed.tzap");
        let key = CreateKey {
            master_key: test_master_key(),
            kdf_params: KdfParams::Raw,
        };
        let root_auth = RootAuthWriterConfig {
            authenticator_id: 0x9001,
            signer_identity_type: 0x9002,
            signer_identity: b"test signer",
            authenticator_value_length: 64,
        };
        let mut authenticator = |_request: &RootAuthSigningRequest| {
            Err(FormatError::WriterUnsupported("test signer failed"))
        };
        let mut input = Cursor::new(test_tar_stream(&[("signed.txt", b"signed")]));

        let error = write_tar_stdin_archive_output_from_reader(
            output.to_str().unwrap(),
            &mut input,
            &key,
            WriterOptions {
                stripe_width: 1,
                volume_loss_tolerance: 0,
                bit_rot_buffer_pct: 0,
                ..WriterOptions::default()
            },
            Some(root_auth),
            Some(&mut authenticator),
            false,
        )
        .unwrap_err();

        assert!(error.to_string().contains("test signer failed"));
        assert!(!output.exists());
    }

    #[test]
    fn raw_spool_multi_volume_signer_failure_removes_temporary_archive_outputs_and_spool() {
        let temp = tempfile::tempdir().unwrap();
        let output = temp.path().join("failed-raw-spool.tzap");
        let volume_0 = create_output_paths(output.to_str().unwrap(), 3)[0].clone();
        let volume_1 = create_output_paths(output.to_str().unwrap(), 3)[1].clone();
        let volume_2 = create_output_paths(output.to_str().unwrap(), 3)[2].clone();
        let key = CreateKey {
            master_key: test_master_key(),
            kdf_params: KdfParams::Raw,
        };
        let root_auth = RootAuthWriterConfig {
            authenticator_id: 0x9001,
            signer_identity_type: 0x9002,
            signer_identity: b"test signer",
            authenticator_value_length: 64,
        };
        let mut authenticator = |_request: &RootAuthSigningRequest| {
            Err(FormatError::WriterUnsupported("test signer failed"))
        };
        let payload = (0..150_000)
            .map(|index| (index % 251) as u8)
            .collect::<Vec<_>>();
        let spool_path;

        {
            let spool = crate::plaintext_spool::spool_unknown_size_raw_stdin_in(
                Cursor::new(payload),
                temp.path(),
                u64::MAX,
                ExplicitPlaintextSpool::acknowledge_plaintext_spool(),
            )
            .unwrap();
            let known_size_source = spool.known_size_source();
            spool_path = spool.path().to_path_buf();
            let mut spool_reader = spool.reopen().unwrap();

            let error = write_raw_stdin_archive_output_from_reader(
                output.to_str().unwrap(),
                &mut spool_reader,
                "raw/spooled.bin",
                known_size_source.size(),
                &key,
                WriterOptions {
                    stripe_width: 3,
                    volume_loss_tolerance: 0,
                    bit_rot_buffer_pct: 0,
                    ..WriterOptions::default()
                },
                Some(root_auth),
                Some(&mut authenticator),
                false,
            )
            .unwrap_err();

            assert!(error.to_string().contains("test signer failed"));
            assert!(spool_path.exists());
        }

        assert!(!spool_path.exists());
        assert!(!output.exists());
        assert!(!volume_0.exists());
        assert!(!volume_1.exists());
        assert!(!volume_2.exists());
    }

    #[test]
    fn read_kdf_params_rejects_stripe_width_mismatch_before_returning_kdf() {
        let archive = write_archive_with_kdf(
            &[RegularFile::new("file.txt", b"contents")],
            &test_master_key(),
            WriterOptions {
                archive_uuid: Some([0x11; 16]),
                session_id: Some([0x22; 16]),
                bit_rot_buffer_pct: 0,
                ..WriterOptions::default()
            },
            &KdfParams::Argon2id {
                t_cost: 1,
                m_cost_kib: 8,
                parallelism: 1,
                salt: vec![0x33; 8],
            },
        )
        .unwrap();
        let mut bytes = archive.bytes;
        let mut volume_header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
        volume_header.stripe_width += 1;
        bytes[..VOLUME_HEADER_LEN].copy_from_slice(&volume_header.to_bytes());

        let err = read_kdf_params_from_volume(&bytes).unwrap_err();

        assert_eq!(
            err.downcast_ref::<FormatError>(),
            Some(&FormatError::InvalidArchive(
                "VolumeHeader and CryptoHeader stripe_width differ"
            ))
        );
    }

    #[test]
    fn bootstrap_required_errors_keep_missing_bootstrap_diagnostic() {
        for err in [
            FormatError::ReaderUnsupported("dictionary bootstrap required"),
            FormatError::ReaderUnsupported(
                "dictionary bootstrap required for non-seekable sequential extraction",
            ),
            FormatError::ReaderUnsupported(
                "non-seekable random access requires a bootstrap sidecar",
            ),
            FormatError::WriterUnsupported("bootstrap sidecar required"),
        ] {
            let diagnostic = classify_format_error(&err);

            assert_eq!(diagnostic.label, "missing-bootstrap");
            assert_eq!(diagnostic.exit_code, EXIT_MISSING_BOOTSTRAP);
            assert_eq!(diagnostic.action, "use --bootstrap with a matching sidecar");
        }
    }

    #[test]
    fn missing_volume_errors_keep_stable_diagnostic() {
        let diagnostic = classify_format_error(&FormatError::InvalidArchive(
            "missing volume count exceeds volume_loss_tolerance",
        ));

        assert_eq!(diagnostic.label, "missing-volume");
        assert_eq!(diagnostic.exit_code, EXIT_CORRUPT_ARCHIVE);
        assert_eq!(
            diagnostic.action,
            "add the missing archive volume(s) or confirm volume-loss tolerance"
        );
    }

    #[test]
    fn metadata_diagnostic_lines_use_stable_cli_warning_prefix() {
        let line = metadata_diagnostic_line(
            "path/in/archive",
            &MetadataDiagnostic {
                profile: "gnu-sparse",
                message: "unsupported sparse-file PAX metadata was ignored",
            },
        );

        assert_eq!(
            line,
            "tzap: degraded-metadata: path/in/archive: gnu-sparse: unsupported sparse-file PAX metadata was ignored"
        );
    }

    #[test]
    fn selected_metadata_diagnostic_lines_filter_to_requested_paths() {
        let entries = vec![
            ArchiveEntry {
                path: "selected.txt".to_string(),
                file_data_size: 1,
                kind: TarEntryKind::Regular,
                mode: 0o644,
                mtime: 0,
                diagnostics: vec![MetadataDiagnostic {
                    profile: "pax-posix-2001",
                    message: "unsupported PAX key was ignored",
                }],
            },
            ArchiveEntry {
                path: "other.txt".to_string(),
                file_data_size: 1,
                kind: TarEntryKind::Regular,
                mode: 0o644,
                mtime: 0,
                diagnostics: vec![MetadataDiagnostic {
                    profile: "gnu-sparse",
                    message: "unsupported sparse-file PAX metadata was ignored",
                }],
            },
        ];

        assert_eq!(
            metadata_diagnostic_lines_for_paths(&entries, &["selected.txt".to_string()]),
            vec![
                "tzap: degraded-metadata: selected.txt: pax-posix-2001: unsupported PAX key was ignored"
                    .to_string()
            ]
        );
        assert_eq!(metadata_diagnostic_lines_for_entries(&entries).len(), 2);
    }
}
