use std::fs::{self, File};
use std::io::{self, Read, Write};
use std::path::Path;
use std::process;
use std::time::Instant;

use cyber_hemera_wgsl::GpuContext;

fn fmt_size(bytes: u64) -> String {
    if bytes < 1024 {
        format!("{bytes} B")
    } else if bytes < 1024 * 1024 {
        format!("{:.1} KB", bytes as f64 / 1024.0)
    } else if bytes < 1024 * 1024 * 1024 {
        format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
    } else {
        format!("{:.2} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
    }
}

/// Print a transient status line to stderr (overwritten by next status or final output).
fn status(msg: &str) {
    if io::IsTerminal::is_terminal(&io::stderr()) {
        eprint!("\x1b[2K\r\x1b[90m{msg}\x1b[0m");
    }
}

/// Clear the status line.
fn status_clear() {
    if io::IsTerminal::is_terminal(&io::stderr()) {
        eprint!("\x1b[2K\r");
    }
}

/// Progress callback that shows percentage on the status line (0.1% resolution).
fn progress_status(label: &str) -> impl Fn(usize, usize) + '_ {
    let last_permille = std::cell::Cell::new(u16::MAX);
    move |done, total| {
        if total == 0 { return; }
        let permille = ((done as u64 * 1000) / total as u64) as u16;
        if permille != last_permille.get() {
            last_permille.set(permille);
            status(&format!("{label} {}.{}%", permille / 10, permille % 10));
        }
    }
}

// โ”€โ”€ backend selection โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

#[derive(Clone, Copy, PartialEq)]
enum Backend {
    Cpu,
    Gpu,
}

impl std::fmt::Display for Backend {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            Backend::Cpu => write!(f, "cpu"),
            Backend::Gpu => write!(f, "gpu"),
        }
    }
}

struct Ctx {
    gpu: Option<GpuContext>,
    forced: Option<Backend>,
}

impl Ctx {
    fn new(forced: Option<Backend>) -> Self {
        // Default to CPU โ€” WGSL u64 emulation is slower than native CPU
        // on Apple Silicon. GPU available via --gpu for testing.
        let gpu = if forced == Some(Backend::Gpu) {
            let g = pollster::block_on(GpuContext::new());
            if g.is_none() {
                eprintln!("hemera: --gpu requested but no GPU adapter available");
                process::exit(1);
            }
            g
        } else {
            None
        };
        Self { gpu, forced }
    }

    fn backend(&self) -> Backend {
        if self.forced == Some(Backend::Cpu) {
            return Backend::Cpu;
        }
        if self.gpu.is_some() {
            Backend::Gpu
        } else {
            Backend::Cpu
        }
    }

    fn gpu(&self) -> &GpuContext {
        self.gpu.as_ref().unwrap()
    }

    fn root_hash(&self, data: &[u8], label: &str) -> (cyber_hemera::Hash, Backend) {
        let b = self.backend();
        let cb = progress_status(label);
        let h = if b == Backend::Gpu {
            pollster::block_on(self.gpu().root_hash_with_progress(data, &cb))
        } else {
            cyber_hemera::tree::root_hash_with_progress(data, &cb)
        };
        (h, b)
    }

    fn outboard(&self, data: &[u8]) -> ((cyber_hemera::Hash, Vec<u8>), Backend) {
        let b = self.backend();
        let result = if b == Backend::Gpu {
            pollster::block_on(self.gpu().outboard(data))
        } else {
            cyber_hemera::stream::outboard(data)
        };
        (result, b)
    }
}

/// Strip --gpu / --cpu from args, return (forced backend, remaining args).
fn parse_backend_flag(args: &[String]) -> (Option<Backend>, Vec<String>) {
    let mut forced = None;
    let mut rest = Vec::new();
    for a in args {
        match a.as_str() {
            "--gpu" => forced = Some(Backend::Gpu),
            "--cpu" => forced = Some(Backend::Cpu),
            _ => rest.push(a.clone()),
        }
    }
    (forced, rest)
}

fn print_timing(backend: Backend, elapsed: std::time::Duration) {
    let us = elapsed.as_nanos() as f64 / 1000.0;
    if us < 1000.0 {
        eprint!("\x1b[90m[{backend} {us:.0}us]\x1b[0m ");
    } else if us < 1_000_000.0 {
        eprint!("\x1b[90m[{backend} {:.2}ms]\x1b[0m ", us / 1000.0);
    } else {
        eprint!("\x1b[90m[{backend} {:.2}s]\x1b[0m ", us / 1_000_000.0);
    }
}

