use comfy_table::{Cell, Table};
use serde_json::Value;

use crate::OutputFormat;

pub fn print_json(value: &Value) {
    println!("{}", serde_json::to_string_pretty(value).unwrap());
}

pub fn print_spans_table(value: &Value) {
    let spans = match value.get("spans").and_then(|s| s.as_array()) {
        Some(s) => s,
        None => {
            println!("No spans found.");
            return;
        }
    };

    if spans.is_empty() {
        println!("No spans found.");
        return;
    }

    // Only show the LLM column when at least one span is an LLM call, so
    // non-LLM traces keep their compact layout.
    let has_llm = spans
        .iter()
        .any(|s| s.get("llm").is_some_and(|v| !v.is_null()));

    let mut table = Table::new();
    let mut header = vec![
        "Trace ID",
        "Span ID",
        "Service",
        "Operation",
        "Duration (ms)",
        "Status",
    ];
    if has_llm {
        header.push("LLM");
    }
    table.set_header(header);

    for span in spans {
        let trace_id = span["trace_id"].as_str().unwrap_or("-");
        let short_trace = if trace_id.len() > 12 {
            &trace_id[..12]
        } else {
            trace_id
        };
        let mut row = vec![
            Cell::new(format!("{short_trace}…")),
            Cell::new(span["span_id"].as_str().unwrap_or("-")),
            Cell::new(span["service"].as_str().unwrap_or("-")),
            Cell::new(span["operation"].as_str().unwrap_or("-")),
            Cell::new(format!(
                "{:.1}",
                span["duration_ms"].as_f64().unwrap_or(0.0)
            )),
            Cell::new(span["status"].as_str().unwrap_or("-")),
        ];
        if has_llm {
            row.push(Cell::new(format_llm_summary(&span["llm"])));
        }
        table.add_row(row);
    }

    println!("{table}");
}

/// Render `{ "rows": [ {col: val, ...}, ... ] }` from the SQL endpoint as a
/// table, using the first row's keys as columns. Surfaces an error payload.
pub fn print_sql_rows(value: &Value) {
    if let Some(err) = value.get("error").and_then(|e| e.as_str()) {
        println!("error: {err}");
        return;
    }
    let rows = match value.get("rows").and_then(|r| r.as_array()) {
        Some(r) if !r.is_empty() => r,
        _ => {
            println!("No rows.");
            return;
        }
    };
    let columns: Vec<String> = match rows[0].as_object() {
        Some(obj) => obj.keys().cloned().collect(),
        None => return,
    };
    let mut table = Table::new();
    table.set_header(columns.clone());
    for row in rows {
        let cells: Vec<Cell> = columns
            .iter()
            .map(|c| {
                let v = &row[c];
                let s = match v {
                    Value::String(s) => s.clone(),
                    Value::Null => "-".to_string(),
                    other => other.to_string(),
                };
                Cell::new(s)
            })
            .collect();
        table.add_row(cells);
    }
    println!("{table}");
}

/// One-line summary of a span's LLM extension for the table view, e.g.
/// `claude-opus-4-7 · 1540 tok · $0.0185`. Returns `-` for non-LLM spans.
fn format_llm_summary(llm: &Value) -> String {
    if llm.is_null() {
        return "-".to_string();
    }
    let mut parts = Vec::new();
    if let Some(model) = llm["model"].as_str().filter(|s| !s.is_empty()) {
        parts.push(model.to_string());
    } else if let Some(provider) = llm["provider"].as_str().filter(|s| !s.is_empty()) {
        parts.push(provider.to_string());
    }
    if let Some(tokens) = llm["total_tokens"].as_u64() {
        parts.push(format!("{tokens} tok"));
    }
    if let Some(cost) = llm["cost_usd"].as_f64() {
        parts.push(format!("${cost:.4}"));
    }
    if parts.is_empty() {
        "-".to_string()
    } else {
        parts.join(" · ")
    }
}

