holger-agent-lib 0.4.0

Holger agent library: airgap, export, push operations using connectors
use anyhow::{Result, Context};
use std::path::PathBuf;
use holger_traits::{ConnectorTrait, RemoteAsset};

/// Transfer assets from a connector to a znippy archive
pub async fn transfer_to_znippy(
    connector: &dyn ConnectorTrait,
    assets: Vec<RemoteAsset>,
    output: PathBuf,
) -> Result<()> {
    use znippy_compress::stream_packer::{compress_stream, ArchiveEntry};

    println!("📦 Creating znippy archive: {}", output.display());
    let compressor = compress_stream(&output, false)?;
    let sender = compressor.sender().clone();

    let total = assets.len();
    let mut downloaded = 0usize;

    for (i, asset) in assets.iter().enumerate() {
        print!("   [{}/{}] {} ... ", i + 1, total, asset.path);
        match connector.download_asset(asset).await {
            Ok(bytes) => {
                sender.send(ArchiveEntry {
                    relative_path: asset.path.clone(),
                    data: bytes.clone(),
                }).context("Failed to send to compressor")?;
                downloaded += 1;
                println!("✓ ({} bytes)", bytes.len());
            }
            Err(e) => {
                println!("{}", e);
            }
        }
    }

    drop(sender);
    let report = compressor.finish()?;
    println!("\n✅ Znippy archive complete: {} assets packed ({} bytes compressed)",
             downloaded, report.compressed_bytes);
    Ok(())
}

/// Transfer assets from a connector to a directory
pub async fn transfer_to_directory(
    connector: &dyn ConnectorTrait,
    assets: Vec<RemoteAsset>,
    output: PathBuf,
) -> Result<()> {
    println!("📁 Downloading to directory: {}", output.display());
    std::fs::create_dir_all(&output)
        .with_context(|| format!("Failed to create output directory: {}", output.display()))?;

    let total = assets.len();
    let mut downloaded = 0usize;
    let mut failed = 0usize;

    for (i, asset) in assets.iter().enumerate() {
        let dest = output.join(&asset.path);
        if let Some(parent) = dest.parent() {
            std::fs::create_dir_all(parent)?;
        }

        print!("   [{}/{}] {} ... ", i + 1, total, asset.path);

        match connector.download_asset(asset).await {
            Ok(bytes) => {
                std::fs::write(&dest, &bytes)
                    .with_context(|| format!("Failed to write {}", dest.display()))?;
                downloaded += 1;
                println!("✓ ({} bytes)", bytes.len());
            }
            Err(e) => {
                failed += 1;
                println!("{}", e);
            }
        }
    }

    println!("\n✅ Done! {} downloaded, {} failed", downloaded, failed);
    Ok(())
}

/// Push assets from a directory to a connector
pub async fn push_directory_to_connector(
    connector: &dyn ConnectorTrait,
    directory: PathBuf,
    repository: &str,
) -> Result<()> {
    println!("📦 Pushing from directory: {}", directory.display());

    let crate_files = find_crate_files(&directory)?;
    println!("   Found {} .crate files to push", crate_files.len());

    if crate_files.is_empty() {
        println!("   Nothing to push.");
        return Ok(());
    }

    let total = crate_files.len();
    let mut pushed = 0usize;
    let mut failed = 0usize;

    for (i, crate_path) in crate_files.iter().enumerate() {
        let relative = extract_crate_relative_path(crate_path);
        let upload_path = relative.to_string_lossy();

        print!("   [{}/{}] {} ... ", i + 1, total, upload_path);

        let data = std::fs::read(crate_path)
            .with_context(|| format!("Failed to read {}", crate_path.display()))?;

        match connector.upload_asset(repository, &upload_path, &data).await {
            Ok(()) => {
                pushed += 1;
                println!("");
            }
            Err(e) => {
                failed += 1;
                println!("{}", e);
            }
        }
    }

    println!("\n✅ Push complete: {} succeeded, {} failed", pushed, failed);
    Ok(())
}

/// Push assets from a znippy archive to a connector
pub async fn push_znippy_to_connector(
    connector: &dyn ConnectorTrait,
    archive: PathBuf,
    repository: &str,
) -> Result<()> {
    use znippy_common::{ZnippyArchive, ZnippyReader};

    println!("📦 Reading znippy archive: {}", archive.display());
    let znippy = ZnippyArchive::open(&archive)?;
    let files = znippy.list_files()?;
    println!("   Found {} files in archive", files.len());

    let total = files.len();
    let mut pushed = 0usize;
    let mut failed = 0usize;

    for (i, path) in files.iter().enumerate() {
        print!("   [{}/{}] {} ... ", i + 1, total, path);
        match znippy.extract_file(path) {
            Ok(data) => {
                match connector.upload_asset(repository, path, &data).await {
                    Ok(()) => { pushed += 1; println!(""); }
                    Err(e) => { failed += 1; println!("{}", e); }
                }
            }
            Err(e) => { failed += 1; println!("✗ extract: {}", e); }
        }
    }

    println!("\n✅ Push complete: {} succeeded, {} failed", pushed, failed);
    Ok(())
}

fn extract_crate_relative_path(path: &std::path::Path) -> PathBuf {
    let components: Vec<_> = path.components().collect();
    for (i, comp) in components.iter().enumerate() {
        if comp.as_os_str() == "crates" {
            return components[i..].iter().collect();
        }
    }
    PathBuf::from(path.file_name().unwrap_or_default())
}

fn find_crate_files(dir: &std::path::Path) -> Result<Vec<PathBuf>> {
    let mut files = Vec::new();
    walk_dir_recursive(dir, &mut files)?;
    files.sort();
    Ok(files)
}

fn walk_dir_recursive(dir: &std::path::Path, files: &mut Vec<PathBuf>) -> Result<()> {
    if !dir.is_dir() {
        return Ok(());
    }
    for entry in std::fs::read_dir(dir)? {
        let entry = entry?;
        let path = entry.path();
        if path.is_dir() {
            walk_dir_recursive(&path, files)?;
        } else if path.extension().and_then(|e| e.to_str()) == Some("crate") {
            files.push(path);
        }
    }
    Ok(())
}