#[allow(unknown_lints, rs_no_vec, rs_no_string)]
fn main() {
    let all_args: Vec<String> = std::env::args().skip(1).collect();
    let (forced, args) = parse_backend_flag(&all_args);

    if args.iter().any(|a| a == "--help" || a == "-h") {
        print_usage();
        return;
    }

    match args.first().map(|s| s.as_str()) {
        Some("tree") => {
            if args.len() != 2 {
                eprintln!("hemera: tree requires <file>");
                process::exit(1);
            }
            process::exit(show_tree(&args[1]));
        }
        Some("prove") => {
            match args.len() {
                2 => process::exit(prove_node(&args[1], 0, 1)),
                3 => {
                    if let Some((s, e)) = args[2].split_once(':') {
                        let start: u64 = s.parse().unwrap_or_else(|_| {
                            eprintln!("hemera: invalid range start: {s}");
                            process::exit(1);
                        });
                        let end: u64 = e.parse().unwrap_or_else(|_| {
                            eprintln!("hemera: invalid range end: {e}");
                            process::exit(1);
                        });
                        process::exit(prove_node(&args[1], start, end));
                    } else {
                        let idx: u64 = args[2].parse().unwrap_or_else(|_| {
                            eprintln!("hemera: invalid chunk index: {}", args[2]);
                            process::exit(1);
                        });
                        process::exit(prove_node(&args[1], idx, idx + 1));
                    }
                }
                _ => {
                    eprintln!("hemera: prove requires <file> [chunk | start:end]");
                    process::exit(1);
                }
            }
        }
        Some("encode") => {
            let ctx = Ctx::new(forced);
            match args.len() {
                2 => process::exit(cmd_encode(&ctx, &args[1], None)),
                4 if args[2] == "-o" => process::exit(cmd_encode(&ctx, &args[1], Some(&args[3]))),
                _ => {
                    eprintln!("hemera: encode requires <file> [-o output]");
                    process::exit(1);
                }
            }
        }
        Some("decode") => match args.len() {
            3 => process::exit(cmd_decode(&args[1], &args[2], None)),
            5 if args[3] == "-o" => {
                process::exit(cmd_decode(&args[1], &args[2], Some(&args[4])))
            }
            _ => {
                eprintln!("hemera: decode requires <file> <hash> [-o output]");
                process::exit(1);
            }
        },
        Some("outboard") => {
            let ctx = Ctx::new(forced);
            match args.len() {
                2 => process::exit(cmd_outboard(&ctx, &args[1], None)),
                4 if args[2] == "-o" => process::exit(cmd_outboard(&ctx, &args[1], Some(&args[3]))),
                _ => {
                    eprintln!("hemera: outboard requires <file> [-o output]");
                    process::exit(1);
                }
            }
        }
        Some("keyed-hash") => {
            if args.len() != 3 {
                eprintln!("hemera: keyed-hash requires <key-hex> <file>");
                process::exit(1);
            }
            let ctx = Ctx::new(forced);
            process::exit(cmd_keyed_hash(&ctx, &args[1], &args[2]));
        }
        Some("derive-key") => {
            if args.len() != 3 {
                eprintln!("hemera: derive-key requires <context> <file>");
                process::exit(1);
            }
            let ctx = Ctx::new(forced);
            process::exit(cmd_derive_key(&ctx, &args[1], &args[2]));
        }
        Some("prove-batch") => {
            if args.len() < 3 {
                eprintln!("hemera: prove-batch requires <file> <index>...");
                process::exit(1);
            }
            let indices: Vec<u64> = args[2..].iter().map(|s| {
                s.parse().unwrap_or_else(|_| {
                    eprintln!("hemera: invalid chunk index: {s}");
                    process::exit(1);
                })
            }).collect();
            process::exit(cmd_prove_batch(&args[1], &indices));
        }
        Some("verify-batch") => {
            if args.len() != 3 {
                eprintln!("hemera: verify-batch requires <data-file> <proof-file>");
                process::exit(1);
            }
            process::exit(cmd_verify_batch(&args[1], &args[2]));
        }
        Some("sparse") => {
            if args.len() < 2 {
                eprintln!("hemera: sparse requires a subcommand (new, insert, get, prove, verify, root)");
                process::exit(1);
            }
            process::exit(cmd_sparse(&args[1..]));
        }
        Some("verify") => match args.len() {
            3 => {
                let ctx = Ctx::new(forced);
                process::exit(verify_single(&ctx, &args[1], &args[2]));
            }
            2 => {
                let ctx = Ctx::new(forced);
                process::exit(verify_checksums(&ctx, &args[1]));
            }
            _ => {
                eprintln!("hemera: verify requires <file> <hash> or <checksums-file>");
                process::exit(1);
            }
        },
        _ => {}
    }

    let ctx = Ctx::new(forced);
    let has_files = args.iter().any(|a| !a.starts_with('-'));

    if !has_files {
        if io::IsTerminal::is_terminal(&io::stdin()) {
            print_usage();
            return;
        }
        status("reading stdinโ€ฆ");
        let mut data = Vec::new();
        io::stdin().read_to_end(&mut data).unwrap_or_else(|e| {
            eprintln!("hemera: {e}");
            process::exit(1);
        });
        let t = Instant::now();
        let (hash, backend) = ctx.root_hash(&data, "hashing");
        status_clear();
        print_timing(backend, t.elapsed());
        println!("{}  -", hash);
    } else {
        for arg in &args {
            if !arg.starts_with('-') {
                hash_path(&ctx, Path::new(arg));
            }
        }
    }
}