pub fn print_services_table(value: &Value) {
    let services = match value.get("services").and_then(|s| s.as_array()) {
        Some(s) => s,
        None => {
            println!("No services found.");
            return;
        }
    };

    if services.is_empty() {
        println!("No services found.");
        return;
    }

    let mut table = Table::new();
    table.set_header(vec![
        "Service",
        "Spans",
        "Traces",
        "Avg Duration (ms)",
        "Error Rate",
    ]);

    for svc in services {
        table.add_row(vec![
            Cell::new(svc["name"].as_str().unwrap_or("-")),
            Cell::new(svc["span_count"].as_i64().unwrap_or(0).to_string()),
            Cell::new(svc["trace_count"].as_i64().unwrap_or(0).to_string()),
            Cell::new(format!(
                "{:.1}",
                svc["avg_duration_ms"].as_f64().unwrap_or(0.0)
            )),
            Cell::new(format!(
                "{:.2}%",
                svc["error_rate"].as_f64().unwrap_or(0.0) * 100.0
            )),
        ]);
    }

    println!("{table}");
}

pub fn print_logs_table(value: &Value) {
    let logs = match value.get("logs").and_then(|l| l.as_array()) {
        Some(l) => l,
        None => {
            println!("No logs found.");
            return;
        }
    };

    if logs.is_empty() {
        println!("No logs found.");
        return;
    }

    let mut table = Table::new();
    table.set_header(vec!["Timestamp", "Service", "Severity", "Body", "Trace ID"]);

    for log in logs {
        let timestamp = log["timestamp"].as_str().unwrap_or("-");
        let short_ts = if timestamp.len() > 19 {
            &timestamp[..19]
        } else {
            timestamp
        };
        let body = log["body"].as_str().unwrap_or("-");
        let short_body = if body.len() > 80 {
            format!("{}…", &body[..80])
        } else {
            body.to_string()
        };
        let trace_id = log["trace_id"].as_str().unwrap_or("-");
        let short_trace = if trace_id.len() > 12 {
            format!("{}…", &trace_id[..12])
        } else {
            trace_id.to_string()
        };
        table.add_row(vec![
            Cell::new(short_ts),
            Cell::new(log["service"].as_str().unwrap_or("-")),
            Cell::new(log["severity"].as_str().unwrap_or("-")),
            Cell::new(short_body),
            Cell::new(short_trace),
        ]);
    }

    println!("{table}");
}

pub fn print_metrics_table(value: &Value) {
    let metrics = match value.get("metrics").and_then(|m| m.as_array()) {
        Some(m) => m,
        None => {
            println!("No metrics found.");
            return;
        }
    };

    if metrics.is_empty() {
        println!("No metrics found.");
        return;
    }

    let mut table = Table::new();
    table.set_header(vec![
        "Timestamp",
        "Service",
        "Name",
        "Type",
        "Value",
        "Unit",
    ]);

    for m in metrics {
        let timestamp = m["timestamp"].as_str().unwrap_or("-");
        let short_ts = if timestamp.len() > 19 {
            &timestamp[..19]
        } else {
            timestamp
        };
        table.add_row(vec![
            Cell::new(short_ts),
            Cell::new(m["service"].as_str().unwrap_or("-")),
            Cell::new(m["name"].as_str().unwrap_or("-")),
            Cell::new(m["metric_type"].as_str().unwrap_or("-")),
            Cell::new(format!("{:.4}", m["value"].as_f64().unwrap_or(0.0))),
            Cell::new(m["unit"].as_str().unwrap_or("")),
        ]);
    }

    println!("{table}");
}

