use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
use crate::app::App;
use crate::theme;
use super::explain::{explain_check, wrap_text};
use super::render::build_file_agent_map;
use super::shared::{render_code_block, render_fix_diff, render_fix_text};
pub(super) fn render_scan_preview(frame: &mut Frame, area: Rect, app: &App) {
let t = theme::theme();
let Some(scan) = &app.last_scan else {
return;
};
let mut filtered: Vec<&crate::types::Finding> = scan
.findings
.iter()
.filter(|f| app.scan_view.findings_filter.matches(f.severity))
.collect();
let file_agent_map = build_file_agent_map(&app.passport_view.loaded_passports);
super::sort_findings_for_display(&mut filtered, &file_agent_map);
let idx = app.scan_view.selected_finding.unwrap_or(0);
let Some(finding) = filtered.get(idx).copied() else {
let block = Block::default()
.title(" Preview ")
.title_style(theme::title_style())
.borders(Borders::ALL)
.border_style(Style::default().fg(t.border));
let inner = block.inner(area);
frame.render_widget(block, area);
frame.render_widget(
Paragraph::new(Line::from(Span::styled(
" Select a finding to preview.",
Style::default().fg(t.muted),
))),
inner,
);
return;
};
let ft = finding.finding_type();
let badge_color = theme::finding_type_color(ft);
let sev_color = theme::severity_color(finding.severity);
let obl = finding.obligation_id.as_deref().unwrap_or("\u{2014}");
let block = Block::default()
.title(format!(" {} {} \u{2014} {obl} ", ft.badge(), ft.label()))
.title_style(
Style::default()
.fg(badge_color)
.add_modifier(Modifier::BOLD),
)
.borders(Borders::ALL)
.border_style(Style::default().fg(t.border));
let inner = block.inner(area);
frame.render_widget(block, area);
let w = inner.width.saturating_sub(4) as usize;
let mut lines: Vec<Line<'_>> = Vec::new();
if let Some(fl) = finding.file_line_label() {
lines.push(Line::from(vec![
Span::styled(" ", Style::default()),
Span::styled(
fl,
Style::default().fg(t.accent).add_modifier(Modifier::BOLD),
),
]));
lines.push(Line::from(Span::styled(
format!(" {}", "\u{2500}".repeat(w)),
Style::default().fg(t.border),
)));
}
if let Some(ctx) = &finding.code_context {
render_code_block(&mut lines, ctx, &t);
lines.push(Line::raw(""));
}
if let Some(diff) = &finding.fix_diff {
lines.push(Line::from(Span::styled(
" -- Suggested Fix \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}",
Style::default().fg(t.accent).add_modifier(Modifier::BOLD),
)));
render_fix_diff(&mut lines, diff, &t);
} else if let Some(fix_text) = &finding.fix {
if finding.code_context.is_some() {
lines.push(Line::from(Span::styled(
" -- Suggested Fix \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}",
Style::default().fg(t.accent).add_modifier(Modifier::BOLD),
)));
} else {
let header = match ft {
crate::types::FindingType::A => " Code Change:",
crate::types::FindingType::B => " Create New File:",
crate::types::FindingType::C => " Config Change:",
};
let header_color = match ft {
crate::types::FindingType::A => t.accent,
crate::types::FindingType::B => t.zone_green,
crate::types::FindingType::C => t.zone_yellow,
};
lines.push(Line::from(Span::styled(
header,
Style::default()
.fg(header_color)
.add_modifier(Modifier::BOLD),
)));
if ft == crate::types::FindingType::B {
let (_, _, file_hint) = explain_check(&finding.check_id);
lines.push(Line::from(vec![
Span::styled(" Path: ", Style::default().fg(t.muted)),
Span::styled(file_hint, Style::default().fg(t.accent)),
]));
}
lines.push(Line::from(vec![
Span::styled(" ", Style::default()),
Span::styled(
finding.message.clone(),
Style::default().fg(t.fg).add_modifier(Modifier::BOLD),
),
]));
let art = finding.article_reference.as_deref().unwrap_or("");
let sev_label = finding.severity.label().to_string();
if !art.is_empty() {
lines.push(Line::from(vec![
Span::styled(" ", Style::default()),
Span::styled(art.to_string(), Style::default().fg(t.muted)),
Span::styled(format!(" | {sev_label}"), Style::default().fg(sev_color)),
]));
}
lines.push(Line::from(Span::styled(
format!(" {}", "\u{2500}".repeat(w)),
Style::default().fg(t.border),
)));
}
lines.push(Line::raw(""));
render_fix_text(&mut lines, fix_text, ft, &t);
} else {
if finding.code_context.is_none() {
lines.push(Line::from(vec![
Span::styled(" ", Style::default()),
Span::styled(
finding.message.clone(),
Style::default().fg(t.fg).add_modifier(Modifier::BOLD),
),
]));
let art = finding.article_reference.as_deref().unwrap_or("");
let sev_label = finding.severity.label().to_string();
if !art.is_empty() {
lines.push(Line::from(vec![
Span::styled(" ", Style::default()),
Span::styled(art.to_string(), Style::default().fg(t.muted)),
Span::styled(format!(" | {sev_label}"), Style::default().fg(sev_color)),
]));
}
lines.push(Line::from(Span::styled(
format!(" {}", "\u{2500}".repeat(w)),
Style::default().fg(t.border),
)));
}
let (desc, action, file_hint) = explain_check(&finding.check_id);
lines.push(Line::from(Span::styled(
" What This Means:",
Style::default().fg(t.accent).add_modifier(Modifier::BOLD),
)));
for chunk in wrap_text(desc, w.saturating_sub(2)) {
lines.push(Line::from(Span::styled(
format!(" {chunk}"),
Style::default().fg(t.fg),
)));
}
lines.push(Line::raw(""));
lines.push(Line::from(Span::styled(
" What To Do:",
Style::default()
.fg(t.zone_green)
.add_modifier(Modifier::BOLD),
)));
for chunk in wrap_text(action, w.saturating_sub(2)) {
lines.push(Line::from(Span::styled(
format!(" {chunk}"),
Style::default().fg(t.fg),
)));
}
lines.push(Line::from(vec![
Span::styled(" File: ", Style::default().fg(t.muted)),
Span::styled(file_hint, Style::default().fg(t.accent)),
]));
}
if finding.fix.is_some() || finding.fix_diff.is_some() {
lines.push(Line::raw(""));
lines.push(Line::from(vec![Span::styled(
format!(" Impact: +{} points", finding.predicted_impact()),
Style::default().fg(t.zone_green),
)]));
}
lines.push(Line::from(Span::styled(
format!(" {}", "\u{2500}".repeat(w)),
Style::default().fg(t.border),
)));
let fix_label = match ft {
crate::types::FindingType::B => "Create",
_ => "Fix",
};
lines.push(Line::from(vec![
Span::styled(" [Enter] ", Style::default().fg(t.accent)),
Span::styled("Detail ", Style::default().fg(t.fg)),
Span::styled("[f] ", Style::default().fg(t.zone_green)),
Span::styled(format!("{fix_label} "), Style::default().fg(t.fg)),
Span::styled("[x] ", Style::default().fg(t.accent)),
Span::styled("Explain ", Style::default().fg(t.fg)),
Span::styled("[d] ", Style::default().fg(t.zone_yellow)),
Span::styled("Dismiss", Style::default().fg(t.fg)),
]));
let scroll = app.scan_view.preview_scroll;
let paragraph = Paragraph::new(lines)
.wrap(Wrap { trim: false })
.scroll((u16::try_from(scroll).unwrap_or(u16::MAX), 0));
frame.render_widget(paragraph, inner);
}