#[allow(unknown_lints, rs_no_vec)]
fn hash_path(ctx: &Ctx, path: &Path) {
    let meta = match fs::metadata(path) {
        Ok(m) => m,
        Err(e) => {
            eprintln!("hemera: {}: {e}", path.display());
            process::exit(1);
        }
    };

    if meta.is_dir() {
        let mut entries: Vec<_> = match fs::read_dir(path) {
            Ok(rd) => rd.filter_map(|e| e.ok()).collect(),
            Err(e) => {
                eprintln!("hemera: {}: {e}", path.display());
                process::exit(1);
            }
        };
        entries.sort_by_key(|e| e.file_name());
        for entry in entries {
            hash_path(ctx, &entry.path());
        }
    } else {
        match hash_file(ctx, path) {
            Ok((hex, backend, elapsed)) => {
                print_timing(backend, elapsed);
                println!("{hex}  {}", path.display());
            }
            Err(e) => {
                eprintln!("hemera: {}: {e}", path.display());
                process::exit(1);
            }
        }
    }
}

#[allow(unknown_lints, rs_no_vec)]
fn hash_file(ctx: &Ctx, path: &Path) -> io::Result<(String, Backend, std::time::Duration)> {
    let meta = fs::metadata(path)?;
    status(&format!("reading {} ({})", path.display(), fmt_size(meta.len())));
    let mut file = File::open(path)?;
    let mut data = Vec::new();
    file.read_to_end(&mut data)?;
    let label = format!("hashing {}", path.display());
    let t = Instant::now();
    let (hash, backend) = ctx.root_hash(&data, &label);
    status_clear();
    Ok((hash.to_string(), backend, t.elapsed()))
}

fn show_tree(path: &str) -> i32 {
    let size = match fs::metadata(path) {
        Ok(m) => m.len(),
        Err(e) => {
            eprintln!("hemera: {path}: {e}");
            return 1;
        }
    };
    status(&format!("reading {path} ({})โ€ฆ", fmt_size(size)));
    let data = match fs::read(path) {
        Ok(d) => d,
        Err(e) => {
            eprintln!("hemera: {path}: {e}");
            return 1;
        }
    };

    let n = cyber_hemera::tree::num_chunks(data.len());
    let tree = cyber_hemera::tree::build_tree_with_progress(&data, progress_status("building tree"));
    status_clear();

    println!("file: {path}");
    println!("size: {} bytes", data.len());
    println!("chunks: {n}");
    println!("depth: {}", tree.depth);
    println!("root: {}", tree.hash);
    println!();
    print_tree_node(&tree, "", "");
    0
}

fn print_tree_node(node: &cyber_hemera::tree::TreeNode, connector: &str, prefix: &str) {
    let short_hash = &node.hash.to_string()[..16];
    let idx = node.index;
    if let Some(chunk) = node.chunk_index {
        println!("{connector}[{idx}] chunk({chunk}) {short_hash}โ€ฆ");
    } else {
        println!("{connector}[{idx}] node {short_hash}โ€ฆ");
    }

    if let (Some(left), Some(right)) = (&node.left, &node.right) {
        print_tree_node(left, &format!("{prefix}โ”œโ”€โ”€ "), &format!("{prefix}โ”‚   "));
        print_tree_node(right, &format!("{prefix}โ””โ”€โ”€ "), &format!("{prefix}    "));
    }
}