pub fn print_series_table(value: &Value) {
    let series = match value.get("series").and_then(|s| s.as_array()) {
        Some(s) => s,
        None => {
            println!("No series returned.");
            return;
        }
    };

    if series.is_empty() {
        println!("No series returned.");
        return;
    }

    let mut table = Table::new();
    table.set_header(vec!["Metric", "Labels", "Value", "Timestamp"]);

    for s in series {
        let labels = s
            .get("labels")
            .and_then(|l| l.as_object())
            .map(|m| {
                let mut entries: Vec<String> = m
                    .iter()
                    .map(|(k, v)| format!("{k}={}", v.as_str().unwrap_or("")))
                    .collect();
                entries.sort();
                entries.join(", ")
            })
            .unwrap_or_default();
        let timestamp = s["timestamp"].as_str().unwrap_or("-");
        let short_ts = if timestamp.len() > 19 {
            &timestamp[..19]
        } else {
            timestamp
        };
        table.add_row(vec![
            Cell::new(s["metric"].as_str().unwrap_or("-")),
            Cell::new(labels),
            Cell::new(format!("{:.4}", s["value"].as_f64().unwrap_or(0.0))),
            Cell::new(short_ts),
        ]);
    }

    println!("{table}");
}

pub fn print_summary(value: &Value) {
    let window = value["window_seconds"].as_i64().unwrap_or(0);
    let svc_filter = value["service_filter"].as_str().unwrap_or("(all)");
    println!("Summary — last {window}s, service: {svc_filter}");
    println!();

    let traces = &value["traces"];
    let mut t = Table::new();
    t.set_header(vec!["Metric", "Value"]);
    t.add_row(vec![
        Cell::new("Spans"),
        Cell::new(traces["span_count"].as_i64().unwrap_or(0).to_string()),
    ]);
    t.add_row(vec![
        Cell::new("Traces"),
        Cell::new(traces["trace_count"].as_i64().unwrap_or(0).to_string()),
    ]);
    t.add_row(vec![
        Cell::new("Errors"),
        Cell::new(traces["error_count"].as_i64().unwrap_or(0).to_string()),
    ]);
    t.add_row(vec![
        Cell::new("Error rate"),
        Cell::new(format!(
            "{:.2}%",
            traces["error_rate"].as_f64().unwrap_or(0.0) * 100.0
        )),
    ]);
    t.add_row(vec![
        Cell::new("Avg duration"),
        Cell::new(format!(
            "{:.1} ms",
            traces["avg_ms"].as_f64().unwrap_or(0.0)
        )),
    ]);
    t.add_row(vec![
        Cell::new("p50"),
        Cell::new(format!(
            "{:.1} ms",
            traces["p50_ms"].as_f64().unwrap_or(0.0)
        )),
    ]);
    t.add_row(vec![
        Cell::new("p95"),
        Cell::new(format!(
            "{:.1} ms",
            traces["p95_ms"].as_f64().unwrap_or(0.0)
        )),
    ]);
    t.add_row(vec![
        Cell::new("p99"),
        Cell::new(format!(
            "{:.1} ms",
            traces["p99_ms"].as_f64().unwrap_or(0.0)
        )),
    ]);
    t.add_row(vec![
        Cell::new("Max"),
        Cell::new(format!(
            "{:.1} ms",
            traces["max_ms"].as_f64().unwrap_or(0.0)
        )),
    ]);
    println!("Traces");
    println!("{t}");
    println!();

    if let Some(svcs) = value["top_services"].as_array() {
        if !svcs.is_empty() {
            let mut st = Table::new();
            st.set_header(vec!["Service", "Spans", "Error Rate", "p95 (ms)"]);
            for s in svcs {
                st.add_row(vec![
                    Cell::new(s["service"].as_str().unwrap_or("-")),
                    Cell::new(s["span_count"].as_i64().unwrap_or(0).to_string()),
                    Cell::new(format!(
                        "{:.2}%",
                        s["error_rate"].as_f64().unwrap_or(0.0) * 100.0
                    )),
                    Cell::new(format!("{:.1}", s["p95_ms"].as_f64().unwrap_or(0.0))),
                ]);
            }
            println!("Top services");
            println!("{st}");
            println!();
        }
    }

    if let Some(ops) = value["top_error_operations"].as_array() {
        if !ops.is_empty() {
            let mut et = Table::new();
            et.set_header(vec!["Service", "Operation", "Errors"]);
            for o in ops {
                et.add_row(vec![
                    Cell::new(o["service"].as_str().unwrap_or("-")),
                    Cell::new(o["operation"].as_str().unwrap_or("-")),
                    Cell::new(o["error_count"].as_i64().unwrap_or(0).to_string()),
                ]);
            }
            println!("Top error operations");
            println!("{et}");
            println!();
        }
    }

    let logs = &value["logs"];
    let mut lt = Table::new();
    lt.set_header(vec!["Severity", "Count"]);
    lt.add_row(vec![
        Cell::new("total"),
        Cell::new(logs["total"].as_i64().unwrap_or(0).to_string()),
    ]);
    lt.add_row(vec![
        Cell::new("error"),
        Cell::new(logs["error"].as_i64().unwrap_or(0).to_string()),
    ]);
    lt.add_row(vec![
        Cell::new("warn"),
        Cell::new(logs["warn"].as_i64().unwrap_or(0).to_string()),
    ]);
    lt.add_row(vec![
        Cell::new("info"),
        Cell::new(logs["info"].as_i64().unwrap_or(0).to_string()),
    ]);
    lt.add_row(vec![
        Cell::new("debug"),
        Cell::new(logs["debug"].as_i64().unwrap_or(0).to_string()),
    ]);
    println!("Logs");
    println!("{lt}");
    println!();

    let metrics = &value["metrics"];
    let mut mt = Table::new();
    mt.set_header(vec!["Metric", "Value"]);
    mt.add_row(vec![
        Cell::new("Points"),
        Cell::new(metrics["point_count"].as_i64().unwrap_or(0).to_string()),
    ]);
    mt.add_row(vec![
        Cell::new("Unique names"),
        Cell::new(metrics["unique_names"].as_i64().unwrap_or(0).to_string()),
    ]);
    println!("Metrics");
    println!("{mt}");
}

