//! CLI command structure and dispatch.

use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};

use bytesize::ByteSize;
use clap::{Parser, Subcommand, ValueEnum};
use comfy_table::{Table, presets::UTF8_FULL};

use crate::archive::{ArchiveEntry, Extractor};
use crate::error::Error;
use crate::format::{self, ArchiveFormat};
use crate::{cpt, gzinspect, lz4, sit};
use sha2::{Digest, Sha256};

/// Output format for structured results.
#[derive(Clone, Copy, Default, ValueEnum)]
enum OutputFormat {
    /// Human-readable text (default)
    #[default]
    Text,
    /// JSON
    Json,
    /// SARIF 2.1.0 (Static Analysis Results Interchange Format)
    Sarif,
    /// Markdown table
    Markdown,
}

/// archmeld — secure, memory-safe, type-safe archive multi-tool.
///
/// Unified CLI for extracting, inspecting, listing, and verifying archives
/// across ZIP, TAR (gz/bz2/xz/zst/lz4), 7-Zip, Gzip, LZ4, `StuffIt`,
/// and Compact Pro formats.
#[derive(Parser)]
#[command(name = "archmeld", version, about, long_about = None)]
#[command(propagate_version = true)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

/// Arguments for the extract subcommand (flattened to reduce enum variant size).
#[derive(clap::Args)]
struct ExtractArgs {
    /// Path to the archive file
    #[arg(value_name = "FILE")]
    input: PathBuf,

    /// Output directory (default: current directory)
    #[arg(short, long, default_value = ".")]
    output: PathBuf,

    /// Override auto-detected format
    #[arg(short, long)]
    format: Option<FormatArg>,

    /// Maximum file size limit in MiB
    #[arg(long, default_value = "100")]
    max_file_size: u64,

    /// Maximum total extraction size in MiB
    #[arg(long, default_value = "1024")]
    max_total_size: u64,

    /// Maximum number of entries (archive bomb protection)
    #[arg(long, default_value = "100000")]
    max_entries: usize,

    /// Maximum compression ratio (archive bomb protection)
    #[arg(long, default_value = "1000")]
    max_ratio: u64,
}

#[derive(Subcommand)]
enum Commands {
    /// Extract files from an archive
    Extract(ExtractArgs),

    /// List contents of an archive
    List {
        /// Path to the archive file
        #[arg(value_name = "FILE")]
        input: PathBuf,

        /// Override auto-detected format
        #[arg(short, long)]
        format: Option<FormatArg>,

        /// Output as JSON (shorthand for --output-format json)
        #[arg(long)]
        json: bool,

        /// Output format: text, json, sarif, markdown
        #[arg(long, short = 'O', value_enum, default_value = "text")]
        output_format: OutputFormat,
    },

    /// Detect and display format information
    Info {
        /// Path to the file to identify
        #[arg(value_name = "FILE")]
        input: PathBuf,

        /// Output as JSON (shorthand for --output-format json)
        #[arg(long)]
        json: bool,

        /// Output format: text, json, sarif, markdown
        #[arg(long, short = 'O', value_enum, default_value = "text")]
        output_format: OutputFormat,
    },

    /// Inspect gzip file headers and metadata
    #[command(name = "gz-inspect")]
    GzInspect {
        /// Path to the gzip file
        #[arg(value_name = "FILE")]
        input: PathBuf,

        /// Verify CRC-32 integrity
        #[arg(long)]
        verify: bool,

        /// Output as JSON
        #[arg(long)]
        json: bool,
    },

    /// LZ4 compression and decompression
    Lz4 {
        #[command(subcommand)]
        action: Lz4Action,
    },

    /// Inspect `StuffIt` (.sit) archive
    #[command(name = "sit-inspect")]
    SitInspect {
        /// Path to the `StuffIt` archive
        #[arg(value_name = "FILE")]
        input: PathBuf,

        /// Output as JSON
        #[arg(long)]
        json: bool,
    },

    /// Inspect Compact Pro (.cpt) archive
    #[command(name = "cpt-inspect")]
    CptInspect {
        /// Path to the Compact Pro archive
        #[arg(value_name = "FILE")]
        input: PathBuf,

        /// Verify header CRC-32
        #[arg(long)]
        verify: bool,

        /// Output as JSON
        #[arg(long)]
        json: bool,
    },

    /// Verify archive integrity
    Verify {
        /// Path to the archive file
        #[arg(value_name = "FILE")]
        input: PathBuf,

        /// Output format: text, json, sarif, markdown
        #[arg(long, short = 'O', value_enum, default_value = "text")]
        output_format: OutputFormat,
    },