fn prove_node(path: &str, start: u64, end: u64) -> i32 {
    let size = fs::metadata(path).map(|m| m.len()).unwrap_or(0);
    status(&format!("reading {path} ({})โ€ฆ", fmt_size(size)));
    let data = match fs::read(path) {
        Ok(d) => d,
        Err(e) => {
            eprintln!("hemera: {path}: {e}");
            return 1;
        }
    };

    let n = cyber_hemera::tree::num_chunks(data.len());
    if start >= end || end > n {
        eprintln!("hemera: invalid range [{start}..{end}) for {n} chunks");
        return 1;
    }

    status(&format!("proving range [{start}..{end})โ€ฆ"));
    let (root, proof) = cyber_hemera::tree::prove_range(&data, start, end);
    status_clear();

    println!("root: {root}");
    if end - start == 1 {
        println!("chunk: {start}/{n}");
    } else {
        println!("range: [{start}..{end}) of {n} chunks");
    }
    println!("depth: {}", proof.depth());
    for (i, sibling) in proof.siblings().iter().enumerate() {
        let (dir, hash) = match sibling {
            cyber_hemera::tree::Sibling::Left(h) => ("L", h),
            cyber_hemera::tree::Sibling::Right(h) => ("R", h),
        };
        println!("  [{i}] {dir} {hash}");
    }
    0
}

fn verify_single(ctx: &Ctx, path: &str, expected: &str) -> i32 {
    match hash_file(ctx, Path::new(path)) {
        Ok((actual, backend, elapsed)) => {
            print_timing(backend, elapsed);
            if actual == expected {
                println!("{path}: OK");
                0
            } else {
                println!("{path}: FAILED");
                1
            }
        }
        Err(e) => {
            eprintln!("hemera: {path}: {e}");
            1
        }
    }
}

fn verify_checksums(ctx: &Ctx, path: &str) -> i32 {
    let content = match fs::read_to_string(path) {
        Ok(c) => c,
        Err(e) => {
            eprintln!("hemera: {path}: {e}");
            return 1;
        }
    };

    let mut failures = 0;
    let mut total = 0;

    for line in content.lines() {
        let line = line.trim();
        if line.is_empty() {
            continue;
        }

        let Some((expected_hex, filename)) = line.split_once("  ") else {
            eprintln!("hemera: malformed line: {line}");
            failures += 1;
            continue;
        };

        total += 1;
        match hash_file(ctx, Path::new(filename.trim())) {
            Ok((actual_hex, backend, elapsed)) => {
                print_timing(backend, elapsed);
                if actual_hex == expected_hex.trim() {
                    println!("{filename}: OK");
                } else {
                    println!("{filename}: FAILED");
                    failures += 1;
                }
            }
            Err(e) => {
                eprintln!("hemera: {filename}: {e}");
                failures += 1;
            }
        }
    }

    if failures > 0 {
        eprintln!("hemera: WARNING: {failures} of {total} computed checksums did NOT match");
        1
    } else {
        0
    }
}

fn cmd_encode(_ctx: &Ctx, path: &str, output: Option<&str>) -> i32 {
    let size = fs::metadata(path).map(|m| m.len()).unwrap_or(0);
    status(&format!("reading {path} ({})โ€ฆ", fmt_size(size)));
    let data = match fs::read(path) {
        Ok(d) => d,
        Err(e) => {
            eprintln!("hemera: {path}: {e}");
            return 1;
        }
    };

    status(&format!("encoding ({})โ€ฆ", fmt_size(size)));
    let t = Instant::now();
    let (root, encoded) = cyber_hemera::stream::encode(&data);
    status_clear();
    let elapsed = t.elapsed();
    let default_path = format!("{path}.hemera");
    let out_path = output.unwrap_or(&default_path);

    if let Err(e) = fs::write(out_path, &encoded) {
        eprintln!("hemera: {out_path}: {e}");
        return 1;
    }

    // encode uses CPU internally (stream format needs sequential structure),
    // but we report the context backend for consistency
    print_timing(Backend::Cpu, elapsed);
    println!("{root}  {out_path}");
    0
}