pub fn print_anomalies(value: &Value) {
    let cur = value["current_seconds"].as_i64().unwrap_or(0);
    let base = value["baseline_seconds"].as_i64().unwrap_or(0);
    let svc = value["service_filter"].as_str().unwrap_or("(all)");
    println!("Anomalies — current {cur}s vs baseline {base}s, service: {svc}");
    println!();

    let anomalies = match value["anomalies"].as_array() {
        Some(a) if !a.is_empty() => a,
        _ => {
            println!("No anomalies detected.");
            return;
        }
    };

    let mut t = Table::new();
    t.set_header(vec![
        "Severity",
        "Service",
        "Kind",
        "Current",
        "Baseline",
        "Description",
    ]);
    for a in anomalies {
        t.add_row(vec![
            Cell::new(a["severity"].as_str().unwrap_or("-")),
            Cell::new(a["service"].as_str().unwrap_or("-")),
            Cell::new(a["kind"].as_str().unwrap_or("-")),
            Cell::new(format!("{:.3}", a["current"].as_f64().unwrap_or(0.0))),
            Cell::new(format!("{:.3}", a["baseline"].as_f64().unwrap_or(0.0))),
            Cell::new(a["description"].as_str().unwrap_or("-")),
        ]);
    }
    println!("{t}");
}