    /// Compress a file with high-ratio `Zopfli` gzip
    #[command(name = "gz-compress")]
    GzCompress {
        /// Input file to compress
        #[arg(value_name = "FILE")]
        input: PathBuf,

        /// Output file (default: append .gz extension)
        #[arg(short, long)]
        output: Option<PathBuf>,
    },

    /// Validate whether decompression is possible (dry run, no output written)
    Validate {
        /// Path to the archive or compressed file
        #[arg(value_name = "FILE")]
        input: PathBuf,

        /// Override auto-detected format
        #[arg(short, long)]
        format: Option<FormatArg>,

        /// Output as JSON (shorthand for --output-format json)
        #[arg(long)]
        json: bool,

        /// Output format: text, json, sarif, markdown
        #[arg(long, short = 'O', value_enum, default_value = "text")]
        output_format: OutputFormat,
    },
}

#[derive(Subcommand)]
enum Lz4Action {
    /// Decompress an LZ4 file
    Decompress {
        /// Input LZ4 file
        #[arg(value_name = "FILE")]
        input: PathBuf,

        /// Output file (default: strip .lz4 extension)
        #[arg(short, long)]
        output: Option<PathBuf>,
    },
    /// Compress a file with LZ4
    Compress {
        /// Input file
        #[arg(value_name = "FILE")]
        input: PathBuf,

        /// Output file (default: append .lz4 extension)
        #[arg(short, long)]
        output: Option<PathBuf>,
    },
    /// Inspect LZ4 frame header
    Inspect {
        /// Input LZ4 file
        #[arg(value_name = "FILE")]
        input: PathBuf,

        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
}

#[derive(Clone, ValueEnum)]
enum FormatArg {
    Zip,
    Tar,
    TarGz,
    TarBz2,
    TarXz,
    TarZst,
    TarLz4,
    #[value(name = "7z")]
    SevenZip,
    Gz,
    Bz2,
    Xz,
    Lz4,
    Zstd,
    Ar,
    Deb,
    Cab,
    Rar,
    Brotli,
    Snappy,
    Lzma,
    Bz3,
}

impl From<FormatArg> for ArchiveFormat {
    // rust-doctor: acknowledged — exhaustive enum match over 21 format variants
    fn from(arg: FormatArg) -> Self {
        match arg {
            FormatArg::Zip => Self::Zip,
            FormatArg::Tar => Self::Tar,
            FormatArg::TarGz => Self::TarGz,
            FormatArg::TarBz2 => Self::TarBz2,
            FormatArg::TarXz => Self::TarXz,
            FormatArg::TarZst => Self::TarZst,
            FormatArg::TarLz4 => Self::TarLz4,
            FormatArg::SevenZip => Self::SevenZip,
            FormatArg::Gz => Self::Gz,
            FormatArg::Bz2 => Self::Bz2,
            FormatArg::Xz => Self::Xz,
            FormatArg::Lz4 => Self::Lz4,
            FormatArg::Zstd => Self::Zstd,
            FormatArg::Ar => Self::Ar,
            FormatArg::Deb => Self::Deb,
            FormatArg::Cab => Self::Cab,
            FormatArg::Rar => Self::Rar,
            FormatArg::Brotli => Self::Brotli,
            FormatArg::Snappy => Self::Snappy,
            FormatArg::Lzma => Self::Lzma,
            FormatArg::Bz3 => Self::Bz3,
        }
    }
}

/// Run the CLI.
///
/// # Errors
///
/// Returns error on any command failure.
pub fn run() -> anyhow::Result<()> {
    let cli = Cli::parse();

    match cli.command {
        Commands::Extract(args) => {
            let extractor = Extractor::new()
                .with_max_file_size(args.max_file_size * 1024 * 1024)
                .with_max_total_size(args.max_total_size * 1024 * 1024)
                .with_max_entries(args.max_entries)
                .with_max_compression_ratio(f64::from(
                    u32::try_from(args.max_ratio).unwrap_or(1000),
                ));
            cmd_extract(
                &args.input,
                &args.output,
                args.format,
                &extractor,
                args.max_file_size,
                args.max_total_size,
                args.max_entries,
            )
        },
        Commands::List {
            input,
            format,
            json,
            output_format,
        } => cmd_list(&input, format, resolve_output_format(json, output_format)),
        Commands::Info {
            input,
            json,
            output_format,
        } => cmd_info(&input, resolve_output_format(json, output_format)),
        Commands::GzInspect {
            input,
            verify,
            json,
        } => cmd_gz_inspect(&input, verify, json),
        Commands::Lz4 { action } => cmd_lz4(action),
        Commands::SitInspect { input, json } => cmd_sit_inspect(&input, json),
        Commands::CptInspect {
            input,
            verify,
            json,
        } => cmd_cpt_inspect(&input, verify, json),
        Commands::Verify {
            input,
            output_format,
        } => cmd_verify(&input, output_format),
        Commands::GzCompress { input, output } => cmd_gz_compress(&input, output.as_deref()),
        Commands::Validate {
            input,
            format,
            json,
            output_format,
        } => cmd_validate(&input, format, resolve_output_format(json, output_format)),
    }
}

/// Resolve effective output format (`--json` overrides `--output-format`).
const fn resolve_output_format(json: bool, fmt: OutputFormat) -> OutputFormat {
    if json { OutputFormat::Json } else { fmt }
}

/// Build a SARIF 2.1.0 log envelope around a list of result objects.
fn make_sarif_log(results: &[serde_json::Value]) -> serde_json::Value {
    serde_json::json!({
        "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
        "version": "2.1.0",
        "runs": [{
            "tool": {
                "driver": {
                    "name": "archmeld",
                    "version": env!("CARGO_PKG_VERSION"),
                    "informationUri": "https://github.com/ndaal/archmeld"
                }
            },
            "results": results
        }]
    })
}

/// Print a Markdown table to stdout.
fn print_markdown_table(headers: &[&str], rows: &[Vec<String>]) {
    println!("| {} |", headers.join(" | "));
    println!(
        "| {} |",
        headers
            .iter()
            .map(|_| "---")
            .collect::<Vec<_>>()
            .join(" | ")
    );
    for row in rows {
        println!("| {} |", row.join(" | "));
    }
}

/// Read a file from disk into a byte vector.
///
/// # Errors
///
/// Returns an error if the file does not exist or cannot be read.
fn read_file(path: &Path) -> anyhow::Result<Vec<u8>> {
    if !path.exists() {
        return Err(Error::FileNotFound(path.to_path_buf()).into());
    }
    Ok(fs::read(path)?)
}

fn resolve_format(path: &Path, data: &[u8], format_arg: Option<FormatArg>) -> ArchiveFormat {
    format_arg.map_or_else(
        || format::detect_format_from_path(path, data),
        std::convert::Into::into,
    )
}

/// Extract files from an archive to the given output directory.
///
/// # Errors
///
/// Returns an error on unknown format, extraction failure, or I/O error.
// rust-doctor: acknowledged — multi-format dispatch with safe_unzip and RAR special cases
#[allow(clippy::too_many_arguments)]
fn cmd_extract(
    input: &Path,
    output: &Path,
    format_arg: Option<FormatArg>,
    extractor: &Extractor,
    max_file_size_mib: u64,
    max_total_size_mib: u64,
    max_entries: usize,
) -> anyhow::Result<()> {
    let data = read_file(input)?;
    let fmt = resolve_format(input, &data, format_arg);

    if fmt == ArchiveFormat::Unknown {
        anyhow::bail!(
            "Could not determine archive format for: {}",
            input.display()
        );
    }

    eprintln!("Extracting {fmt} archive: {}", input.display());

    // Use safe_unzip for secure ZIP extraction to disk
    if fmt == ArchiveFormat::Zip {
        let limits = safe_unzip::Limits {
            max_total_bytes: max_total_size_mib * 1024 * 1024,
            max_file_count: max_entries,
            max_single_file: max_file_size_mib * 1024 * 1024,
            max_path_depth: 50,
        };
        let safe_ext = safe_unzip::Extractor::new_or_create(output)
            .map_err(|e| anyhow::anyhow!("safe_unzip init: {e}"))?
            .limits(limits)
            .symlinks(safe_unzip::SymlinkPolicy::Error)
            .mode(safe_unzip::ExtractionMode::ValidateFirst)
            .overwrite(safe_unzip::OverwritePolicy::Skip);

        let report = safe_ext
            .extract(std::io::Cursor::new(&data))
            .map_err(|e| anyhow::anyhow!("safe_unzip: {e}"))?;

        eprintln!(
            "Extracted {} files ({}) to {} [secure mode via safe_unzip]",
            report.files_extracted,
            ByteSize(report.bytes_written),
            output.display()
        );
        return Ok(());
    }

    let files = extractor.extract_path(input, fmt)?;
    drop(data); // Free memory before writing files

    fs::create_dir_all(output)?;

    let mut count = 0u32;
    let mut total_bytes: u64 = 0;

    for file in &files {
        let dest = output.join(&file.path);

        if file.is_directory {
            fs::create_dir_all(&dest)?;
            continue;
        }

        if let Some(parent) = dest.parent() {
            fs::create_dir_all(parent)?;
        }

        fs::write(&dest, &file.data)?;
        count += 1;
        total_bytes += file.size;
    }

    eprintln!(
        "Extracted {count} files ({}) to {}",
        ByteSize(total_bytes),
        output.display()
    );

    Ok(())
}

/// List contents of an archive.
///
/// # Errors
///
/// Returns an error on unknown format, parse failure, or I/O error.
// rust-doctor: acknowledged — 4 output format branches with format-specific rendering
#[allow(clippy::too_many_lines)]
fn cmd_list(
    input: &Path,
    format_arg: Option<FormatArg>,
    output_format: OutputFormat,
) -> anyhow::Result<()> {
    let data = read_file(input)?;
    let fmt = resolve_format(input, &data, format_arg);

    if fmt == ArchiveFormat::Unknown {
        anyhow::bail!(
            "Could not determine archive format for: {}",
            input.display()
        );
    }

    let extractor = Extractor::new();

    // For formats that support listing, use list; otherwise fall back to extract
    let entries: Vec<ArchiveEntry> = if let Ok(e) = extractor.list_path(input, fmt) {
        e
    } else if let Ok(e) = extractor.list(&data, fmt) {
        e
    } else {
        // Fall back: extract and convert to entries
        let files = extractor.extract(&data, fmt)?;
        files
            .into_iter()
            .map(|f| ArchiveEntry {
                path: f.path,
                compressed_size: f.size,
                uncompressed_size: f.size,
                is_directory: f.is_directory,
                compression_method: "unknown".into(),
            })
            .collect()
    };

    match output_format {
        OutputFormat::Json => {
            let out = serde_json::to_string_pretty(&entries)?;
            println!("{out}");
        },
        OutputFormat::Sarif => {
            let results: Vec<serde_json::Value> = entries
                .iter()
                .map(|entry| {
                    serde_json::json!({
                        "ruleId": "archmeld/archive-entry",
                        "level": "note",
                        "message": {
                            "text": format!(
                                "{}: {} ({})",
                                if entry.is_directory { "dir" } else { "file" },
                                entry.path,
                                entry.compression_method
                            )
                        },
                        "properties": {
                            "path": entry.path,
                            "compressedSize": entry.compressed_size,
                            "uncompressedSize": entry.uncompressed_size,
                            "compressionMethod": entry.compression_method,
                            "isDirectory": entry.is_directory
                        }
                    })
                })
                .collect();
            let log = make_sarif_log(&results);
            println!("{}", serde_json::to_string_pretty(&log)?);
        },
        OutputFormat::Markdown => {
            let rows: Vec<Vec<String>> = entries
                .iter()
                .map(|entry| {
                    vec![
                        entry.path.clone(),
                        ByteSize(entry.compressed_size).to_string(),
                        ByteSize(entry.uncompressed_size).to_string(),
                        entry.compression_method.clone(),
                        if entry.is_directory { "dir" } else { "file" }.into(),
                    ]
                })
                .collect();
            print_markdown_table(
                &["Path", "Compressed", "Uncompressed", "Method", "Type"],
                &rows,
            );
            eprintln!("{} entries in {}", entries.len(), input.display());
        },
        OutputFormat::Text => {
            let mut table = Table::new();
            table.load_preset(UTF8_FULL);
            table.set_header(vec!["Path", "Compressed", "Uncompressed", "Method", "Type"]);

            for entry in &entries {
                table.add_row([
                    entry.path.as_str(),
                    &ByteSize(entry.compressed_size).to_string(),
                    &ByteSize(entry.uncompressed_size).to_string(),
                    entry.compression_method.as_str(),
                    if entry.is_directory { "dir" } else { "file" },
                ]);
            }

            println!("{table}");
            eprintln!("{} entries in {}", entries.len(), input.display());
        },
    }

    Ok(())
}

/// Display format information for a file.
///
/// # Errors
///
/// Returns an error if the file cannot be read.
fn cmd_info(input: &Path, output_format: OutputFormat) -> anyhow::Result<()> {
    let data = read_file(input)?;
    let fmt = format::detect_format_from_path(input, &data);
    let info = format::format_info(fmt);

    match output_format {
        OutputFormat::Json => {
            let out = serde_json::to_string_pretty(&info)?;
            println!("{out}");
        },
        OutputFormat::Sarif => {
            let result = serde_json::json!({
                "ruleId": "archmeld/format-detection",
                "level": "note",
                "message": {
                    "text": format!(
                        "Detected format: {} ({})",
                        info.format, info.description
                    )
                },
                "properties": {
                    "format": info.format.to_string(),
                    "description": info.description,
                    "mimeType": info.mime_type,
                    "isArchive": info.is_archive,
                    "isCompressed": info.is_compressed,
                    "fileSize": data.len()
                }
            });
            let log = make_sarif_log(&[result]);
            println!("{}", serde_json::to_string_pretty(&log)?);
        },
        OutputFormat::Markdown => {
            print_markdown_table(
                &["Property", "Value"],
                &[
                    vec!["File".into(), input.display().to_string()],
                    vec!["Format".into(), info.format.to_string()],
                    vec!["Description".into(), info.description.clone()],
                    vec!["MIME type".into(), info.mime_type.clone()],
                    vec!["Is archive".into(), info.is_archive.to_string()],
                    vec!["Compressed".into(), info.is_compressed.to_string()],
                    vec!["File size".into(), ByteSize(data.len() as u64).to_string()],
                ],
            );
        },
        OutputFormat::Text => {
            println!("File:        {}", input.display());
            println!("Format:      {}", info.format);
            println!("Description: {}", info.description);
            println!("MIME type:   {}", info.mime_type);
            println!("Is archive:  {}", info.is_archive);
            println!("Compressed:  {}", info.is_compressed);
            println!("File size:   {}", ByteSize(data.len() as u64));
        },
    }

    Ok(())
}

/// Inspect gzip file headers and metadata.
///
/// # Errors
///
/// Returns an error on invalid gzip data, CRC failure, or I/O error.
#[allow(clippy::too_many_lines)]
fn cmd_gz_inspect(input: &Path, verify: bool, json: bool) -> anyhow::Result<()> {
    let data = read_file(input)?;
    let analysis = gzinspect::inspect(&data)?;

    if verify {
        let valid = gzinspect::verify_crc(&data)?;
        if valid {
            eprintln!("CRC-32 verification: PASSED");
        } else {
            eprintln!("CRC-32 verification: FAILED");
            anyhow::bail!("CRC-32 verification failed");
        }
    }

    if json {
        let out = serde_json::to_string_pretty(&analysis)?;
        println!("{out}");
    } else {
        let h = &analysis.header;
        println!("=== Gzip Header ===");
        println!("Compression:   {}", h.compression_method_name);
        println!("Flags:         0x{:02X}", h.flags);
        println!("  FTEXT:       {}", h.is_text);
        println!("  FHCRC:       {}", h.has_header_crc);
        println!("  FEXTRA:      {}", h.has_extra);
        println!("  FNAME:       {}", h.has_name);
        println!("  FCOMMENT:    {}", h.has_comment);
        println!("Mod time:      {}", h.mtime_formatted);
        println!(
            "Extra flags:   {} ({})",
            h.extra_flags, h.extra_flags_description
        );
        println!("OS:            {} ({})", h.os_code, h.os_name);

        if let Some(ref name) = h.original_name {
            println!("Orig name:     {name}");
        }
        if let Some(ref comment) = h.comment {
            println!("Comment:       {comment}");
        }
        if let Some(ref extra) = h.extra_data {
            println!("Extra data:    {} bytes", extra.len());
        }
        if let Some(crc) = h.header_crc16 {
            println!("Header CRC16:  0x{crc:04X}");
        }

        println!("\n=== File Statistics ===");
        println!("Header size:      {} bytes", h.header_size);
        println!("Compressed size:  {}", ByteSize(analysis.compressed_size));
        println!("File size:        {}", ByteSize(analysis.file_size));
        println!("SHA-256:          {}", analysis.sha256);
        println!("Members:          {}", analysis.member_count);
        println!("Multi-member:     {}", analysis.is_multi_member);

        if let Some(ref t) = analysis.trailer {
            println!("\n=== Gzip Trailer ===");
            println!("CRC-32:        0x{:08X}", t.crc32);
            println!(
                "Original size: {} (mod 2^32)",
                ByteSize(u64::from(t.original_size))
            );
        }
    }

    Ok(())
}

/// Handle LZ4 decompress, compress, and inspect subcommands.
///
/// # Errors
///
/// Returns an error on decompression/compression failure or I/O error.
fn cmd_lz4(action: Lz4Action) -> anyhow::Result<()> {
    match action {
        Lz4Action::Decompress { input, output } => {
            let data = read_file(&input)?;
            let decompressed = lz4::decompress_frame(&data)?;

            let out_path = output.unwrap_or_else(|| {
                let name = input.to_string_lossy();
                if let Some(stripped) = name.strip_suffix(".lz4") {
                    PathBuf::from(stripped)
                } else {
                    PathBuf::from(format!("{name}.decompressed"))
                }
            });

            fs::write(&out_path, &decompressed)?;
            eprintln!(
                "Decompressed {} -> {} ({})",
                input.display(),
                out_path.display(),
                ByteSize(decompressed.len() as u64)
            );

            Ok(())
        },
        Lz4Action::Compress { input, output } => {
            let data = read_file(&input)?;
            let compressed = lz4::compress_frame(&data)?;

            let out_path =
                output.unwrap_or_else(|| PathBuf::from(format!("{}.lz4", input.display())));

            fs::write(&out_path, &compressed)?;
            eprintln!(
                "Compressed {} -> {} ({} -> {})",
                input.display(),
                out_path.display(),
                ByteSize(data.len() as u64),
                ByteSize(compressed.len() as u64)
            );

            Ok(())
        },
        Lz4Action::Inspect { input, json } => {
            let data = read_file(&input)?;
            let info = lz4::parse_frame_header(&data)?;

            if json {
                let out = serde_json::to_string_pretty(&info)?;
                println!("{out}");
            } else {
                println!("=== LZ4 Frame Header ===");
                println!("Block independent:  {}", info.block_independent);
                println!("Block checksum:     {}", info.block_checksum);
                println!(
                    "Content size:       {}",
                    info.content_size
                        .map_or_else(|| "not present".into(), |s| ByteSize(s).to_string(),)
                );
                println!("Content checksum:   {}", info.content_checksum);
                println!(
                    "Block max size:     {}",
                    ByteSize(u64::from(info.block_max_size))
                );
            }

            Ok(())
        },
    }
}

/// Inspect a `StuffIt` archive.
///
/// # Errors
///
/// Returns an error on invalid `StuffIt` data or I/O error.
fn cmd_sit_inspect(input: &Path, json: bool) -> anyhow::Result<()> {
    let data = read_file(input)?;
    let analysis = sit::analyze(&data)?;

    if json {
        let out = serde_json::to_string_pretty(&analysis)?;
        println!("{out}");
    } else {
        let h = &analysis.header;
        println!("=== StuffIt Archive ===");
        println!("Signature:  {}", h.signature);
        println!("Version:    {}", h.version);
        println!(
            "Format:     {}",
            if h.is_stuffit5 {
                "StuffIt 5.x"
            } else {
                "Classic StuffIt"
            }
        );
        println!("Entries:    {}", h.num_entries);
        println!("Size:       {}", ByteSize(u64::from(h.archive_size)));

        if !analysis.entries.is_empty() {
            println!("\n=== Entries ===");
            let mut table = Table::new();
            table.load_preset(UTF8_FULL);
            table.set_header(vec![
                "Name",
                "Type",
                "Compressed",
                "Uncompressed",
                "Method",
                "Encrypted",
            ]);

            for entry in &analysis.entries {
                table.add_row([
                    entry.name.as_str(),
                    if entry.is_directory { "dir" } else { "file" },
                    &ByteSize(u64::from(entry.data_compressed_size)).to_string(),
                    &ByteSize(u64::from(entry.data_uncompressed_size)).to_string(),
                    entry.compression_method_name.as_str(),
                    if entry.is_encrypted { "YES" } else { "no" },
                ]);
            }

            println!("{table}");
        }
    }

    Ok(())
}

/// Inspect a Compact Pro archive.
///
/// # Errors
///
/// Returns an error on invalid Compact Pro data, CRC failure, or I/O error.
fn cmd_cpt_inspect(input: &Path, verify: bool, json: bool) -> anyhow::Result<()> {
    let data = read_file(input)?;
    let analysis = cpt::analyze(&data)?;

    if verify {
        let valid = cpt::verify(&data)?;
        if valid {
            eprintln!("Header CRC-32 verification: PASSED");
        } else {
            eprintln!("Header CRC-32 verification: FAILED");
        }
    }

    if json {
        let out = serde_json::to_string_pretty(&analysis)?;
        println!("{out}");
    } else {
        let h = &analysis.header;
        println!("=== Compact Pro Archive ===");
        println!("Volume:       {}", h.volume_number);
        println!("Header CRC:   0x{:08X}", h.header_crc32);
        println!("Entries:      {}", h.total_entries);
        if let Some(ref c) = h.comment {
            println!("Comment:      {c}");
        }

        if !analysis.entries.is_empty() {
            println!("\n=== Entries ===");
            let mut table = Table::new();
            table.load_preset(UTF8_FULL);
            table.set_header(vec![
                "Name",
                "Type",
                "Data Size",
                "Rsrc Size",
                "LZH",
                "Encrypted",
            ]);

            for entry in &analysis.entries {
                match entry {
                    cpt::CptEntry::Directory(d) => {
                        table.add_row([d.name.as_str(), "dir", "-", "-", "-", "-"]);
                    },
                    cpt::CptEntry::File(f) => {
                        let data_size = ByteSize(u64::from(f.data_uncompressed_size)).to_string();
                        let rsrc_size = ByteSize(u64::from(f.rsrc_uncompressed_size)).to_string();
                        let lzh = format!("d:{} r:{}", f.data_lzh, f.rsrc_lzh);
                        table.add_row([
                            f.name.as_str(),
                            "file",
                            &data_size,
                            &rsrc_size,
                            &lzh,
                            if f.is_encrypted { "YES" } else { "no" },
                        ]);
                    },
                }
            }

            println!("{table}");
        }
    }

    Ok(())
}

/// Verify archive integrity via checksums and trial extraction.
///
/// # Errors
///
/// Returns an error if verification fails or the file cannot be read.
// rust-doctor: acknowledged — per-format verification + 4 output format branches
#[allow(clippy::too_many_lines)]
fn cmd_verify(input: &Path, output_format: OutputFormat) -> anyhow::Result<()> {
    let data = read_file(input)?;
    let fmt = format::detect_format_from_path(input, &data);

    eprintln!("Verifying {} (format: {fmt})", input.display());

    // Run verification and collect a (status, message, detail) tuple.
    let (passed, check_name, message) = match fmt {
        ArchiveFormat::Gz | ArchiveFormat::TarGz => {
            let valid = gzinspect::verify_crc(&data)?;
            (
                valid,
                "Gzip CRC-32",
                if valid {
                    "Gzip CRC-32 verification succeeded".into()
                } else {
                    "Gzip CRC-32 verification failed".into()
                },
            )
        },
        ArchiveFormat::Zip => {
            let extractor = Extractor::new();
            let files = extractor.extract(&data, fmt)?;
            (
                true,
                "ZIP extraction",
                format!("ZIP archive verified ({} entries)", files.len()),
            )
        },
        ArchiveFormat::CompactPro => {
            let valid = cpt::verify(&data)?;
            (
                valid,
                "Compact Pro CRC-32",
                if valid {
                    "Compact Pro header CRC-32 verified".into()
                } else {
                    "Compact Pro header CRC-32 mismatch".into()
                },
            )
        },
        ArchiveFormat::StuffIt => {
            let analysis = sit::analyze(&data)?;
            (
                true,
                "StuffIt header",
                format!(
                    "StuffIt archive header parsed ({} entries)",
                    analysis.entries.len()
                ),
            )
        },
        ArchiveFormat::Lz4 => {
            let _info = lz4::parse_frame_header(&data)?;
            let decompressed = lz4::decompress_frame(&data)?;
            (
                true,
                "LZ4 decompression",
                format!(
                    "LZ4 frame decompressed successfully ({} bytes)",
                    decompressed.len()
                ),
            )
        },
        _ => {
            let extractor = Extractor::new();
            let files = extractor.extract(&data, fmt)?;
            (
                true,
                "Generic extraction",
                format!("Archive verified ({} entries)", files.len()),
            )
        },
    };

    let mut hasher = Sha256::new();
    hasher.update(&data);
    let hash = hex::encode(hasher.finalize());

    let status_label = if passed { "PASS" } else { "FAIL" };

    match output_format {
        OutputFormat::Json => {
            let obj = serde_json::json!({
                "file": input.display().to_string(),
                "format": fmt.to_string(),
                "check": check_name,
                "passed": passed,
                "message": message,
                "sha256": hash
            });
            println!("{}", serde_json::to_string_pretty(&obj)?);
        },
        OutputFormat::Sarif => {
            let result = serde_json::json!({
                "ruleId": "archmeld/verify",
                "level": if passed { "note" } else { "error" },
                "message": {
                    "text": format!("{status_label}: {message}")
                },
                "properties": {
                    "check": check_name,
                    "passed": passed,
                    "format": fmt.to_string(),
                    "sha256": hash
                }
            });
            let log = make_sarif_log(&[result]);
            println!("{}", serde_json::to_string_pretty(&log)?);
        },
        OutputFormat::Markdown => {
            print_markdown_table(
                &["Check", "Status", "Message", "SHA-256"],
                &[vec![
                    check_name.into(),
                    status_label.into(),
                    message.clone(),
                    hash,
                ]],
            );
        },
        OutputFormat::Text => {
            println!("{status_label}: {message}");
            println!("SHA-256: {hash}");
        },
    }

    std::io::stdout().flush()?;

    if !passed {
        anyhow::bail!("{status_label}: {message}");
    }

    Ok(())
}

/// Validate whether an archive can be decompressed (dry run, no output).
///
/// # Errors
///
/// Returns an error if the file cannot be read or the format is unknown.
// rust-doctor: acknowledged — RAR special case + 4 output format branches for Ok/Err
#[allow(clippy::too_many_lines)]
fn cmd_validate(
    input: &Path,
    format_arg: Option<FormatArg>,
    output_format: OutputFormat,
) -> anyhow::Result<()> {
    let data = read_file(input)?;
    let fmt = resolve_format(input, &data, format_arg);

    if fmt == ArchiveFormat::Unknown {
        anyhow::bail!(
            "Could not determine archive format for: {}",
            input.display()
        );
    }

    let extractor = Extractor::new();
    let result = extractor.validate(&data, fmt);

    match result {
        Ok(vr) => match output_format {
            OutputFormat::Json => {
                let out = serde_json::to_string_pretty(&vr)?;
                println!("{out}");
            },
            OutputFormat::Sarif => {
                let result_obj = serde_json::json!({
                    "ruleId": "archmeld/validate",
                    "level": "note",
                    "message": {
                        "text": format!(
                            "VALID: {} can be decompressed ({} entries, {})",
                            input.display(),
                            vr.entry_count,
                            ByteSize(vr.total_uncompressed_size)
                        )
                    },
                    "properties": {
                        "format": vr.format.to_string(),
                        "entryCount": vr.entry_count,
                        "totalUncompressedSize": vr.total_uncompressed_size,
                        "isValid": vr.is_valid
                    }
                });
                let log = make_sarif_log(&[result_obj]);
                println!("{}", serde_json::to_string_pretty(&log)?);
            },
            OutputFormat::Markdown => {
                print_markdown_table(
                    &["Property", "Value"],
                    &[
                        vec!["File".into(), input.display().to_string()],
                        vec!["Valid".into(), "true".into()],
                        vec!["Format".into(), vr.format.to_string()],
                        vec!["Entries".into(), vr.entry_count.to_string()],
                        vec![
                            "Uncompressed size".into(),
                            ByteSize(vr.total_uncompressed_size).to_string(),
                        ],
                    ],
                );
            },
            OutputFormat::Text => {
                println!("VALID: {} can be decompressed", input.display());
                println!("  Format:            {}", vr.format);
                println!("  Entries:           {}", vr.entry_count);
                println!(
                    "  Uncompressed size: {}",
                    ByteSize(vr.total_uncompressed_size)
                );
            },
        },
        Err(e) => {
            match output_format {
                OutputFormat::Json => {
                    let err_obj = serde_json::json!({
                        "is_valid": false,
                        "error": e.to_string(),
                    });
                    println!("{}", serde_json::to_string_pretty(&err_obj)?);
                },
                OutputFormat::Sarif => {
                    let result_obj = serde_json::json!({
                        "ruleId": "archmeld/validate",
                        "level": "error",
                        "message": {
                            "text": format!(
                                "INVALID: {} cannot be decompressed: {}",
                                input.display(),
                                e
                            )
                        },
                        "properties": {
                            "isValid": false,
                            "error": e.to_string()
                        }
                    });
                    let log = make_sarif_log(&[result_obj]);
                    println!("{}", serde_json::to_string_pretty(&log)?);
                },
                OutputFormat::Markdown => {
                    print_markdown_table(
                        &["Property", "Value"],
                        &[
                            vec!["File".into(), input.display().to_string()],
                            vec!["Valid".into(), "false".into()],
                            vec!["Error".into(), e.to_string()],
                        ],
                    );
                },
                OutputFormat::Text => {
                    eprintln!("INVALID: {} cannot be decompressed", input.display());
                    eprintln!("  Error: {e}");
                },
            }
            anyhow::bail!("Validation failed for: {}", input.display());
        },
    }

    Ok(())
}

#[allow(clippy::cast_precision_loss)]
/// Compress a file with `Zopfli` (high-ratio gzip).
///
/// # Errors
///
/// Returns an error if the file cannot be read or compression fails.
fn cmd_gz_compress(input: &Path, output: Option<&Path>) -> anyhow::Result<()> {
    let data = read_file(input)?;

    let out_path = output.map_or_else(
        || PathBuf::from(format!("{}.gz", input.display())),
        PathBuf::from,
    );

    let compressed = crate::archive::compress_zopfli(&data)?;
    fs::write(&out_path, &compressed)?;

    eprintln!(
        "Compressed {} -> {} ({} -> {}, ratio {:.1}x)",
        input.display(),
        out_path.display(),
        ByteSize(data.len() as u64),
        ByteSize(compressed.len() as u64),
        data.len() as f64 / compressed.len() as f64,
    );

    Ok(())
}