fn cmd_decode(path: &str, hash_hex: &str, output: Option<&str>) -> i32 {
    let encoded = match fs::read(path) {
        Ok(d) => d,
        Err(e) => {
            eprintln!("hemera: {path}: {e}");
            return 1;
        }
    };

    let root = match parse_hash(hash_hex) {
        Some(h) => h,
        None => {
            eprintln!("hemera: invalid hash: {hash_hex}");
            return 1;
        }
    };

    match cyber_hemera::stream::decode(&encoded, &root) {
        Ok(data) => {
            if let Some(out_path) = output {
                if let Err(e) = fs::write(out_path, &data) {
                    eprintln!("hemera: {out_path}: {e}");
                    return 1;
                }
                println!("{path}: OK โ†’ {out_path}");
            } else {
                let stdout = io::stdout();
                let mut handle = stdout.lock();
                if let Err(e) = handle.write_all(&data) {
                    eprintln!("hemera: {e}");
                    return 1;
                }
            }
            0
        }
        Err(e) => {
            eprintln!("hemera: {path}: {e}");
            1
        }
    }
}

fn cmd_outboard(ctx: &Ctx, path: &str, output: Option<&str>) -> i32 {
    let size = fs::metadata(path).map(|m| m.len()).unwrap_or(0);
    status(&format!("reading {path} ({})โ€ฆ", fmt_size(size)));
    let data = match fs::read(path) {
        Ok(d) => d,
        Err(e) => {
            eprintln!("hemera: {path}: {e}");
            return 1;
        }
    };

    status(&format!("computing outboard ({})โ€ฆ", fmt_size(size)));
    let t = Instant::now();
    let ((root, ob), backend) = ctx.outboard(&data);
    status_clear();
    let elapsed = t.elapsed();
    let default_path = format!("{path}.obao");
    let out_path = output.unwrap_or(&default_path);

    if let Err(e) = fs::write(out_path, &ob) {
        eprintln!("hemera: {out_path}: {e}");
        return 1;
    }

    print_timing(backend, elapsed);
    println!("{root}  {out_path}");
    0
}

fn cmd_keyed_hash(_ctx: &Ctx, key_hex: &str, path: &str) -> i32 {
    if key_hex.len() != cyber_hemera::OUTPUT_BYTES * 2 {
        eprintln!(
            "hemera: key must be {} hex chars ({} bytes)",
            cyber_hemera::OUTPUT_BYTES * 2,
            cyber_hemera::OUTPUT_BYTES
        );
        return 1;
    }

    let key = match parse_hex_fixed::<{ cyber_hemera::OUTPUT_BYTES }>(key_hex) {
        Some(k) => k,
        None => {
            eprintln!("hemera: invalid hex key: {key_hex}");
            return 1;
        }
    };

    let data = match fs::read(path) {
        Ok(d) => d,
        Err(e) => {
            eprintln!("hemera: {path}: {e}");
            return 1;
        }
    };

    // keyed_hash is a single sponge โ€” CPU path
    let t = Instant::now();
    let h = cyber_hemera::keyed_hash(&key, &data);
    let elapsed = t.elapsed();

    print_timing(Backend::Cpu, elapsed);
    println!("{h}  {path}");
    0
}

fn cmd_derive_key(_ctx: &Ctx, context: &str, path: &str) -> i32 {
    let data = match fs::read(path) {
        Ok(d) => d,
        Err(e) => {
            eprintln!("hemera: {path}: {e}");
            return 1;
        }
    };

    let t = Instant::now();
    let key = cyber_hemera::derive_key(context, &data);
    let elapsed = t.elapsed();

    print_timing(Backend::Cpu, elapsed);
    for byte in &key {
        print!("{byte:02x}");
    }
    println!("  {path}");
    0
}

fn cmd_prove_batch(path: &str, indices: &[u64]) -> i32 {
    let size = fs::metadata(path).map(|m| m.len()).unwrap_or(0);
    status(&format!("reading {path} ({})โ€ฆ", fmt_size(size)));
    let data = match fs::read(path) {
        Ok(d) => d,
        Err(e) => {
            eprintln!("hemera: {path}: {e}");
            return 1;
        }
    };

    let mut sorted = indices.to_vec();
    sorted.sort();
    sorted.dedup();

    status(&format!("proving batch ({} indices)โ€ฆ", sorted.len()));
    let (root, proof) = cyber_hemera::batch::prove_batch(&data, &sorted);
    status_clear();
    println!("root: {root}");
    println!("indices: {sorted:?}");
    println!("siblings: {}", proof.siblings.len());
    println!("chunks: {}", proof.num_chunks);
    for (i, sib) in proof.siblings.iter().enumerate() {
        println!("  [{i}] {sib}");
    }
    0
}