pub fn print_correlate(value: &Value) {
    if let Some(err) = value.get("error").and_then(|e| e.as_str()) {
        println!("{err}");
        return;
    }
    let trace_id = value["trace_id"].as_str().unwrap_or("-");
    let span_count = value["span_count"].as_i64().unwrap_or(0);
    let dur = value["duration_ms"].as_f64().unwrap_or(0.0);
    let errors = value["error_count"].as_i64().unwrap_or(0);
    let services: Vec<&str> = value["services"]
        .as_array()
        .map(|a| a.iter().filter_map(|v| v.as_str()).collect())
        .unwrap_or_default();

    println!("Trace {trace_id}");
    println!("  spans: {span_count}  errors: {errors}  duration: {dur:.1} ms");
    println!("  services: {}", services.join(", "));
    println!(
        "  window: {} → {}",
        value["start_time"].as_str().unwrap_or("-"),
        value["end_time"].as_str().unwrap_or("-")
    );
    println!();

    let logs = value["logs"].as_array().cloned().unwrap_or_default();
    if logs.is_empty() {
        println!("Logs: (none)");
    } else {
        let mut lt = Table::new();
        lt.set_header(vec!["Timestamp", "Service", "Severity", "Body"]);
        for l in logs.iter().take(50) {
            let body = l["body"].as_str().unwrap_or("-");
            let short = if body.len() > 80 { &body[..80] } else { body };
            lt.add_row(vec![
                Cell::new(l["timestamp"].as_str().unwrap_or("-")),
                Cell::new(l["service"].as_str().unwrap_or("-")),
                Cell::new(l["severity"].as_str().unwrap_or("-")),
                Cell::new(short),
            ]);
        }
        println!("Logs ({}):", logs.len());
        println!("{lt}");
    }
    println!();

    let metrics = value["metrics"].as_array().cloned().unwrap_or_default();
    if metrics.is_empty() {
        println!("Metrics: (none in trace window)");
    } else {
        let mut mt = Table::new();
        mt.set_header(vec!["Timestamp", "Service", "Name", "Type", "Value"]);
        for m in metrics.iter().take(50) {
            mt.add_row(vec![
                Cell::new(m["timestamp"].as_str().unwrap_or("-")),
                Cell::new(m["service"].as_str().unwrap_or("-")),
                Cell::new(m["name"].as_str().unwrap_or("-")),
                Cell::new(m["metric_type"].as_str().unwrap_or("-")),
                Cell::new(format!("{:.3}", m["value"].as_f64().unwrap_or(0.0))),
            ]);
        }
        println!("Metrics ({}):", metrics.len());
        println!("{mt}");
    }
}

pub fn print_watch_tick(delta: &Value) {
    let ts = delta["timestamp"].as_str().unwrap_or("-");
    let tr = &delta["traces"];
    let lg = &delta["logs"];
    let mt = &delta["metrics"];
    let fmt_signed_i = |v: i64| {
        if v >= 0 {
            format!("+{v}")
        } else {
            v.to_string()
        }
    };
    let fmt_signed_f = |v: f64| {
        if v >= 0.0 {
            format!("+{v:.2}")
        } else {
            format!("{v:.2}")
        }
    };
    println!(
        "[{ts}] spans={} ({}) errors={} ({}) err_rate={:.2}% ({}) p95={:.1}ms ({}) logs.err={} ({}) metrics={} ({})",
        tr["span_count"].as_i64().unwrap_or(0),
        fmt_signed_i(tr["delta_span_count"].as_i64().unwrap_or(0)),
        tr["error_count"].as_i64().unwrap_or(0),
        fmt_signed_i(tr["delta_error_count"].as_i64().unwrap_or(0)),
        tr["error_rate"].as_f64().unwrap_or(0.0) * 100.0,
        fmt_signed_f(tr["delta_error_rate"].as_f64().unwrap_or(0.0) * 100.0),
        tr["p95_ms"].as_f64().unwrap_or(0.0),
        fmt_signed_f(tr["delta_p95_ms"].as_f64().unwrap_or(0.0)),
        lg["error"].as_i64().unwrap_or(0),
        fmt_signed_i(lg["delta_error"].as_i64().unwrap_or(0)),
        mt["point_count"].as_i64().unwrap_or(0),
        fmt_signed_i(mt["delta_point_count"].as_i64().unwrap_or(0)),
    );
}

