use crate::core::types::{ForjarConfig, ResourceType};
pub(crate) fn print_graph_svg(config: &ForjarConfig) {
let nodes: Vec<(&str, &ResourceType)> = config
.resources
.iter()
.map(|(id, r)| (id.as_str(), &r.resource_type))
.collect();
let edges: Vec<(&str, &str)> = config
.resources
.iter()
.flat_map(|(id, r)| {
r.depends_on
.iter()
.filter(|dep| config.resources.contains_key(dep.as_str()))
.map(move |dep| (dep.as_str(), id.as_str()))
})
.collect();
let n = nodes.len();
let col_width = 200;
let row_height = 60;
let node_w = 160;
let node_h = 36;
let cols = 3;
let rows = n.div_ceil(cols);
let svg_w = cols * col_width + 40;
let svg_h = rows * row_height + 60;
let positions: Vec<(usize, usize)> = (0..n)
.map(|i| {
let col = i % cols;
let row = i / cols;
(20 + col * col_width, 30 + row * row_height)
})
.collect();
let node_idx: std::collections::HashMap<&str, usize> = nodes
.iter()
.enumerate()
.map(|(i, (id, _))| (*id, i))
.collect();
println!("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{svg_w}\" height=\"{svg_h}\" viewBox=\"0 0 {svg_w} {svg_h}\">");
println!(" <style>");
println!(" .node {{ fill: #f5f5f5; stroke: #333; stroke-width: 1.5; rx: 6; }}");
println!(" .label {{ font-family: monospace; font-size: 11px; fill: #333; }}");
println!(" .type-label {{ font-family: monospace; font-size: 9px; fill: #666; }}");
println!(" .edge {{ stroke: #999; stroke-width: 1; fill: none; marker-end: url(#arrow); }}");
println!(" </style>");
println!(" <defs>");
println!(" <marker id=\"arrow\" viewBox=\"0 0 10 10\" refX=\"10\" refY=\"5\" markerWidth=\"6\" markerHeight=\"6\" orient=\"auto\">");
println!(" <path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"#999\"/>");
println!(" </marker>");
println!(" </defs>");
for (from, to) in &edges {
if let (Some(&fi), Some(&ti)) = (node_idx.get(from), node_idx.get(to)) {
let (fx, fy) = positions[fi];
let (tx, ty) = positions[ti];
let x1 = fx + node_w / 2;
let y1 = fy + node_h;
let x2 = tx + node_w / 2;
let y2 = ty;
println!(" <line class=\"edge\" x1=\"{x1}\" y1=\"{y1}\" x2=\"{x2}\" y2=\"{y2}\"/>");
}
}
for (i, (id, rtype)) in nodes.iter().enumerate() {
let (x, y) = positions[i];
let fill = type_color(rtype);
println!(" <rect x=\"{x}\" y=\"{y}\" width=\"{node_w}\" height=\"{node_h}\" class=\"node\" fill=\"{fill}\"/>");
let tx = x + 8;
let ty = y + 15;
let label = truncate(id, 20);
println!(" <text x=\"{tx}\" y=\"{ty}\" class=\"label\">{label}</text>");
let tty = y + 28;
let type_str = format!("{rtype:?}").to_lowercase();
println!(" <text x=\"{tx}\" y=\"{tty}\" class=\"type-label\">{type_str}</text>");
}
println!("</svg>");
}
fn type_color(rtype: &ResourceType) -> &'static str {
match rtype {
ResourceType::Package => "#e3f2fd",
ResourceType::File => "#f3e5f5",
ResourceType::Service => "#e8f5e9",
ResourceType::Docker => "#fff3e0",
ResourceType::Mount => "#fce4ec",
ResourceType::User => "#e0f7fa",
ResourceType::Cron => "#fff9c4",
ResourceType::Network => "#f1f8e9",
ResourceType::Model => "#ede7f6",
ResourceType::Gpu => "#ffccbc",
ResourceType::Task => "#e8eaf6",
_ => "#f5f5f5",
}
}
fn truncate(s: &str, max: usize) -> String {
if s.len() > max {
if max < 4 {
return s.chars().take(max).collect();
}
let mut end = max.saturating_sub(3);
while end > 0 && !s.is_char_boundary(end) {
end -= 1;
}
format!("{}...", &s[..end])
} else {
s.to_string()
}
}