fn cmd_verify_batch(data_path: &str, proof_path: &str) -> i32 {
    let data = match fs::read(data_path) {
        Ok(d) => d,
        Err(e) => {
            eprintln!("hemera: {data_path}: {e}");
            return 1;
        }
    };

    let content = match fs::read_to_string(proof_path) {
        Ok(c) => c,
        Err(e) => {
            eprintln!("hemera: {proof_path}: {e}");
            return 1;
        }
    };

    // Parse prove-batch output format:
    //   root: <hash>
    //   indices: [0, 1, 3]
    //   siblings: 5
    //   chunks: 8
    //   [0] <hash>
    //   [1] <hash>
    //   ...
    let mut root = None;
    let mut indices: Vec<u64> = Vec::new();
    let mut num_chunks: u64 = 0;
    let mut siblings: Vec<cyber_hemera::Hash> = Vec::new();

    for line in content.lines() {
        let line = line.trim();
        if let Some(rest) = line.strip_prefix("root: ") {
            root = parse_hash(rest.trim());
            if root.is_none() {
                eprintln!("hemera: invalid root hash in proof: {rest}");
                return 1;
            }
        } else if let Some(rest) = line.strip_prefix("indices: ") {
            let trimmed = rest.trim().trim_start_matches('[').trim_end_matches(']');
            indices = trimmed
                .split(',')
                .map(|s| s.trim().parse::<u64>())
                .collect::<Result<Vec<_>, _>>()
                .unwrap_or_else(|_| {
                    eprintln!("hemera: invalid indices in proof: {rest}");
                    process::exit(1);
                });
        } else if let Some(rest) = line.strip_prefix("chunks: ") {
            num_chunks = rest.trim().parse().unwrap_or_else(|_| {
                eprintln!("hemera: invalid chunks count in proof: {rest}");
                process::exit(1);
            });
        } else if line.starts_with('[') {
            // Sibling line: [N] <hash>
            if let Some((_idx_part, hash_part)) = line.split_once("] ") {
                match parse_hash(hash_part.trim()) {
                    Some(h) => siblings.push(h),
                    None => {
                        eprintln!("hemera: invalid sibling hash: {hash_part}");
                        return 1;
                    }
                }
            }
        }
    }

    let root = match root {
        Some(r) => r,
        None => {
            eprintln!("hemera: no root hash found in proof file");
            return 1;
        }
    };

    if indices.is_empty() {
        eprintln!("hemera: no indices found in proof file");
        return 1;
    }

    if num_chunks == 0 {
        eprintln!("hemera: no chunk count found in proof file");
        return 1;
    }

    // Extract chunk data from the original file.
    let chunk_size = cyber_hemera::CHUNK_SIZE;
    let chunks_data: Vec<&[u8]> = indices
        .iter()
        .map(|&idx| {
            let start = idx as usize * chunk_size;
            let end = (start + chunk_size).min(data.len());
            &data[start..end]
        })
        .collect();

    let proof = cyber_hemera::batch::BatchInclusionProof {
        indices,
        siblings,
        num_chunks,
        root,
    };

    let t = Instant::now();
    let valid = cyber_hemera::batch::verify_batch(&chunks_data, &proof);
    let elapsed = t.elapsed();

    print_timing(Backend::Cpu, elapsed);
    if valid {
        println!("verify-batch: OK");
        0
    } else {
        println!("verify-batch: FAILED");
        1
    }
}

