1use bevy::prelude::*;
2use bevy_ratatui::{error::exit_on_error, terminal::RatatuiContext};
3use crossterm::event::{KeyCode, KeyEventKind};
4use ratatui::{
5 buffer::Buffer,
6 layout::{Alignment, Constraint, Direction, Layout, Rect},
7 style::Style,
8 text::{Line, Span},
9 widgets::{Block, Borders, List, ListItem, Paragraph, StatefulWidgetRef, WidgetRef},
10};
11
12use crate::{
13 bevy_states::app::AppState,
14 events::{app::AppEvent, reports::ReportsEvent},
15 theme::THEME,
16 widget_states::{
17 analysis::AnalysisWidgetState,
18 reports::{ExportStatus, ReportFormat, ReportsWidgetState, ViewMode},
19 },
20};
21
22pub struct ReportsPlugin;
23
24impl Plugin for ReportsPlugin {
25 fn build(&self, app: &mut App) {
26 app.add_event::<ReportsEvent>()
27 .init_resource::<ReportsWidgetState>()
28 .add_systems(PreUpdate, reports_event_handler)
29 .add_systems(Update, sync_analysis_data)
30 .add_systems(Update, render_reports.pipe(exit_on_error));
31 }
32}
33
34fn sync_analysis_data(
35 analysis_state: Res<AnalysisWidgetState>,
36 mut reports_state: ResMut<ReportsWidgetState>,
37) {
38 if let Some(review) = &analysis_state.review {
40 if reports_state.review.is_none() {
41 reports_state.set_review(review.clone());
42 }
43 }
44}
45
46fn reports_event_handler(
47 mut reports_events: EventReader<ReportsEvent>,
48 mut reports_state: ResMut<ReportsWidgetState>,
49 mut app_events: EventWriter<AppEvent>,
50) {
51 for event in reports_events.read() {
52 match event {
53 ReportsEvent::KeyEvent(key_event) => {
54 match key_event.code {
55 KeyCode::Esc => {
56 match reports_state.view_mode {
58 ViewMode::Report => {
59 reports_state.back_to_selection();
61 }
62 ViewMode::Selection => {
63 app_events.send(AppEvent::SwitchTo(AppState::Overview));
65 }
66 }
67 }
68 _ => {
69 if key_event.kind == KeyEventKind::Release {
71 match key_event.code {
72 KeyCode::Left => {
73 reports_state.previous_format();
74 }
75 KeyCode::Right => {
76 reports_state.next_format();
77 }
78 KeyCode::Tab => {
79 reports_state.next_format();
80 }
81 KeyCode::Enter => {
82 match reports_state.view_mode {
83 ViewMode::Selection => {
84 reports_state.generate_report();
86 }
87 ViewMode::Report => {
88 export_report(&mut reports_state);
90 }
91 }
92 }
93 KeyCode::Char('a') => {
94 app_events.send(AppEvent::SwitchTo(AppState::Analysis));
95 }
96 _ => {}
97 }
98 }
99 }
100 }
101 }
102 ReportsEvent::MouseEvent(_mouse_event) => {
103 }
105 }
106 }
107}
108
109fn export_report(reports_state: &mut ReportsWidgetState) {
110 if let Some(_review) = &reports_state.review {
111 let format = match reports_state.selected_format {
112 ReportFormat::Summary => "summary".to_string(),
113 ReportFormat::Detailed => "detailed".to_string(),
114 ReportFormat::Json => "json".to_string(),
115 ReportFormat::Markdown => "markdown".to_string(),
116 };
117
118 reports_state.start_export(format.clone());
119
120 let filename = format!(
122 "code_review_report.{}",
123 match reports_state.selected_format {
124 ReportFormat::Json => "json",
125 ReportFormat::Markdown => "md",
126 _ => "txt",
127 }
128 );
129
130 reports_state.complete_export(filename);
131 }
132}
133
134fn render_reports(
135 app_state: Res<State<AppState>>,
136 mut ratatui_context: ResMut<RatatuiContext>,
137 mut reports_state: ResMut<ReportsWidgetState>,
138) -> color_eyre::Result<()> {
139 if app_state.get() != &AppState::Reports {
140 return Ok(());
141 }
142
143 ratatui_context.draw(|frame| {
144 let area = frame.area();
145 frame.render_stateful_widget_ref(ReportsWidget, area, &mut reports_state);
146 })?;
147
148 Ok(())
149}
150
151struct ReportsWidget;
152
153impl StatefulWidgetRef for ReportsWidget {
154 type State = ReportsWidgetState;
155
156 fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
157 let chunks = Layout::default()
158 .direction(Direction::Vertical)
159 .constraints([
160 Constraint::Length(3), Constraint::Min(10), Constraint::Length(3), ])
164 .split(area);
165
166 let title_text = match state.view_mode {
168 ViewMode::Selection => "📊 Reports & Export",
169 ViewMode::Report => "📄 Generated Report",
170 };
171
172 let title = Paragraph::new(title_text)
173 .style(THEME.title_style())
174 .alignment(Alignment::Center)
175 .block(
176 Block::default()
177 .borders(Borders::ALL)
178 .border_style(THEME.header_style()),
179 );
180 title.render_ref(chunks[0], buf);
181
182 match state.view_mode {
184 ViewMode::Selection => {
185 if state.review.is_some() {
186 self.render_report_content(chunks[1], buf, state);
187 } else {
188 self.render_no_data(chunks[1], buf);
189 }
190 }
191 ViewMode::Report => {
192 self.render_generated_report(chunks[1], buf, state);
193 }
194 }
195
196 self.render_status_bar(chunks[2], buf, state);
198 }
199}
200
201impl ReportsWidget {
202 fn render_no_data(&self, area: Rect, buf: &mut Buffer) {
203 let content = Paragraph::new(vec![
204 Line::from(""),
205 Line::from("No analysis data available"),
206 Line::from(""),
207 Line::from("Please run an analysis first before generating reports."),
208 Line::from(""),
209 Line::from("Press 'A' to go to the Analysis screen."),
210 ])
211 .alignment(Alignment::Center)
212 .block(
213 Block::default()
214 .borders(Borders::ALL)
215 .title("No Data")
216 .title_style(THEME.warning_style()),
217 );
218
219 content.render_ref(area, buf);
220 }
221
222 fn render_report_content(&self, area: Rect, buf: &mut Buffer, state: &ReportsWidgetState) {
223 let chunks = Layout::default()
224 .direction(Direction::Horizontal)
225 .constraints([
226 Constraint::Percentage(40), Constraint::Percentage(60), ])
229 .split(area);
230
231 self.render_format_selection(chunks[0], buf, state);
233
234 self.render_export_area(chunks[1], buf, state);
236 }
237
238 fn render_format_selection(&self, area: Rect, buf: &mut Buffer, state: &ReportsWidgetState) {
239 let formats = [
240 (
241 "Summary",
242 ReportFormat::Summary,
243 "Quick overview with key findings",
244 ),
245 (
246 "Detailed",
247 ReportFormat::Detailed,
248 "Complete issue breakdown",
249 ),
250 ("JSON", ReportFormat::Json, "Machine-readable format"),
251 (
252 "Markdown",
253 ReportFormat::Markdown,
254 "Documentation-friendly format",
255 ),
256 ];
257
258 let items: Vec<ListItem> = formats
259 .iter()
260 .map(|(name, format, description)| {
261 let is_selected = *format == state.selected_format;
262 let style = if is_selected {
263 THEME.selected_style()
264 } else {
265 Style::default()
266 };
267
268 ListItem::new(vec![
269 Line::from(vec![Span::styled(
270 *name,
271 if is_selected {
272 THEME.selected_style()
273 } else {
274 THEME.text_primary.into()
275 },
276 )]),
277 Line::from(vec![Span::styled(*description, THEME.info_style())]),
278 ])
279 .style(style)
280 })
281 .collect();
282
283 let format_list = List::new(items).block(
284 Block::default()
285 .borders(Borders::ALL)
286 .title("Export Format")
287 .title_style(THEME.header_style()),
288 );
289
290 WidgetRef::render_ref(&format_list, area, buf);
291 }
292
293 fn render_export_area(&self, area: Rect, buf: &mut Buffer, state: &ReportsWidgetState) {
294 if let Some(review) = &state.review {
295 let chunks = Layout::default()
296 .direction(Direction::Vertical)
297 .constraints([
298 Constraint::Length(8), Constraint::Length(5), Constraint::Min(3), ])
302 .split(area);
303
304 self.render_preview(chunks[0], buf, state, review);
306
307 self.render_export_button(chunks[1], buf);
309
310 self.render_export_status(chunks[2], buf, state);
312 }
313 }
314
315 fn render_preview(
316 &self,
317 area: Rect,
318 buf: &mut Buffer,
319 state: &ReportsWidgetState,
320 review: &crate::core::review::Review,
321 ) {
322 let preview_content = match state.selected_format {
323 ReportFormat::Summary => {
324 vec![
325 Line::from("# Code Review Summary"),
326 Line::from(""),
327 Line::from(format!("Files analyzed: {}", review.files_count)),
328 Line::from(format!("Total issues: {}", review.issues_count)),
329 Line::from(format!("Critical: {}", review.critical_issues)),
330 Line::from(format!("High: {}", review.high_issues)),
331 ]
332 }
333 ReportFormat::Detailed => {
334 vec![
335 Line::from("# Detailed Code Review Report"),
336 Line::from(""),
337 Line::from("## Issues Found:"),
338 Line::from(format!("- {} Critical issues", review.critical_issues)),
339 Line::from(format!("- {} High priority issues", review.high_issues)),
340 Line::from("(Full details in exported file)"),
341 ]
342 }
343 ReportFormat::Json => {
344 vec![
345 Line::from("{"),
346 Line::from(
347 " \"files_count\": {},".replace("{}", &review.files_count.to_string()),
348 ),
349 Line::from(
350 " \"issues_count\": {},".replace("{}", &review.issues_count.to_string()),
351 ),
352 Line::from(
353 " \"critical_issues\": {},"
354 .replace("{}", &review.critical_issues.to_string()),
355 ),
356 Line::from(" \"issues\": [...]"),
357 Line::from("}"),
358 ]
359 }
360 ReportFormat::Markdown => {
361 vec![
362 Line::from("# Code Review Report"),
363 Line::from(""),
364 Line::from("## Summary"),
365 Line::from(format!("- **Files analyzed**: {}", review.files_count)),
366 Line::from(format!("- **Total issues**: {}", review.issues_count)),
367 Line::from(""),
368 Line::from("## Issues"),
369 ]
370 }
371 };
372
373 let preview = Paragraph::new(preview_content)
374 .block(
375 Block::default()
376 .borders(Borders::ALL)
377 .title("Preview")
378 .title_style(THEME.header_style()),
379 )
380 .wrap(ratatui::widgets::Wrap { trim: true });
381
382 preview.render_ref(area, buf);
383 }
384
385 fn render_export_button(&self, area: Rect, buf: &mut Buffer) {
386 let button = Paragraph::new("� Generate Report (Press Enter)")
387 .style(THEME.button_style(false))
388 .alignment(Alignment::Center)
389 .block(
390 Block::default()
391 .borders(Borders::ALL)
392 .border_style(Style::default().fg(THEME.primary)),
393 );
394
395 button.render_ref(area, buf);
396 }
397
398 fn render_export_status(&self, area: Rect, buf: &mut Buffer, state: &ReportsWidgetState) {
399 let (status_text, status_style) = match &state.export_status {
400 ExportStatus::None => ("Ready to export".to_string(), THEME.info_style()),
401 ExportStatus::Exporting(format) => (
402 format!("Exporting {format} report..."),
403 THEME.warning_style(),
404 ),
405 ExportStatus::Success(path) => (
406 format!("✅ Exported successfully to: {path}"),
407 THEME.success_style(),
408 ),
409 };
410
411 let status = Paragraph::new(status_text)
412 .style(status_style)
413 .alignment(Alignment::Center)
414 .block(
415 Block::default()
416 .borders(Borders::ALL)
417 .title("Status")
418 .title_style(THEME.header_style()),
419 );
420
421 status.render_ref(area, buf);
422 }
423
424 fn render_status_bar(&self, area: Rect, buf: &mut Buffer, state: &ReportsWidgetState) {
425 let status_text = match state.view_mode {
426 ViewMode::Selection => {
427 if state.review.is_some() {
428 "Use ←→ or Tab to change format, Enter to generate report, A for analysis, Esc to go back"
429 } else {
430 "A to run analysis, Esc to go back"
431 }
432 }
433 ViewMode::Report => "Enter to export report, Esc to go back to selection",
434 };
435
436 let status = Paragraph::new(status_text)
437 .style(THEME.info_style())
438 .alignment(Alignment::Center)
439 .block(
440 Block::default()
441 .borders(Borders::TOP)
442 .border_style(THEME.info_style()),
443 );
444
445 status.render_ref(area, buf);
446 }
447
448 fn render_generated_report(&self, area: Rect, buf: &mut Buffer, state: &ReportsWidgetState) {
449 if let Some(report_content) = &state.generated_report {
450 let lines: Vec<Line> = report_content
452 .lines()
453 .map(|line| Line::from(line.to_string()))
454 .collect();
455
456 let report = Paragraph::new(lines)
457 .block(
458 Block::default()
459 .borders(Borders::ALL)
460 .title(format!(
461 " {} Report ",
462 match state.selected_format {
463 ReportFormat::Summary => "Summary",
464 ReportFormat::Detailed => "Detailed",
465 ReportFormat::Json => "JSON",
466 ReportFormat::Markdown => "Markdown",
467 }
468 ))
469 .title_style(THEME.header_style()),
470 )
471 .wrap(ratatui::widgets::Wrap { trim: false })
472 .scroll((0, 0)); report.render_ref(area, buf);
475 } else {
476 let error = Paragraph::new("No report generated")
477 .alignment(Alignment::Center)
478 .block(
479 Block::default()
480 .borders(Borders::ALL)
481 .title("Error")
482 .title_style(THEME.error_style()),
483 );
484 error.render_ref(area, buf);
485 }
486 }
487}