velesdb-cli 1.12.0

Interactive CLI and REPL for VelesDB with VelesQL support
//! Handlers for data commands: `export`, `import`, `get`, `upsert`, `delete-points`.

use std::path::{Path, PathBuf};

use anyhow::Result;
use colored::Colorize;

use crate::cli_types::MetricArg;
use crate::cli_types::StorageModeArg;
use crate::import;

/// Handles the `export` subcommand: exports a vector collection to JSON.
pub fn handle_export(
    path: &Path,
    collection: &str,
    output: Option<PathBuf>,
    include_vectors: bool,
) -> Result<()> {
    let db = velesdb_core::Database::open(path)?;
    let col = db.get_vector_collection(collection).ok_or_else(|| {
        anyhow::anyhow!(
            "Vector collection '{}' not found. Export requires a vector collection.",
            collection
        )
    })?;

    let cfg = col.config();
    let output_path = output.unwrap_or_else(|| PathBuf::from(format!("{collection}.json")));

    println!(
        "Exporting {} records from {}...",
        cfg.point_count,
        collection.green()
    );

    let records = collect_export_records(&col, cfg.point_count, include_vectors);
    std::fs::write(&output_path, serde_json::to_string_pretty(&records)?)?;
    println!(
        "{} Exported {} records to {}",
        "\u{2713}".green(),
        records.len(),
        output_path.display().to_string().green()
    );
    Ok(())
}

/// Collects all records from a vector collection for export.
fn collect_export_records(
    col: &velesdb_core::VectorCollection,
    point_count: usize,
    include_vectors: bool,
) -> Vec<serde_json::Value> {
    let mut records = Vec::new();
    let batch_size = 1000;

    for batch_start in (0..point_count).step_by(batch_size) {
        let ids: Vec<u64> =
            ((batch_start as u64 + 1)..=((batch_start + batch_size) as u64)).collect();
        let points = col.get(&ids);

        for point in points.into_iter().flatten() {
            let mut record = serde_json::Map::new();
            record.insert("id".to_string(), serde_json::json!(point.id));
            if include_vectors {
                record.insert("vector".to_string(), serde_json::json!(point.vector));
            }
            if let Some(payload) = &point.payload {
                record.insert("payload".to_string(), payload.clone());
            }
            records.push(serde_json::Value::Object(record));
        }
    }
    records
}

/// Handles the `import` subcommand: imports data from CSV or JSONL.
#[allow(clippy::too_many_arguments)] // Reason: mirrors clap subcommand field count directly
pub fn handle_import(
    file: &Path,
    database: &Path,
    collection: String,
    dimension: Option<usize>,
    metric: MetricArg,
    storage_mode: StorageModeArg,
    id_column: String,
    vector_column: String,
    batch_size: usize,
    progress: bool,
) -> Result<()> {
    let db = velesdb_core::Database::open(database)?;
    let config = import::ImportConfig {
        collection,
        dimension,
        metric: metric.into(),
        storage_mode: storage_mode.into(),
        batch_size,
        id_column,
        vector_column,
        show_progress: progress,
    };

    let ext = file.extension().and_then(|e| e.to_str()).unwrap_or("");

    let stats = match ext.to_lowercase().as_str() {
        "jsonl" | "ndjson" => import::import_jsonl(&db, file, &config)?,
        "csv" => import::import_csv(&db, file, &config)?,
        _ => {
            anyhow::bail!("Unsupported file format: {}. Use .csv or .jsonl", ext);
        }
    };

    print_import_summary(&stats);
    Ok(())
}

/// Prints a summary after a successful import.
fn print_import_summary(stats: &import::ImportStats) {
    println!("\n{}", "Import Summary".green().bold());
    println!("  Total records:    {}", stats.total);
    println!("  Imported:         {}", stats.imported.to_string().green());
    if stats.errors > 0 {
        println!("  Errors:           {}", stats.errors.to_string().red());
    }
    println!("  Duration:         {} ms", stats.duration_ms);
    println!(
        "  Throughput:       {:.0} records/sec",
        stats.records_per_sec()
    );
}

