lcpfs 2026.1.102

LCP File System - A ZFS-inspired copy-on-write filesystem for Rust
// Copyright 2025 LunaOS Contributors
// SPDX-License-Identifier: Apache-2.0

//! Analytics report generation

use alloc::string::{String, ToString};
use alloc::vec::Vec;
use alloc::{format, vec};

use super::types::*;

/// Report format options
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReportFormat {
    /// Plain text
    Text,
    /// JSON format
    Json,
    /// Prometheus metrics format
    Prometheus,
}

/// Format a full report
pub fn format_report(report: &FullReport, format: ReportFormat) -> String {
    match format {
        ReportFormat::Text => format_text_report(report),
        ReportFormat::Json => format_json_report(report),
        ReportFormat::Prometheus => format_prometheus_report(report),
    }
}

/// Format as plain text
fn format_text_report(report: &FullReport) -> String {
    let mut lines = Vec::new();

    lines.push("=== Storage Analytics Report ===".to_string());
    lines.push(format!("Dataset: {}", report.dataset));
    lines.push(format!("Health Score: {}/100", report.health_score));
    lines.push(String::new());

    // Usage summary
    lines.push("--- Usage Summary ---".to_string());
    lines.push(format!(
        "Total Capacity: {} bytes",
        report.usage.total_capacity
    ));
    lines.push(format!("Used Space: {} bytes", report.usage.used_space));
    lines.push(format!("Free Space: {} bytes", report.usage.free_space));
    lines.push(format!("Usage: {:.1}%", report.usage.usage_percent));
    lines.push(format!("Files: {}", report.usage.file_count));
    lines.push(format!("Directories: {}", report.usage.directory_count));
    lines.push(format!("Snapshots: {}", report.usage.snapshot_count));
    lines.push(format!(
        "Data Reduction Ratio: {:.2}x",
        report.usage.reduction_ratio
    ));
    lines.push(String::new());

    // I/O stats
    lines.push("--- I/O Statistics ---".to_string());
    lines.push(format!("Read Ops: {}", report.io.read_ops));
    lines.push(format!("Write Ops: {}", report.io.write_ops));
    lines.push(format!("Bytes Read: {}", report.io.bytes_read));
    lines.push(format!("Bytes Written: {}", report.io.bytes_written));
    lines.push(format!(
        "Avg Read Latency: {} us",
        report.io.avg_read_latency_us
    ));
    lines.push(format!(
        "Avg Write Latency: {} us",
        report.io.avg_write_latency_us
    ));
    lines.push(format!(
        "Cache Hit Rate: {:.1}%",
        report.io.cache_hit_rate * 100.0
    ));
    lines.push(String::new());

    // Capacity forecast
    lines.push("--- Capacity Forecast ---".to_string());
    lines.push(format!(
        "Daily Growth Rate: {} bytes/day",
        report.forecast.daily_growth_rate
    ));
    if report.forecast.days_until_full >= 0 {
        lines.push(format!(
            "Days Until Full: {}",
            report.forecast.days_until_full
        ));
    } else {
        lines.push("Days Until Full: N/A (declining)".to_string());
    }
    lines.push(format!(
        "Recommendation: {:?}",
        report.forecast.recommendation
    ));
    lines.push(format!(
        "Confidence: {:.0}%",
        report.forecast.confidence * 100.0
    ));
    lines.push(String::new());

    // Recommendations
    if !report.recommendations.is_empty() {
        lines.push("--- Recommendations ---".to_string());
        for rec in &report.recommendations {
            lines.push(format!("• {}", rec));
        }
    }

    lines.join("\n")
}

