partiri-cli 0.1.6

partiri CLI — Deploy and manage services on Partiri Cloud
use owo_colors::OwoColorize;

use crate::client::{ApiClient, PrometheusData, PrometheusResponse};
use crate::config::PartiriConfig;
use crate::error::Result;
use crate::output::{colored_job_status, sparkline};

fn extract_values(data: &PrometheusData) -> Vec<f64> {
    data.result
        .first()
        .map(|r| {
            r.values
                .iter()
                .filter_map(|(_, v)| v.parse::<f64>().ok())
                .collect()
        })
        .unwrap_or_default()
}

fn format_bytes_rate(bytes: f64) -> String {
    if bytes >= 1_048_576.0 {
        format!("{:.1} MB/s", bytes / 1_048_576.0)
    } else if bytes >= 1024.0 {
        format!("{:.0} KB/s", bytes / 1024.0)
    } else {
        format!("{:.0} B/s", bytes)
    }
}

fn print_sparkline(label: &str, resp: &PrometheusResponse, fmt: fn(f64) -> String) {
    print!("  {}  ", label.bold());
    let vals = extract_values(&resp.data);
    if vals.is_empty() {
        println!("{}", "no data".dimmed());
    } else {
        let last = *vals.last().unwrap();
        let trimmed: Vec<f64> = vals.into_iter().rev().take(60).rev().collect();
        println!("{}  {}", sparkline(&trimmed).cyan(), fmt(last).dimmed());
    }
}

fn format_bytes(bytes: f64) -> String {
    if bytes >= 1_073_741_824.0 {
        format!("{:.1} GB", bytes / 1_073_741_824.0)
    } else if bytes >= 1_048_576.0 {
        format!("{:.0} MB", bytes / 1_048_576.0)
    } else if bytes >= 1024.0 {
        format!("{:.0} KB", bytes / 1024.0)
    } else {
        format!("{:.0} B", bytes)
    }
}

pub fn run(client: &ApiClient, config: &PartiriConfig) -> Result<()> {
    let id = config.id_or_err()?;

    let service = client.read_service(id)?;
    let deploy_tag = config.deploy_tag.as_deref();
    let cpu_resp = client.read_metrics_cpu(id, deploy_tag);
    let mem_resp = client.read_metrics_memory(id, deploy_tag);
    let net_resp = client.read_metrics_network(id, deploy_tag);
    let jobs_resp = client.list_service_jobs(id);

    println!(
        "\n  {} {}\n",
        service.name.bold(),
        format!("({})", service.deploy_type).dimmed(),
    );

    match cpu_resp {
        Ok(r) => print_sparkline("CPU", &r, |v| format!("{:.3} cores", v)),
        Err(_) => println!("  {}  {}", "CPU".bold(), "unavailable".dimmed()),
    }
    match mem_resp {
        Ok(r) => print_sparkline("RAM", &r, format_bytes),
        Err(_) => println!("  {}  {}", "RAM".bold(), "unavailable".dimmed()),
    }
    match net_resp {
        Ok(r) => {
            print_sparkline("NET↓", &r.download, format_bytes_rate);
            print_sparkline("NET↑", &r.upload, format_bytes_rate);
        }
        Err(_) => println!("  {}  {}", "NET".bold(), "unavailable".dimmed()),
    }

    println!();
    println!("  {}", "Recent jobs".bold());
    match jobs_resp {
        Ok(jobs) => {
            let recent: Vec<_> = jobs.iter().take(5).collect();
            if recent.is_empty() {
                println!("  {}", "No jobs found.".dimmed());
            } else {
                for job in recent {
                    let ts = job
                        .created_at
                        .as_deref()
                        .filter(|s| s.len() >= 16 && s.as_bytes().get(10) == Some(&b'T'))
                        .map(|s| format!("{} {}", &s[..10], &s[11..16]))
                        .unwrap_or_else(|| "".to_string());
                    let ref_str = job
                        .deploy_ref
                        .as_deref()
                        .map(|r| format!(" {}", r.get(..7).unwrap_or(r).dimmed()))
                        .unwrap_or_default();
                    println!(
                        "  {}  {}{}  {}",
                        ts.dimmed(),
                        job.job_type.bold(),
                        ref_str,
                        colored_job_status(&job.status),
                    );
                }
            }
        }
        Err(_) => println!("  {}", "unavailable".dimmed()),
    }
    println!();

    Ok(())
}