shaha 0.2.1

Hash database builder and reverse lookup tool
use std::path::PathBuf;

use anyhow::Result;
use clap::{Args, ValueEnum};

use crate::storage::{ParquetStorage, R2Storage, Storage};

#[derive(Clone, ValueEnum)]
pub enum OutputFormat {
    Plain,
    Json,
}

#[derive(Args)]
pub struct InfoArgs {
    #[arg(default_value = "hashes.parquet")]
    pub database: PathBuf,

    #[arg(short, long, default_value = "plain")]
    pub format: OutputFormat,
    #[command(flatten)]
    pub r2: super::R2Args,
}

pub fn run(args: InfoArgs) -> Result<()> {
    let (stats, location) = if args.r2.enabled {
        let r2_config = args.r2.build_config(&args.database)?;
        let url = r2_config.s3_url();
        let storage = R2Storage::new(r2_config)?;
        (storage.stats()?, url)
    } else {
        let storage = ParquetStorage::new(&args.database);
        (storage.stats()?, args.database.display().to_string())
    };

    match args.format {
        OutputFormat::Plain => print_plain(&location, &stats),
        OutputFormat::Json => print_json(&location, &stats)?,
    }

    Ok(())
}

fn print_plain(location: &str, stats: &crate::storage::Stats) {
    println!("Database:   {}", location);
    println!("Records:    {}", stats.total_records);
    if stats.file_size_bytes > 0 {
        println!("Size:       {}", format_bytes(stats.file_size_bytes));
    }
    println!(
        "Algorithms: {}",
        if stats.algorithms.is_empty() {
            "-".to_string()
        } else {
            stats.algorithms.join(", ")
        }
    );
    println!(
        "Sources:    {}",
        if stats.sources.is_empty() {
            "-".to_string()
        } else {
            stats.sources.join(", ")
        }
    );
}

fn print_json(location: &str, stats: &crate::storage::Stats) -> Result<()> {
    #[derive(serde::Serialize)]
    struct JsonInfo {
        database: String,
        total_records: usize,
        #[serde(skip_serializing_if = "Option::is_none")]
        file_size_bytes: Option<u64>,
        algorithms: Vec<String>,
        sources: Vec<String>,
    }

    let info = JsonInfo {
        database: location.to_string(),
        total_records: stats.total_records,
        file_size_bytes: if stats.file_size_bytes > 0 {
            Some(stats.file_size_bytes)
        } else {
            None
        },
        algorithms: stats.algorithms.clone(),
        sources: stats.sources.clone(),
    };

    println!("{}", serde_json::to_string_pretty(&info)?);
    Ok(())
}


fn format_bytes(bytes: u64) -> String {
    const KB: u64 = 1024;
    const MB: u64 = KB * 1024;
    const GB: u64 = MB * 1024;

    if bytes >= GB {
        format!("{:.2} GB", bytes as f64 / GB as f64)
    } else if bytes >= MB {
        format!("{:.2} MB", bytes as f64 / MB as f64)
    } else if bytes >= KB {
        format!("{:.2} KB", bytes as f64 / KB as f64)
    } else {
        format!("{} B", bytes)
    }
}