/// Format as JSON
fn format_json_report(report: &FullReport) -> String {
    let mut json = String::new();
    json.push_str("{\n");
    json.push_str(&format!("  \"dataset\": \"{}\",\n", report.dataset));
    json.push_str(&format!("  \"health_score\": {},\n", report.health_score));
    json.push_str(&format!("  \"generated_at\": {},\n", report.generated_at));

    // Usage
    json.push_str("  \"usage\": {\n");
    json.push_str(&format!(
        "    \"total_capacity\": {},\n",
        report.usage.total_capacity
    ));
    json.push_str(&format!(
        "    \"used_space\": {},\n",
        report.usage.used_space
    ));
    json.push_str(&format!(
        "    \"free_space\": {},\n",
        report.usage.free_space
    ));
    json.push_str(&format!(
        "    \"usage_percent\": {:.2},\n",
        report.usage.usage_percent
    ));
    json.push_str(&format!(
        "    \"file_count\": {},\n",
        report.usage.file_count
    ));
    json.push_str(&format!(
        "    \"reduction_ratio\": {:.2}\n",
        report.usage.reduction_ratio
    ));
    json.push_str("  },\n");

    // I/O
    json.push_str("  \"io\": {\n");
    json.push_str(&format!("    \"read_ops\": {},\n", report.io.read_ops));
    json.push_str(&format!("    \"write_ops\": {},\n", report.io.write_ops));
    json.push_str(&format!(
        "    \"read_throughput\": {},\n",
        report.io.read_throughput
    ));
    json.push_str(&format!(
        "    \"write_throughput\": {},\n",
        report.io.write_throughput
    ));
    json.push_str(&format!(
        "    \"cache_hit_rate\": {:.2}\n",
        report.io.cache_hit_rate
    ));
    json.push_str("  },\n");

    // Forecast
    json.push_str("  \"forecast\": {\n");
    json.push_str(&format!(
        "    \"daily_growth_rate\": {},\n",
        report.forecast.daily_growth_rate
    ));
    json.push_str(&format!(
        "    \"days_until_full\": {},\n",
        report.forecast.days_until_full
    ));
    json.push_str(&format!(
        "    \"confidence\": {:.2}\n",
        report.forecast.confidence
    ));
    json.push_str("  },\n");

    // Recommendations
    json.push_str("  \"recommendations\": [\n");
    for (i, rec) in report.recommendations.iter().enumerate() {
        if i < report.recommendations.len() - 1 {
            json.push_str(&format!("    \"{}\",\n", rec));
        } else {
            json.push_str(&format!("    \"{}\"\n", rec));
        }
    }
    json.push_str("  ]\n");

    json.push_str("}\n");
    json
}