/// Handles the `get` subcommand: retrieves a single point by ID.
pub fn handle_get(path: &Path, collection: &str, id: u64, format: &str) -> Result<()> {
    let db = velesdb_core::Database::open(path)?;
    let col = db
        .get_vector_collection(collection)
        .ok_or_else(|| anyhow::anyhow!("Collection '{}' not found", collection))?;

    let points = col.get(&[id]);

    if format == "json" {
        print_point_json(points);
    } else {
        print_point_table(points, id);
    }
    Ok(())
}

/// Prints a point as JSON.
fn print_point_json(points: Vec<Option<velesdb_core::Point>>) {
    if let Some(point) = points.into_iter().flatten().next() {
        let output = serde_json::json!({
            "id": point.id,
            "vector": point.vector,
            "payload": point.payload
        });
        // Reason: doc output to stdout; formatting failure is not recoverable
        if let Ok(json) = serde_json::to_string_pretty(&output) {
            println!("{json}");
        }
    } else {
        println!("null");
    }
}

/// Prints a point as a colored table.
fn print_point_table(points: Vec<Option<velesdb_core::Point>>, id: u64) {
    if let Some(point) = points.into_iter().flatten().next() {
        println!("\n{}", "Point Found".bold().underline());
        println!("  ID: {}", point.id.to_string().green());
        println!("  Vector: [{} dimensions]", point.vector.len());
        if let Some(payload) = &point.payload {
            println!("  Payload: {payload}");
        }
    } else {
        println!("{} Point with ID {} not found", "\u{274c}".red(), id);
    }
}

/// Handles the `upsert` subcommand: inserts or updates a single point.
pub fn handle_upsert(
    path: &Path,
    collection: &str,
    id: u64,
    vector: Option<String>,
    payload: Option<String>,
) -> Result<()> {
    let db = velesdb_core::Database::open(path)?;
    let col = db
        .get_vector_collection(collection)
        .ok_or_else(|| anyhow::anyhow!("Vector collection '{}' not found", collection))?;

    let vec_data = parse_vector_json(vector)?;
    let payload_data = parse_payload_json(payload)?;

    let point = velesdb_core::Point::new(id, vec_data, payload_data);
    col.upsert(vec![point])
        .map_err(|e| anyhow::anyhow!("Upsert failed: {e}"))?;

    println!(
        "{} Upserted point {} into '{}'",
        "\u{2705}".green(),
        id.to_string().green(),
        collection.cyan()
    );
    Ok(())
}

/// Parses an optional vector JSON string into a `Vec<f32>`.
fn parse_vector_json(raw: Option<String>) -> Result<Vec<f32>> {
    match raw {
        Some(v) => {
            serde_json::from_str(&v).map_err(|e| anyhow::anyhow!("Invalid vector JSON: {e}"))
        }
        None => Ok(vec![]),
    }
}

/// Parses an optional payload JSON string into a `serde_json::Value`.
fn parse_payload_json(raw: Option<String>) -> Result<Option<serde_json::Value>> {
    match raw {
        Some(p) => {
            let v = serde_json::from_str(&p)
                .map_err(|e| anyhow::anyhow!("Invalid payload JSON: {e}"))?;
            Ok(Some(v))
        }
        None => Ok(None),
    }
}

/// Handles the `delete-points` subcommand: removes points by ID.
pub fn handle_delete_points(path: &Path, collection: &str, ids: &[u64]) -> Result<()> {
    let db = velesdb_core::Database::open(path)?;
    let col = db
        .get_vector_collection(collection)
        .ok_or_else(|| anyhow::anyhow!("Vector collection '{}' not found", collection))?;

    col.delete(ids)
        .map_err(|e| anyhow::anyhow!("Delete failed: {e}"))?;

    println!(
        "{} Deleted {} point(s) from '{}'",
        "\u{2705}".green(),
        ids.len(),
        collection.cyan()
    );
    Ok(())
}