#[allow(unknown_lints, rs_no_vec, rs_no_string)]
fn cmd_sparse(args: &[String]) -> i32 {
    match args[0].as_str() {
        "hash-leaf" => {
            if args.len() != 3 {
                eprintln!("hemera: sparse hash-leaf requires <key-hex-32bytes> <file>");
                return 1;
            }
            let key = match parse_hex_fixed::<32>(&args[1]) {
                Some(k) => k,
                None => {
                    eprintln!("hemera: invalid 32-byte hex key: {}", args[1]);
                    return 1;
                }
            };
            let data = match fs::read(&args[2]) {
                Ok(d) => d,
                Err(e) => {
                    eprintln!("hemera: {}: {e}", args[2]);
                    return 1;
                }
            };
            let mut input = Vec::with_capacity(32 + data.len());
            input.extend_from_slice(&key);
            input.extend_from_slice(&data);
            let h = cyber_hemera::tree::hash_leaf(&input, 0, false);
            println!("{h}");
            0
        }
        "verify" => {
            // hemera sparse verify <proof-file> <root-hash> [--value <file>] [--depth N]
            // Omitting --value means non-inclusion proof.
            if args.len() < 3 {
                eprintln!("hemera: sparse verify requires <proof-file> <root-hash> [--value <file>] [--depth N]");
                return 1;
            }
            let proof_path = &args[1];
            let root_hex = &args[2];
            let mut value_path: Option<&str> = None;
            let mut depth: u32 = cyber_hemera::sparse::DEFAULT_DEPTH;
            let mut i = 3;
            while i < args.len() {
                match args[i].as_str() {
                    "--value" => {
                        i += 1;
                        if i >= args.len() {
                            eprintln!("hemera: --value requires an argument");
                            return 1;
                        }
                        value_path = Some(&args[i]);
                    }
                    "--depth" => {
                        i += 1;
                        if i >= args.len() {
                            eprintln!("hemera: --depth requires an argument");
                            return 1;
                        }
                        depth = args[i].parse().unwrap_or_else(|_| {
                            eprintln!("hemera: invalid depth: {}", args[i]);
                            process::exit(1);
                        });
                    }
                    other => {
                        eprintln!("hemera: unknown flag: {other}");
                        return 1;
                    }
                }
                i += 1;
            }
            cmd_sparse_verify(proof_path, root_hex, value_path, depth)
        }
        "sentinel" => {
            // hemera sparse sentinel [depth]
            let depth: u32 = if args.len() > 1 {
                args[1].parse().unwrap_or_else(|_| {
                    eprintln!("hemera: invalid depth: {}", args[1]);
                    process::exit(1);
                })
            } else {
                cyber_hemera::sparse::DEFAULT_DEPTH
            };
            let table = cyber_hemera::sparse::sentinel_table(depth);
            println!("depth: {depth}");
            println!("root sentinel: {}", table[depth as usize]);
            0
        }
        _ => {
            eprintln!("hemera: unknown sparse subcommand: {}", args[0]);
            eprintln!("  available: hash-leaf, verify, sentinel");
            1
        }
    }
}

#[allow(unknown_lints, rs_no_vec)]
fn cmd_sparse_verify(proof_path: &str, root_hex: &str, value_path: Option<&str>, depth: u32) -> i32 {
    let root = match parse_hash(root_hex) {
        Some(h) => h,
        None => {
            eprintln!("hemera: invalid root hash: {root_hex}");
            return 1;
        }
    };

    let content = match fs::read_to_string(proof_path) {
        Ok(c) => c,
        Err(e) => {
            eprintln!("hemera: {proof_path}: {e}");
            return 1;
        }
    };

    // Parse proof file format:
    //   key: <64 hex chars>
    //   bitmask: <64 hex chars>
    //   siblings: N
    //     [0] <hash>
    //     [1] <hash>
    let mut key = None;
    let mut bitmask = None;
    let mut siblings: Vec<cyber_hemera::Hash> = Vec::new();

    for line in content.lines() {
        let line = line.trim();
        if let Some(rest) = line.strip_prefix("key: ") {
            key = parse_hex_fixed::<32>(rest.trim());
            if key.is_none() {
                eprintln!("hemera: invalid key in proof: {rest}");
                return 1;
            }
        } else if let Some(rest) = line.strip_prefix("bitmask: ") {
            bitmask = parse_hex_fixed::<32>(rest.trim());
            if bitmask.is_none() {
                eprintln!("hemera: invalid bitmask in proof: {rest}");
                return 1;
            }
        } else if line.starts_with('[') {
            if let Some((_idx_part, hash_part)) = line.split_once("] ") {
                match parse_hash(hash_part.trim()) {
                    Some(h) => siblings.push(h),
                    None => {
                        eprintln!("hemera: invalid sibling hash: {hash_part}");
                        return 1;
                    }
                }
            }
        }
    }

    let key = match key {
        Some(k) => k,
        None => {
            eprintln!("hemera: no key found in proof file");
            return 1;
        }
    };
    let bitmask = match bitmask {
        Some(b) => b,
        None => {
            eprintln!("hemera: no bitmask found in proof file");
            return 1;
        }
    };

    let proof = cyber_hemera::sparse::CompressedSparseProof { key, bitmask, siblings };

    let value_data = match value_path {
        Some(p) => match fs::read(p) {
            Ok(d) => Some(d),
            Err(e) => {
                eprintln!("hemera: {p}: {e}");
                return 1;
            }
        },
        None => None,
    };

    let t = Instant::now();
    let valid = cyber_hemera::sparse::SparseTree::verify(
        &proof,
        value_data.as_deref(),
        &root,
        depth,
    );
    let elapsed = t.elapsed();

    print_timing(Backend::Cpu, elapsed);
    if valid {
        let kind = if value_data.is_some() { "inclusion" } else { "non-inclusion" };
        println!("sparse verify ({kind}): OK");
        0
    } else {
        println!("sparse verify: FAILED");
        1
    }
}