/// Format as Prometheus metrics
fn format_prometheus_report(report: &FullReport) -> String {
    let mut lines = Vec::new();
    let dataset = &report.dataset;

    // Usage metrics
    lines.push(format!(
        "lcpfs_capacity_bytes{{dataset=\"{}\"}} {}",
        dataset, report.usage.total_capacity
    ));
    lines.push(format!(
        "lcpfs_used_bytes{{dataset=\"{}\"}} {}",
        dataset, report.usage.used_space
    ));
    lines.push(format!(
        "lcpfs_free_bytes{{dataset=\"{}\"}} {}",
        dataset, report.usage.free_space
    ));
    lines.push(format!(
        "lcpfs_usage_percent{{dataset=\"{}\"}} {}",
        dataset, report.usage.usage_percent
    ));
    lines.push(format!(
        "lcpfs_file_count{{dataset=\"{}\"}} {}",
        dataset, report.usage.file_count
    ));
    lines.push(format!(
        "lcpfs_directory_count{{dataset=\"{}\"}} {}",
        dataset, report.usage.directory_count
    ));
    lines.push(format!(
        "lcpfs_snapshot_count{{dataset=\"{}\"}} {}",
        dataset, report.usage.snapshot_count
    ));
    lines.push(format!(
        "lcpfs_reduction_ratio{{dataset=\"{}\"}} {}",
        dataset, report.usage.reduction_ratio
    ));

    // I/O metrics
    lines.push(format!(
        "lcpfs_read_ops_total{{dataset=\"{}\"}} {}",
        dataset, report.io.read_ops
    ));
    lines.push(format!(
        "lcpfs_write_ops_total{{dataset=\"{}\"}} {}",
        dataset, report.io.write_ops
    ));
    lines.push(format!(
        "lcpfs_read_bytes_total{{dataset=\"{}\"}} {}",
        dataset, report.io.bytes_read
    ));
    lines.push(format!(
        "lcpfs_write_bytes_total{{dataset=\"{}\"}} {}",
        dataset, report.io.bytes_written
    ));
    lines.push(format!(
        "lcpfs_read_latency_us{{dataset=\"{}\"}} {}",
        dataset, report.io.avg_read_latency_us
    ));
    lines.push(format!(
        "lcpfs_write_latency_us{{dataset=\"{}\"}} {}",
        dataset, report.io.avg_write_latency_us
    ));
    lines.push(format!(
        "lcpfs_read_throughput_bps{{dataset=\"{}\"}} {}",
        dataset, report.io.read_throughput
    ));
    lines.push(format!(
        "lcpfs_write_throughput_bps{{dataset=\"{}\"}} {}",
        dataset, report.io.write_throughput
    ));
    lines.push(format!(
        "lcpfs_cache_hit_rate{{dataset=\"{}\"}} {}",
        dataset, report.io.cache_hit_rate
    ));

    // Forecast metrics
    lines.push(format!(
        "lcpfs_daily_growth_bytes{{dataset=\"{}\"}} {}",
        dataset, report.forecast.daily_growth_rate
    ));
    lines.push(format!(
        "lcpfs_days_until_full{{dataset=\"{}\"}} {}",
        dataset, report.forecast.days_until_full
    ));

    // Health
    lines.push(format!(
        "lcpfs_health_score{{dataset=\"{}\"}} {}",
        dataset, report.health_score
    ));

    // Dedup metrics
    lines.push(format!(
        "lcpfs_dedup_ratio{{dataset=\"{}\"}} {}",
        dataset, report.dedup.dedup_ratio
    ));
    lines.push(format!(
        "lcpfs_dedup_space_saved_bytes{{dataset=\"{}\"}} {}",
        dataset, report.dedup.space_saved
    ));

    // Compression metrics
    lines.push(format!(
        "lcpfs_compression_ratio{{dataset=\"{}\"}} {}",
        dataset, report.compression.compression_ratio
    ));
    lines.push(format!(
        "lcpfs_compression_space_saved_bytes{{dataset=\"{}\"}} {}",
        dataset, report.compression.space_saved
    ));

    lines.join("\n")
}

/// Generate a summary line
pub fn format_summary(report: &FullReport) -> String {
    format!(
        "{}: {:.1}% used ({} / {} bytes), Health: {}/100",
        report.dataset,
        report.usage.usage_percent,
        format_bytes(report.usage.used_space),
        format_bytes(report.usage.total_capacity),
        report.health_score
    )
}

/// Format bytes in human-readable form
pub fn format_bytes(bytes: u64) -> String {
    const KB: u64 = 1024;
    const MB: u64 = 1024 * KB;
    const GB: u64 = 1024 * MB;
    const TB: u64 = 1024 * GB;
    const PB: u64 = 1024 * TB;

    if bytes >= PB {
        format!("{:.2} PB", bytes as f64 / PB as f64)
    } else if bytes >= TB {
        format!("{:.2} TB", bytes as f64 / TB as f64)
    } else 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)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_format_bytes() {
        assert_eq!(format_bytes(0), "0 B");
        assert_eq!(format_bytes(512), "512 B");
        assert_eq!(format_bytes(1024), "1.00 KB");
        assert_eq!(format_bytes(1024 * 1024), "1.00 MB");
        assert_eq!(format_bytes(1024 * 1024 * 1024), "1.00 GB");
    }

    #[test]
    fn test_format_text_report() {
        let report = FullReport {
            dataset: "test".to_string(),
            health_score: 85,
            ..Default::default()
        };

        let text = format_text_report(&report);
        assert!(text.contains("test"));
        assert!(text.contains("85"));
    }

    #[test]
    fn test_format_prometheus() {
        let report = FullReport {
            dataset: "pool/data".to_string(),
            usage: UsageSummary {
                used_space: 1000,
                ..Default::default()
            },
            ..Default::default()
        };

        let prom = format_prometheus_report(&report);
        assert!(prom.contains("lcpfs_used_bytes"));
        assert!(prom.contains("pool/data"));
    }
}