pub fn print_eval_runs(value: &Value) {
    let runs = match value.get("runs").and_then(|v| v.as_array()) {
        Some(v) if !v.is_empty() => v,
        _ => {
            println!("No eval runs found.");
            return;
        }
    };
    let mut table = Table::new();
    table.set_header(vec![
        "Run ID", "Suite", "Status", "Cases", "Scored", "Pass", "Fail", "Updated",
    ]);
    for run in runs {
        table.add_row(vec![
            Cell::new(run["run_id"].as_str().unwrap_or("-")),
            Cell::new(run["suite_id"].as_str().unwrap_or("-")),
            Cell::new(run["status"].as_str().unwrap_or("-")),
            Cell::new(match run["case_count"].as_u64() {
                Some(total) => format!("{}/{}", run["observed_cases"].as_u64().unwrap_or(0), total),
                None => run["observed_cases"].as_u64().unwrap_or(0).to_string(),
            }),
            Cell::new(run["scored_cases"].as_u64().unwrap_or(0).to_string()),
            Cell::new(run["passed_cases"].as_u64().unwrap_or(0).to_string()),
            Cell::new(run["failed_cases"].as_u64().unwrap_or(0).to_string()),
            Cell::new(run["updated_at"].as_str().unwrap_or("-")),
        ]);
    }
    println!("{table}");
}

pub fn print_eval_status(value: &Value) {
    let run = value.get("run").unwrap_or(value);
    if run.is_null() {
        println!("Eval run not found.");
        return;
    }
    let mut table = Table::new();
    table.set_header(vec!["Metric", "Value"]);
    table.add_row(vec![
        Cell::new("run_id"),
        Cell::new(run["run_id"].as_str().unwrap_or("-")),
    ]);
    table.add_row(vec![
        Cell::new("suite"),
        Cell::new(run["suite_id"].as_str().unwrap_or("-")),
    ]);
    table.add_row(vec![
        Cell::new("status"),
        Cell::new(run["status"].as_str().unwrap_or("-")),
    ]);
    table.add_row(vec![
        Cell::new("cases"),
        Cell::new(match run["case_count"].as_u64() {
            Some(total) => format!("{}/{}", run["observed_cases"].as_u64().unwrap_or(0), total),
            None => run["observed_cases"].as_u64().unwrap_or(0).to_string(),
        }),
    ]);
    table.add_row(vec![
        Cell::new("scored"),
        Cell::new(run["scored_cases"].as_u64().unwrap_or(0).to_string()),
    ]);
    table.add_row(vec![
        Cell::new("pass"),
        Cell::new(run["passed_cases"].as_u64().unwrap_or(0).to_string()),
    ]);
    table.add_row(vec![
        Cell::new("fail"),
        Cell::new(run["failed_cases"].as_u64().unwrap_or(0).to_string()),
    ]);
    table.add_row(vec![
        Cell::new("cost_usd"),
        Cell::new(format!("{:.4}", run["cost_usd"].as_f64().unwrap_or(0.0))),
    ]);
    if let Some(scores) = run["avg_scores"].as_object() {
        for (metric, value) in scores {
            table.add_row(vec![
                Cell::new(format!("avg {metric}")),
                Cell::new(format!("{:.4}", value.as_f64().unwrap_or(0.0))),
            ]);
        }
    }
    println!("{table}");
}

