use super::components::{add_label_value, add_section_header};
use crate::priority::UnifiedDebtItem;
use crate::tui::results::app::ResultsApp;
use crate::tui::theme::Theme;
use ratatui::{
layout::Rect,
style::Style,
text::{Line, Span},
widgets::{Block, Borders, Paragraph, Wrap},
Frame,
};
pub fn build_page_lines(item: &UnifiedDebtItem, theme: &Theme, width: u16) -> Vec<Line<'static>> {
let mut lines = Vec::new();
add_section_header(&mut lines, "function dependencies", theme);
add_label_value(
&mut lines,
"upstream",
item.upstream_dependencies.to_string(),
theme,
width,
);
add_label_value(
&mut lines,
"downstream",
item.downstream_dependencies.to_string(),
theme,
width,
);
let blast_radius = item.upstream_dependencies + item.downstream_dependencies;
add_label_value(
&mut lines,
"blast radius",
blast_radius.to_string(),
theme,
width,
);
let is_critical = item.upstream_dependencies > 5 || item.downstream_dependencies > 10;
add_label_value(
&mut lines,
"critical",
if is_critical { "Yes" } else { "No" }.to_string(),
theme,
width,
);
build_file_coupling_section(&mut lines, item, theme, width);
lines
}
pub fn render(
frame: &mut Frame,
app: &ResultsApp,
item: &UnifiedDebtItem,
area: Rect,
theme: &Theme,
) {
let lines = build_page_lines(item, theme, area.width);
let paragraph = Paragraph::new(lines)
.block(Block::default().borders(Borders::NONE))
.wrap(Wrap { trim: false })
.scroll(app.detail_scroll_offset());
frame.render_widget(paragraph, area);
}
fn build_file_coupling_section(
lines: &mut Vec<Line<'static>>,
item: &UnifiedDebtItem,
theme: &Theme,
width: u16,
) {
let afferent_coupling = item.upstream_dependencies;
let efferent_coupling = item.downstream_dependencies;
let total_coupling = afferent_coupling + efferent_coupling;
if total_coupling == 0 {
return;
}
let instability = if total_coupling > 0 {
efferent_coupling as f64 / total_coupling as f64
} else {
0.0
};
lines.push(Line::from(""));
add_section_header(lines, "coupling profile", theme);
let classification =
derive_coupling_classification(afferent_coupling, efferent_coupling, instability);
render_classification_badge(lines, &classification, theme, width);
add_label_value(
lines,
"afferent (ca)",
afferent_coupling.to_string(),
theme,
width,
);
add_label_value(
lines,
"efferent (ce)",
efferent_coupling.to_string(),
theme,
width,
);
render_instability_bar(lines, instability, theme, width);
if total_coupling > 15 {
lines.push(Line::from(vec![
Span::styled("Warning: ", Style::default().fg(ratatui::style::Color::Red)),
Span::styled(
"High coupling may indicate architectural issues.",
Style::default().fg(theme.muted),
),
]));
} else if instability < 0.1 && afferent_coupling > 0 {
lines.push(Line::from(vec![
Span::styled("Note: ", Style::default().fg(theme.primary)),
Span::styled(
"Stable core - changes need careful review.",
Style::default().fg(theme.muted),
),
]));
} else if instability > 0.9 {
lines.push(Line::from(vec![
Span::styled("Note: ", Style::default().fg(theme.success)),
Span::styled(
"Unstable leaf - safe to refactor.",
Style::default().fg(theme.muted),
),
]));
}
render_dependency_list(
lines,
&item.upstream_callers,
"dependents (who calls this)",
theme,
width,
);
render_dependency_list(
lines,
&item.downstream_callees,
"dependencies (what this calls)",
theme,
width,
);
}
fn render_classification_badge(
lines: &mut Vec<Line<'static>>,
classification: &str,
theme: &Theme,
_width: u16,
) {
const INDENT: usize = 2;
const LABEL_WIDTH: usize = 24;
const GAP: usize = 4;
let badge_text = format!("[{}]", classification.to_uppercase());
let badge_style = theme.coupling_badge_style(classification);
let label_with_indent = format!("{}{}", " ".repeat(INDENT), "classification");
let padded_label = format!("{:width$}", label_with_indent, width = LABEL_WIDTH);
let gap = " ".repeat(GAP);
lines.push(Line::from(vec![
Span::raw(padded_label),
Span::raw(gap),
Span::styled(badge_text, badge_style),
]));
}
fn render_instability_bar(
lines: &mut Vec<Line<'static>>,
instability: f64,
theme: &Theme,
_width: u16,
) {
const INDENT: usize = 2;
const LABEL_WIDTH: usize = 24;
const GAP: usize = 4;
let label_with_indent = format!("{}{}", " ".repeat(INDENT), "instability");
let padded_label = format!("{:width$}", label_with_indent, width = LABEL_WIDTH);
let gap = " ".repeat(GAP);
let bar_width = 20;
let filled = ((instability * bar_width as f64).round() as usize).min(bar_width);
let empty = bar_width - filled;
let bar_color = theme.instability_color(instability);
let filled_bar: String = "█".repeat(filled);
let empty_bar: String = "░".repeat(empty);
lines.push(Line::from(vec![
Span::raw(padded_label),
Span::raw(gap),
Span::styled(
format!("{:.2} ", instability),
Style::default().fg(theme.primary),
),
Span::styled(filled_bar, Style::default().fg(bar_color)),
Span::styled(empty_bar, Style::default().fg(theme.muted)),
]));
}
fn render_dependency_list(
lines: &mut Vec<Line<'static>>,
items: &[String],
title: &str,
theme: &Theme,
_width: u16,
) {
if items.is_empty() {
return;
}
lines.push(Line::from(""));
add_section_header(lines, title, theme);
let max_display = 5;
for item in items.iter().take(max_display) {
let display_name = shorten_path(item);
lines.push(Line::from(vec![Span::styled(
format!(" {} {}", "\u{2022}", display_name), Style::default().fg(theme.text),
)]));
}
if items.len() > max_display {
lines.push(Line::from(vec![Span::styled(
format!(" (+{} more)", items.len() - max_display),
Style::default().fg(theme.muted),
)]));
}
}
fn shorten_path(path: &str) -> &str {
path.rsplit('/').next().unwrap_or(path)
}
fn derive_coupling_classification(afferent: usize, efferent: usize, instability: f64) -> String {
let total = afferent + efferent;
if total > 15 {
"Highly Coupled".to_string()
} else if total <= 2 {
"Isolated".to_string()
} else if instability < 0.3 && afferent > efferent {
"Stable Core".to_string()
} else if instability > 0.7 && efferent > afferent {
"Leaf Module".to_string()
} else {
"Utility Module".to_string()
}
}