fn parse_hash(hex: &str) -> Option<cyber_hemera::Hash> {
    let bytes = parse_hex_fixed::<{ cyber_hemera::OUTPUT_BYTES }>(hex)?;
    Some(cyber_hemera::Hash::from_bytes(bytes))
}

fn parse_hex_fixed<const N: usize>(hex: &str) -> Option<[u8; N]> {
    if hex.len() != N * 2 {
        return None;
    }
    let mut out = [0u8; N];
    for (i, byte) in out.iter_mut().enumerate() {
        *byte = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).ok()?;
    }
    Some(out)
}

fn print_usage() {
    eprintln!(
        "\
\x1b[31m
    โ–ˆโ–ˆโ•—  โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ•—   โ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—  โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—
\x1b[33m    โ–ˆโ–ˆโ•‘  โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—
\x1b[32m    โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—  โ–ˆโ–ˆโ•”โ–ˆโ–ˆโ–ˆโ–ˆโ•”โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—  โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘
\x1b[36m    โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ•  โ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ•  โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•‘
\x1b[34m    โ–ˆโ–ˆโ•‘  โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘ โ•šโ•โ• โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘  โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘  โ–ˆโ–ˆโ•‘
\x1b[35m    โ•šโ•โ•  โ•šโ•โ•โ•šโ•โ•โ•โ•โ•โ•โ•โ•šโ•โ•     โ•šโ•โ•โ•šโ•โ•โ•โ•โ•โ•โ•โ•šโ•โ•  โ•šโ•โ•โ•šโ•โ•  โ•šโ•โ•
\x1b[0m\x1b[37m    the hash for superintelligence\x1b[0m
\x1b[90m
    Poseidon2 ยท Goldilocks field ยท p = 2^64 - 2^32 + 1
    t=16  R_F=8  R_P=64  d=7  rate=8  output=64B
    genesis: [0x63, 0x79, 0x62, 0x65, 0x72]
\x1b[0m
  hemera file1.txt file2.txt         Hash files
  hemera src/                        Hash directory (recursive)
  echo hello | hemera                Hash stdin
  hemera tree file.txt               Show tree structure
  hemera prove file.txt [chunk]      Leaf inclusion proof
  hemera prove file.txt 0:4          Subtree inclusion proof
  hemera verify file.txt <hash>      Verify file against hash
  hemera verify sums.txt             Verify checksums from file
  hemera encode file.txt [-o out]    Encode to verified stream
  hemera decode file.hemera <hash>   Decode and verify stream
  hemera outboard file.txt [-o out]  Compute outboard hash tree
  hemera prove-batch file 0 1 3      Batch inclusion proof
  hemera verify-batch file proof.txt Verify batch proof
  hemera sparse hash-leaf <key> file Sparse leaf hash
  hemera sparse verify proof root    Verify sparse proof
  hemera sparse sentinel [depth]     Show sentinel root hash
  hemera keyed-hash <key-hex> file   Keyed hash
  hemera derive-key <context> file   Derive key from context
\x1b[90m
  flags:    --gpu  force GPU backend
            --cpu  force CPU backend
            (default: CPU)
\x1b[0m
  -h, --help  Print this help"
    );
}

Dimensions

trident/src/main.rs
optica/src/main.rs
rs/rsc/src/main.rs
nebu/cli/src/main.rs

Local Graph