pub fn print_eval_cases(value: &Value) {
    let cases = match value.get("cases").and_then(|v| v.as_array()) {
        Some(v) if !v.is_empty() => v,
        _ => {
            println!("No eval cases found.");
            return;
        }
    };
    let mut table = Table::new();
    table.set_header(vec!["Case", "Status", "Score", "Cost", "Duration", "Trace"]);
    for case in cases {
        let score = case["scores"]
            .as_object()
            .and_then(|scores| {
                scores
                    .get("correctness")
                    .or_else(|| scores.get("pass"))
                    .or_else(|| scores.iter().next().map(|(_, v)| v))
            })
            .and_then(|v| v.as_f64())
            .map(|v| format!("{v:.3}"))
            .unwrap_or_else(|| "-".to_string());
        let trace = case["trace_id"].as_str().unwrap_or("-");
        let short_trace = if trace.len() > 12 {
            format!("{}...", &trace[..12])
        } else {
            trace.to_string()
        };
        table.add_row(vec![
            Cell::new(case["case_id"].as_str().unwrap_or("-")),
            Cell::new(case["status"].as_str().unwrap_or("-")),
            Cell::new(score),
            Cell::new(format!("{:.4}", case["cost_usd"].as_f64().unwrap_or(0.0))),
            Cell::new(match case["duration_ms"].as_f64() {
                Some(ms) => format!("{ms:.0}ms"),
                None => "-".to_string(),
            }),
            Cell::new(short_trace),
        ]);
    }
    println!("{table}");
}

pub fn print_eval_scores(value: &Value) {
    let scores = match value
        .get("scores")
        .or_else(|| value.get("score").and_then(|_| Some(value)))
        .and_then(|v| v.as_array())
    {
        Some(v) if !v.is_empty() => v,
        _ => {
            if let Some(score) = value.get("score") {
                print_eval_scores(&serde_json::json!({ "scores": [score] }));
            } else {
                println!("No eval scores found.");
            }
            return;
        }
    };
    let mut table = Table::new();
    table.set_header(vec!["Case", "Metric", "Value", "Label", "Scorer", "Trace"]);
    for score in scores {
        let trace = score["trace_id"].as_str().unwrap_or("-");
        let short_trace = if trace.len() > 12 {
            format!("{}...", &trace[..12])
        } else {
            trace.to_string()
        };
        table.add_row(vec![
            Cell::new(score["case_id"].as_str().unwrap_or("-")),
            Cell::new(score["metric"].as_str().unwrap_or("-")),
            Cell::new(format!("{:.4}", score["value"].as_f64().unwrap_or(0.0))),
            Cell::new(score["label"].as_str().unwrap_or("-")),
            Cell::new(score["scorer"].as_str().unwrap_or("-")),
            Cell::new(short_trace),
        ]);
    }
    println!("{table}");
}

pub fn print_eval_report(value: &Value) {
    print_eval_status(
        &serde_json::json!({ "run": value.get("run").cloned().unwrap_or(Value::Null) }),
    );
    println!();
    print_eval_cases(
        &serde_json::json!({ "cases": value.get("cases").cloned().unwrap_or(Value::Array(Vec::new())) }),
    );
}

pub fn print_eval_compare(value: &Value) {
    let cases = match value.get("cases").and_then(|v| v.as_array()) {
        Some(v) if !v.is_empty() => v,
        _ => {
            println!("No comparable eval scores found.");
            return;
        }
    };
    println!(
        "Compare {} vs {}",
        value["current_run_id"].as_str().unwrap_or("-"),
        value["baseline_run_id"].as_str().unwrap_or("-")
    );
    let mut table = Table::new();
    table.set_header(vec!["Case", "Metric", "Current", "Baseline", "Delta"]);
    for case in cases {
        table.add_row(vec![
            Cell::new(case["case_id"].as_str().unwrap_or("-")),
            Cell::new(case["metric"].as_str().unwrap_or("-")),
            Cell::new(format_optional_f64(&case["current_value"])),
            Cell::new(format_optional_f64(&case["baseline_value"])),
            Cell::new(format_optional_f64(&case["delta"])),
        ]);
    }
    println!("{table}");
}

fn format_optional_f64(value: &Value) -> String {
    value
        .as_f64()
        .map(|v| format!("{v:.4}"))
        .unwrap_or_else(|| "-".to_string())
}

pub fn render(format: &OutputFormat, value: &Value, table_fn: fn(&Value)) {
    match format {
        OutputFormat::Json => print_json(value),
        OutputFormat::Table => table_fn(value),
    }
}
