use std::io;

use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
use ratatui::backend::Backend;
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{
    Block, BorderType, Borders, Cell, Paragraph, Row, Scrollbar, ScrollbarOrientation,
    ScrollbarState, Table, TableState,
};
use ratatui::Terminal;

use crate::models::Entry;
use crate::repository::Repository;
use crate::risk::{self, RiskLevel, SessionRisk};
use crate::theme::theme;
use crate::util::{dirs_home, shorten_path};

use super::{
    compute_agent_counts, format_datetime, format_full_datetime, load_entries, truncate, Period,
};

const PAGE_SIZE: usize = 50;

enum DashboardAction {
    Continue,
    Quit,
    OpenPrompts,
}

struct AgentApp {
    entries: Vec<Entry>,
    /// Filtered indices into `entries`, recent first
    visible: Vec<usize>,
    risk_summary: SessionRisk,
    agent_counts: Vec<(String, usize)>,
    agent_names: Vec<String>,

    /// Precomputed count of high-risk entries in `visible` (for header display).
    visible_high_risk_count: usize,

    // Filters
    period: Period,
    agent_filter: Option<usize>,
    risk_filter: bool,
    cli_executor: Option<String>,
    cwd_filter: Option<String>,

    // Pagination
    page: usize, // 1-based
    page_size: usize,

    // UI state
    table_state: TableState,
    detail_open: bool,

    home: String,
    status_message: Option<(String, std::time::Instant)>,
}

impl AgentApp {
    fn new(
        repo: &Repository,
        initial_after_ms: Option<i64>,
        executor: Option<&str>,
        cwd: Option<&str>,
    ) -> Self {
        let home = dirs_home();
        let period = Period::from_after_ms(initial_after_ms);

        let entries = load_entries(repo, initial_after_ms, executor, cwd);
        let risk_summary = risk::session_risk(&entries);
        let agent_counts = compute_agent_counts(&entries);
        let agent_names: Vec<String> = agent_counts.iter().map(|(n, _)| n.clone()).collect();
        // Recent first
        let visible: Vec<usize> = (0..entries.len()).rev().collect();
        let visible_high_risk_count = visible
            .iter()
            .filter(|&&i| risk::risk_level(&entries[i].command) >= RiskLevel::High)
            .count();

        let mut app = Self {
            entries,
            visible,
            risk_summary,
            agent_counts,
            agent_names,
            visible_high_risk_count,
            period,
            agent_filter: None,
            risk_filter: false,
            cli_executor: executor.map(String::from),
            cwd_filter: cwd.map(String::from),
            page: 1,
            page_size: PAGE_SIZE,
            table_state: TableState::default(),
            detail_open: true,
            home,
            status_message: None,
        };
        if !app.visible.is_empty() {
            app.table_state.select(Some(0));
        }
        app
    }

    fn reload(&mut self, repo: &Repository) {
        let after_ms = self.period.after_ms();
        self.entries = load_entries(
            repo,
            after_ms,
            self.cli_executor.as_deref(),
            self.cwd_filter.as_deref(),
        );
        self.risk_summary = risk::session_risk(&self.entries);
        self.agent_counts = compute_agent_counts(&self.entries);
        self.agent_names = self.agent_counts.iter().map(|(n, _)| n.clone()).collect();
        if let Some(idx) = self.agent_filter {
            if idx >= self.agent_names.len() {
                self.agent_filter = None;
            }
        }
        self.rebuild_visible();
    }

    fn rebuild_visible(&mut self) {
        let agent_name = self
            .agent_filter
            .and_then(|i| self.agent_names.get(i).cloned());

        let mut high_risk_count = 0usize;

        // Recent first — compute risk on-demand during filter pass
        self.visible = (0..self.entries.len())
            .rev()
            .filter(|&i| {
                if let Some(ref name) = agent_name {
                    let entry_agent = self.entries[i].executor.as_deref().unwrap_or("unknown");
                    if entry_agent != name {
                        return false;
                    }
                }
                if self.risk_filter {
                    let rl = risk::risk_level(&self.entries[i].command);
                    if rl < RiskLevel::Medium {
                        return false;
                    }
                    if rl >= RiskLevel::High {
                        high_risk_count += 1;
                    }
                    return true;
                }
                if risk::risk_level(&self.entries[i].command) >= RiskLevel::High {
                    high_risk_count += 1;
                }
                true
            })
            .collect();

        self.visible_high_risk_count = high_risk_count;
        self.page = 1;
        if self.visible.is_empty() {
            self.table_state.select(None);
        } else {
            self.table_state.select(Some(0));
        }
    }

    fn total_pages(&self) -> usize {
        self.visible.len().div_ceil(self.page_size).max(1)
    }

    /// Indices into `visible` for the current page.
    fn page_slice(&self) -> &[usize] {
        let start = (self.page - 1) * self.page_size;
        let end = (start + self.page_size).min(self.visible.len());
        if start >= self.visible.len() {
            &[]
        } else {
            &self.visible[start..end]
        }
    }

    fn selected_entry(&self) -> Option<&Entry> {
        let page_offset = (self.page - 1) * self.page_size;
        self.table_state
            .selected()
            .and_then(|i| self.visible.get(page_offset + i))
            .map(|&idx| &self.entries[idx])
    }

    fn selected_risk(&self) -> RiskLevel {
        self.selected_entry()
            .map_or(RiskLevel::None, |e| risk::risk_level(&e.command))
    }

    // ── Input ────────────────────────────────────────────────

    fn handle_input(
        &mut self,
        key: crossterm::event::KeyEvent,
        repo: &Repository,
    ) -> DashboardAction {
        match key.code {
            KeyCode::Esc | KeyCode::Char('q') => return DashboardAction::Quit,
            KeyCode::Char('p') => return DashboardAction::OpenPrompts,
            // Period
            KeyCode::Char('1') => {
                self.period = Period::Today;
                self.reload(repo);
            }
            KeyCode::Char('2') => {
                self.period = Period::Days7;
                self.reload(repo);
            }
            KeyCode::Char('3') => {
                self.period = Period::Days30;
                self.reload(repo);
            }
            KeyCode::Char('4') => {
                self.period = Period::AllTime;
                self.reload(repo);
            }
            // Agent filter
            KeyCode::Char('a') => {
                if self.agent_names.is_empty() {
                    self.agent_filter = None;
                } else {
                    self.agent_filter = match self.agent_filter {
                        None => Some(0),
                        Some(i) if i + 1 >= self.agent_names.len() => None,
                        Some(i) => Some(i + 1),
                    };
                }
                self.rebuild_visible();
            }
            // Risk filter
            KeyCode::Char('r') => {
                self.risk_filter = !self.risk_filter;
                self.rebuild_visible();
            }
            // Detail pane
            KeyCode::Tab => {
                self.detail_open = !self.detail_open;
            }
            // Copy
            KeyCode::Char('y') if key.modifiers.contains(KeyModifiers::CONTROL) => {
                if let Some(entry) = self.selected_entry() {
                    match arboard::Clipboard::new()
                        .and_then(|mut c| c.set_text(entry.command.clone()))
                    {
                        Ok(()) => {
                            self.status_message =
                                Some(("Copied!".into(), std::time::Instant::now()));
                        }
                        Err(_) => {
                            self.status_message =
                                Some(("Copy failed".into(), std::time::Instant::now()));
                        }
                    }
                }
            }
            // Page navigation
            KeyCode::Left if self.page > 1 => {
                self.page -= 1;
                self.table_state.select(Some(0));
            }
            KeyCode::Right if self.page < self.total_pages() => {
                self.page += 1;
                self.table_state.select(Some(0));
            }
            // Row navigation
            KeyCode::Up | KeyCode::Char('k') => {
                if let Some(cur) = self.table_state.selected() {
                    self.table_state.select(Some(cur.saturating_sub(1)));
                }
            }
            KeyCode::Down | KeyCode::Char('j') => {
                let max = self.page_slice().len().saturating_sub(1);
                if let Some(cur) = self.table_state.selected() {
                    self.table_state
                        .select(Some(cur.saturating_add(1).min(max)));
                }
            }
            KeyCode::Home if !self.page_slice().is_empty() => {
                self.table_state.select(Some(0));
            }
            KeyCode::End if !self.page_slice().is_empty() => {
                self.table_state
                    .select(Some(self.page_slice().len().saturating_sub(1)));
            }
            _ => {}
        }
        DashboardAction::Continue
    }

    // ── Render ───────────────────────────────────────────────

    fn render(&mut self, f: &mut ratatui::Frame) {
        let t = theme();
        let size = f.area();

        let chunks = Layout::default()
            .direction(Direction::Vertical)
            .constraints([
                Constraint::Length(1), // header
                Constraint::Min(8),    // body
                Constraint::Length(1), // footer
            ])
            .split(size);

        self.render_header(f, chunks[0], t);

        // Body: summary | table | detail (optional)
        if self.detail_open {
            let body = Layout::default()
                .direction(Direction::Horizontal)
                .constraints([
                    Constraint::Length(24),     // summary
                    Constraint::Percentage(70), // table
                    Constraint::Percentage(30), // detail
                ])
                .split(chunks[1]);
            self.render_summary(f, body[0], t);
            self.render_table(f, body[1], t);
            self.render_detail(f, body[2], t);
        } else {
            let body = Layout::default()
                .direction(Direction::Horizontal)
                .constraints([Constraint::Length(24), Constraint::Min(30)])
                .split(chunks[1]);
            self.render_summary(f, body[0], t);
            self.render_table(f, body[1], t);
        }

        self.render_footer(f, chunks[2], t);
    }

    fn render_header(&self, f: &mut ratatui::Frame, area: Rect, t: &crate::theme::Theme) {
        let total = self.visible.len();
        let risk_count = self.visible_high_risk_count;

        let agent_label = self
            .agent_filter
            .and_then(|i| self.agent_names.get(i))
            .map_or_else(|| "All agents".to_string(), Clone::clone);

        let mut spans = vec![
            Span::styled(
                " SUVADU AGENT MONITOR ",
                Style::default().fg(t.primary).add_modifier(Modifier::BOLD),
            ),
            Span::styled("  ", Style::default()),
        ];

        for (i, p) in [
            Period::Today,
            Period::Days7,
            Period::Days30,
            Period::AllTime,
        ]
        .iter()
        .enumerate()
        {
            let is_active = *p == self.period;
            spans.push(Span::styled(
                format!("{}", i + 1),
                Style::default().fg(t.text_muted),
            ));
            if is_active {
                spans.push(Span::styled(
                    format!(" {} ", p.label()),
                    Style::default()
                        .bg(t.primary)
                        .fg(Color::Black)
                        .add_modifier(Modifier::BOLD),
                ));
            } else {
                spans.push(Span::styled(
                    format!(" {} ", p.label()),
                    Style::default().fg(t.text_muted),
                ));
            }
            spans.push(Span::raw(" "));
        }

        spans.push(Span::styled("  ", Style::default()));
        spans.push(Span::styled(
            format!("{agent_label} · {total} cmds"),
            Style::default().fg(t.text_secondary),
        ));
        if risk_count > 0 {
            spans.push(Span::styled(
                format!(" · ⚠ {risk_count}"),
                Style::default().fg(t.warning),
            ));
        }

        f.render_widget(Paragraph::new(Line::from(spans)), area);
    }

    fn render_summary(&self, f: &mut ratatui::Frame, area: Rect, t: &crate::theme::Theme) {
        let block = Block::default()
            .borders(Borders::RIGHT)
            .border_style(Style::default().fg(t.border));

        let inner = block.inner(area);
        f.render_widget(block, area);

        let label_style = Style::default()
            .fg(t.text_secondary)
            .add_modifier(Modifier::BOLD);
        let value_style = Style::default().fg(t.text);

        let mut lines = Vec::new();
        self.build_summary_agents(&mut lines, label_style, value_style, t);
        lines.push(Line::from(""));
        self.build_summary_risk(&mut lines, label_style, t);
        lines.push(Line::from(""));
        self.build_summary_stats(&mut lines, label_style, value_style, t);

        if self.risk_filter {
            lines.push(Line::from(""));
            lines.push(Line::from(Span::styled(
                " [risk-only]",
                Style::default().fg(t.warning).add_modifier(Modifier::BOLD),
            )));
        }

        f.render_widget(Paragraph::new(lines), inner);
    }

    fn build_summary_agents(
        &self,
        lines: &mut Vec<Line>,
        label_style: Style,
        value_style: Style,
        t: &crate::theme::Theme,
    ) {
        lines.push(Line::from(Span::styled(" Agents", label_style)));
        for (name, count) in &self.agent_counts {
            let is_filtered = self
                .agent_filter
                .and_then(|i| self.agent_names.get(i))
                .is_some_and(|n| n == name);
            let dot = if is_filtered { "●" } else { " " };
            let dot_style = if is_filtered {
                Style::default().fg(t.primary)
            } else {
                Style::default().fg(t.text_muted)
            };
            lines.push(Line::from(vec![
                Span::styled(format!(" {dot} "), dot_style),
                Span::styled(
                    truncate(name, 12),
                    if is_filtered {
                        Style::default().fg(t.primary).add_modifier(Modifier::BOLD)
                    } else {
                        value_style
                    },
                ),
                Span::styled(format!("  {count}"), Style::default().fg(t.text_muted)),
            ]));
        }
    }

    fn build_summary_risk(
        &self,
        lines: &mut Vec<Line>,
        label_style: Style,
        t: &crate::theme::Theme,
    ) {
        lines.push(Line::from(Span::styled(" Risk", label_style)));
        if self.risk_summary.critical_count > 0 {
            lines.push(Line::from(vec![
                Span::styled("  ", Style::default()),
                Span::styled(
                    format!("⚠ {} critical", self.risk_summary.critical_count),
                    Style::default().fg(t.risk_critical),
                ),
            ]));
        }
        if self.risk_summary.high_count > 0 {
            lines.push(Line::from(vec![
                Span::styled("  ", Style::default()),
                Span::styled(
                    format!("⚠ {} high", self.risk_summary.high_count),
                    Style::default().fg(t.risk_high),
                ),
            ]));
        }
        if self.risk_summary.medium_count > 0 {
            lines.push(Line::from(vec![
                Span::styled("  ", Style::default()),
                Span::styled(
                    format!("⚡ {} medium", self.risk_summary.medium_count),
                    Style::default().fg(t.risk_medium),
                ),
            ]));
        }
        let safe = self.risk_summary.safe_count + self.risk_summary.low_count;
        lines.push(Line::from(vec![
            Span::styled("  ", Style::default()),
            Span::styled(format!("✔ {safe} safe"), Style::default().fg(t.success)),
        ]));
    }

    fn build_summary_stats(
        &self,
        lines: &mut Vec<Line>,
        label_style: Style,
        value_style: Style,
        t: &crate::theme::Theme,
    ) {
        lines.push(Line::from(Span::styled(" Stats", label_style)));
        let total = self.entries.len();
        let success = self
            .entries
            .iter()
            .filter(|e| e.exit_code == Some(0))
            .count();
        #[allow(clippy::cast_precision_loss)]
        let rate = if total > 0 {
            success as f64 / total as f64 * 100.0
        } else {
            0.0
        };
        lines.push(Line::from(vec![
            Span::styled("  Success: ", Style::default().fg(t.text_muted)),
            Span::styled(format!("{rate:.1}%"), value_style),
        ]));
        if !self.risk_summary.packages_installed.is_empty() {
            let pkg_count: usize = self
                .risk_summary
                .packages_installed
                .iter()
                .map(|p| p.packages.len())
                .sum();
            lines.push(Line::from(vec![
                Span::styled("  Packages: ", Style::default().fg(t.text_muted)),
                Span::styled(format!("{pkg_count}"), value_style),
            ]));
        }
        let failures = self.risk_summary.failed_commands.len();
        if failures > 0 {
            lines.push(Line::from(vec![
                Span::styled("  Failures: ", Style::default().fg(t.text_muted)),
                Span::styled(format!("{failures}"), Style::default().fg(t.error)),
            ]));
        }
    }

    fn render_table(&mut self, f: &mut ratatui::Frame, area: Rect, t: &crate::theme::Theme) {
        let scrollbar_area = Rect {
            x: area.x + area.width.saturating_sub(1),
            width: 1,
            ..area
        };
        let table_area = Rect {
            width: area.width.saturating_sub(1),
            ..area
        };

        let page_items: Vec<usize> = self.page_slice().to_vec();
        let rows = Self::build_table_rows(&self.entries, &self.home, &page_items, t);
        let title = self.build_table_title(&page_items);

        let header = Row::new(vec![
            Cell::from("Time"),
            Cell::from("Command"),
            Cell::from("Executor"),
            Cell::from("Path"),
            Cell::from("Status"),
            Cell::from("Duration"),
        ])
        .style(
            Style::default()
                .fg(t.text_secondary)
                .add_modifier(Modifier::BOLD),
        )
        .bottom_margin(1);

        let widths = [
            Constraint::Length(12),
            Constraint::Min(10),
            Constraint::Length(12),
            Constraint::Length(20),
            Constraint::Length(8),
            Constraint::Length(8),
        ];

        let table = Table::new(rows, widths)
            .header(header)
            .row_highlight_style(
                Style::default()
                    .bg(t.selection_bg)
                    .fg(t.selection_fg)
                    .add_modifier(Modifier::BOLD),
            )
            .highlight_symbol(" > ")
            .block(
                Block::default()
                    .borders(Borders::ALL)
                    .border_type(BorderType::Rounded)
                    .border_style(Style::default().fg(t.border))
                    .title(title),
            );

        f.render_stateful_widget(table, table_area, &mut self.table_state);

        if self.visible.is_empty() {
            let hint = Paragraph::new(Line::from(Span::styled(
                "  No agent commands found. Try a broader time range or check integration setup.",
                Style::default().fg(t.text_muted),
            )));
            let hint_area = Rect {
                x: table_area.x + 1,
                y: table_area.y + 2,
                width: table_area.width.saturating_sub(2),
                height: 1,
            };
            f.render_widget(hint, hint_area);
        }

        let total_pages = self.total_pages();
        let mut scrollbar_state =
            ScrollbarState::new(total_pages).position(self.page.saturating_sub(1));
        f.render_stateful_widget(
            Scrollbar::new(ScrollbarOrientation::VerticalRight)
                .thumb_style(Style::default().fg(t.primary_dim))
                .track_style(Style::default().fg(t.border)),
            scrollbar_area,
            &mut scrollbar_state,
        );
    }

    fn build_table_rows<'a>(
        entries: &'a [Entry],
        home: &str,
        page_items: &[usize],
        t: &crate::theme::Theme,
    ) -> Vec<Row<'a>> {
        page_items
            .iter()
            .map(|&idx| {
                let entry = &entries[idx];
                let rl = risk::risk_level(&entry.command);

                let time = format_datetime(entry.started_at);
                let executor = entry.executor.as_deref().unwrap_or("unknown");
                let path_full = shorten_path(&entry.cwd, home);
                let path_display = crate::util::truncate_str_start(&path_full, 18, "...");
                let command_display = crate::util::highlight_command(&entry.command, 0);

                let risk_icon = rl.icon();
                let exit_display = match entry.exit_code {
                    Some(0) => format!("✔ {risk_icon}"),
                    Some(c) => format!("✘ {c} {risk_icon}"),
                    None => format!("○ {risk_icon}"),
                };

                #[allow(clippy::cast_precision_loss)]
                let dur = entry.duration_ms as f64 / 1000.0;
                let dur_str = format!("{dur:.1}s");

                let exit_style = match entry.exit_code {
                    Some(0) => Style::default().fg(t.success),
                    Some(_) => Style::default().fg(t.error),
                    None => Style::default().fg(t.text_muted),
                };

                Row::new(vec![
                    Cell::from(time).style(Style::default().fg(t.text_muted)),
                    Cell::from(command_display),
                    Cell::from(executor).style(Style::default().fg(t.badge_executor)),
                    Cell::from(path_display).style(Style::default().fg(t.badge_path)),
                    Cell::from(exit_display).style(exit_style),
                    Cell::from(dur_str).style(Style::default().fg(t.text_muted)),
                ])
            })
            .collect()
    }

    fn build_table_title(&self, page_items: &[usize]) -> String {
        if self.visible.is_empty() {
            "Agent Commands (0/0)".to_string()
        } else {
            let start = (self.page - 1) * self.page_size + 1;
            let end = start + page_items.len().saturating_sub(1);
            format!("Agent Commands ({start}-{end} / {})", self.visible.len())
        }
    }

    fn render_detail(&self, f: &mut ratatui::Frame, area: Rect, t: &crate::theme::Theme) {
        let block = Block::default()
            .borders(Borders::ALL)
            .border_type(BorderType::Rounded)
            .border_style(Style::default().fg(t.border))
            .title(" Detail ")
            .title_style(
                Style::default()
                    .fg(t.text_secondary)
                    .add_modifier(Modifier::BOLD),
            );

        let inner = block.inner(area);
        f.render_widget(block, area);

        let Some(entry) = self.selected_entry() else {
            f.render_widget(
                Paragraph::new(Span::styled(
                    " No entry selected",
                    Style::default().fg(t.text_muted),
                )),
                inner,
            );
            return;
        };

        let label = Style::default()
            .fg(t.text_secondary)
            .add_modifier(Modifier::BOLD);
        let val = Style::default().fg(t.text);
        let rl = self.selected_risk();
        let max_w = inner.width.saturating_sub(2) as usize;

        let mut lines = Vec::new();
        Self::build_detail_command(&mut lines, entry, max_w, label, t);
        Self::build_detail_metadata(&mut lines, entry, &self.home, label, val, t);
        lines.push(Line::from(""));
        Self::build_detail_risk(&mut lines, entry, rl, label, t);
        Self::build_detail_prompt(&mut lines, entry, max_w, label, t);

        f.render_widget(Paragraph::new(lines), inner);
    }

    fn build_detail_command(
        lines: &mut Vec<Line>,
        entry: &Entry,
        max_w: usize,
        label: Style,
        t: &crate::theme::Theme,
    ) {
        lines.push(Line::from(Span::styled("Command", label)));
        let cmd_chars: Vec<char> = entry.command.chars().collect();
        for chunk in cmd_chars.chunks(max_w.max(1)) {
            let chunk_str: String = chunk.iter().collect();
            lines.push(Line::from(Span::styled(
                format!(" {chunk_str}"),
                Style::default().fg(t.primary),
            )));
        }
        lines.push(Line::from(""));
    }

    fn build_detail_metadata(
        lines: &mut Vec<Line>,
        entry: &Entry,
        home: &str,
        label: Style,
        val: Style,
        t: &crate::theme::Theme,
    ) {
        let path = shorten_path(&entry.cwd, home);
        lines.push(Line::from(vec![
            Span::styled("Path     ", label),
            Span::styled(path, val),
        ]));

        let time_str = format_full_datetime(entry.started_at);
        lines.push(Line::from(vec![
            Span::styled("Time     ", label),
            Span::styled(time_str, val),
        ]));

        #[allow(clippy::cast_precision_loss)]
        let dur_secs = entry.duration_ms as f64 / 1000.0;
        lines.push(Line::from(vec![
            Span::styled("Duration ", label),
            Span::styled(format!("{dur_secs:.2}s"), val),
        ]));

        let exit_str = match entry.exit_code {
            Some(0) => "✔ 0 (success)".to_string(),
            Some(c) => format!("✘ {c} (failed)"),
            None => "○ (unknown)".to_string(),
        };
        let exit_style = match entry.exit_code {
            Some(0) => Style::default().fg(t.success),
            Some(_) => Style::default().fg(t.error),
            None => Style::default().fg(t.text_muted),
        };
        lines.push(Line::from(vec![
            Span::styled("Exit     ", label),
            Span::styled(exit_str, exit_style),
        ]));

        let executor = match (&entry.executor_type, &entry.executor) {
            (Some(et), Some(n)) => format!("{et}: {n}"),
            (Some(et), None) => et.clone(),
            (None, Some(n)) => n.clone(),
            _ => "unknown".to_string(),
        };
        lines.push(Line::from(vec![
            Span::styled("Executor ", label),
            Span::styled(executor, val),
        ]));

        lines.push(Line::from(vec![
            Span::styled("Session  ", label),
            Span::styled(entry.session_id.clone(), val),
        ]));
    }

    fn build_detail_risk(
        lines: &mut Vec<Line>,
        entry: &Entry,
        rl: RiskLevel,
        label: Style,
        t: &crate::theme::Theme,
    ) {
        if rl > RiskLevel::None {
            if let Some(a) = risk::assess_risk(&entry.command) {
                let risk_color = match a.level {
                    RiskLevel::Critical => t.risk_critical,
                    RiskLevel::High => t.risk_high,
                    RiskLevel::Medium => t.risk_medium,
                    RiskLevel::Low => t.risk_low,
                    RiskLevel::None => t.text_muted,
                };
                lines.push(Line::from(vec![
                    Span::styled("Risk     ", label),
                    Span::styled(
                        format!(
                            "{} {} — {}",
                            a.level.icon(),
                            a.level.label().to_uppercase(),
                            a.category,
                        ),
                        Style::default().fg(risk_color),
                    ),
                ]));
                lines.push(Line::from(Span::styled(
                    format!("         {}", a.description),
                    Style::default().fg(t.text_muted),
                )));
                lines.push(Line::from(""));
            }
        }
    }

    fn build_detail_prompt(
        lines: &mut Vec<Line>,
        entry: &Entry,
        max_w: usize,
        label: Style,
        t: &crate::theme::Theme,
    ) {
        if let Some(ctx) = &entry.context {
            if let Some(prompt) = ctx.get("agent_prompt") {
                lines.push(Line::from(Span::styled("Prompt", label)));
                let prompt_chars: Vec<char> = prompt.chars().collect();
                for chunk in prompt_chars.chunks(max_w.max(1)) {
                    let chunk_str: String = chunk.iter().collect();
                    lines.push(Line::from(Span::styled(
                        format!(" {chunk_str}"),
                        Style::default().fg(t.info),
                    )));
                }
            }
        }
    }

    fn render_footer(&self, f: &mut ratatui::Frame, area: Rect, t: &crate::theme::Theme) {
        let badge_key = Style::default()
            .fg(t.bg_elevated)
            .bg(t.text_secondary)
            .add_modifier(Modifier::BOLD);
        let badge_label = Style::default().fg(t.text_muted);

        let total_pages = self.total_pages();

        let mut spans = vec![
            Span::styled(" 1-4 ", badge_key),
            Span::styled(" Period  ", badge_label),
            Span::styled(" ←→ ", badge_key),
            Span::styled(" Page  ", badge_label),
            Span::styled(" Tab ", badge_key),
            Span::styled(" Detail  ", badge_label),
            Span::styled(" a ", badge_key),
            Span::styled(" Agent  ", badge_label),
            Span::styled(" r ", badge_key),
            Span::styled(
                if self.risk_filter {
                    " All  "
                } else {
                    " Risk only  "
                },
                badge_label,
            ),
            Span::styled(" p ", badge_key),
            Span::styled(" Prompts  ", badge_label),
            Span::styled(" ^Y ", badge_key),
            Span::styled(" Copy  ", badge_label),
            Span::styled(" q/Esc ", badge_key),
            Span::styled(" Quit  ", badge_label),
        ];

        spans.push(Span::styled(
            format!(" {}/{total_pages} ", self.page),
            Style::default().fg(t.text_muted),
        ));

        if let Some((msg, time)) = &self.status_message {
            if time.elapsed() < std::time::Duration::from_secs(2) {
                spans.push(Span::styled(
                    format!(" {msg} "),
                    Style::default().fg(t.success).add_modifier(Modifier::BOLD),
                ));
            }
        }

        f.render_widget(Paragraph::new(Line::from(spans)), area);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn make_entry(cmd: &str, executor: Option<&str>, cwd: &str) -> Entry {
        let mut e = Entry::new(
            "sess1".into(),
            cmd.into(),
            cwd.into(),
            Some(0),
            1_000_000,
            1_001_000,
        );
        e.executor_type = Some("agent".into());
        e.executor = executor.map(String::from);
        e
    }

    fn make_app(entries: Vec<Entry>) -> AgentApp {
        let visible: Vec<usize> = (0..entries.len()).rev().collect();
        let agent_counts = compute_agent_counts(&entries);
        let agent_names: Vec<String> = agent_counts.iter().map(|(n, _)| n.clone()).collect();
        let risk_summary = risk::session_risk(&entries);
        let visible_high_risk_count = visible
            .iter()
            .filter(|&&i| risk::risk_level(&entries[i].command) >= RiskLevel::High)
            .count();
        let mut app = AgentApp {
            entries,
            visible,
            risk_summary,
            agent_counts,
            agent_names,
            visible_high_risk_count,
            period: Period::AllTime,
            agent_filter: None,
            risk_filter: false,
            cli_executor: None,
            cwd_filter: None,
            page: 1,
            page_size: PAGE_SIZE,
            table_state: TableState::default(),
            detail_open: true,
            home: "/home/test".into(),
            status_message: None,
        };
        if !app.visible.is_empty() {
            app.table_state.select(Some(0));
        }
        app
    }

    // ── build_table_title ──

    #[test]
    fn build_table_title_empty() {
        let app = make_app(vec![]);
        assert_eq!(app.build_table_title(&[]), "Agent Commands (0/0)");
    }

    #[test]
    fn build_table_title_with_items() {
        let entries = vec![
            make_entry("ls", Some("claude"), "/tmp"),
            make_entry("pwd", Some("claude"), "/home"),
        ];
        let app = make_app(entries);
        let page_items: Vec<usize> = app.page_slice().to_vec();
        let title = app.build_table_title(&page_items);
        assert!(title.contains("1-2"));
        assert!(title.contains("/ 2"));
    }

    // ── total_pages ──

    #[test]
    fn total_pages_empty() {
        let app = make_app(vec![]);
        assert_eq!(app.total_pages(), 1);
    }

    #[test]
    fn total_pages_within_one_page() {
        let entries: Vec<Entry> = (0..5)
            .map(|i| make_entry(&format!("cmd{i}"), Some("claude"), "/tmp"))
            .collect();
        let app = make_app(entries);
        assert_eq!(app.total_pages(), 1);
    }

    // ── page_slice ──

    #[test]
    fn page_slice_empty() {
        let app = make_app(vec![]);
        assert!(app.page_slice().is_empty());
    }

    #[test]
    fn page_slice_returns_correct_count() {
        let entries: Vec<Entry> = (0..5)
            .map(|i| make_entry(&format!("cmd{i}"), Some("claude"), "/tmp"))
            .collect();
        let app = make_app(entries);
        assert_eq!(app.page_slice().len(), 5);
    }

    // ── selected_entry ──

    #[test]
    fn selected_entry_returns_entry() {
        let entries = vec![
            make_entry("first", Some("claude"), "/tmp"),
            make_entry("second", Some("claude"), "/tmp"),
        ];
        let app = make_app(entries);
        let sel = app.selected_entry().unwrap();
        // visible is reversed, so index 0 in visible = last entry
        assert_eq!(sel.command, "second");
    }

    #[test]
    fn selected_entry_empty() {
        let app = make_app(vec![]);
        assert!(app.selected_entry().is_none());
    }

    // ── selected_risk ──

    #[test]
    fn selected_risk_safe_command() {
        let entries = vec![make_entry("ls -la", Some("claude"), "/tmp")];
        let app = make_app(entries);
        assert!(app.selected_risk() <= RiskLevel::Low);
    }

    #[test]
    fn selected_risk_dangerous_command() {
        let entries = vec![make_entry("rm -rf /", Some("claude"), "/tmp")];
        let app = make_app(entries);
        assert!(app.selected_risk() >= RiskLevel::High);
    }

    // ── rebuild_visible ──

    #[test]
    fn rebuild_visible_no_filter() {
        let entries = vec![
            make_entry("ls", Some("claude"), "/tmp"),
            make_entry("pwd", Some("cursor"), "/home"),
        ];
        let mut app = make_app(entries);
        app.rebuild_visible();
        assert_eq!(app.visible.len(), 2);
    }

    #[test]
    fn rebuild_visible_agent_filter() {
        let entries = vec![
            make_entry("ls", Some("claude"), "/tmp"),
            make_entry("pwd", Some("cursor"), "/home"),
            make_entry("cat", Some("claude"), "/tmp"),
        ];
        let mut app = make_app(entries);
        // Filter to first agent (sorted by count, claude has 2)
        app.agent_filter = Some(0);
        app.rebuild_visible();
        assert_eq!(app.visible.len(), 2);
    }

    #[test]
    fn rebuild_visible_risk_filter() {
        let entries = vec![
            make_entry("ls", Some("claude"), "/tmp"),
            make_entry("rm -rf /important", Some("claude"), "/tmp"),
        ];
        let mut app = make_app(entries);
        app.risk_filter = true;
        app.rebuild_visible();
        // Only the risky command should pass
        assert!(app.visible.len() <= 2);
    }

    #[test]
    fn rebuild_visible_resets_page_and_selection() {
        let entries = vec![make_entry("ls", Some("claude"), "/tmp")];
        let mut app = make_app(entries);
        app.page = 3;
        app.rebuild_visible();
        assert_eq!(app.page, 1);
        assert_eq!(app.table_state.selected(), Some(0));
    }

    #[test]
    fn rebuild_visible_empty_selects_none() {
        let entries = vec![make_entry("ls", Some("claude"), "/tmp")];
        let mut app = make_app(entries);
        // Filter to non-existent agent
        app.agent_names = vec!["nonexistent".into()];
        app.agent_filter = Some(0);
        app.rebuild_visible();
        assert!(app.visible.is_empty());
        assert!(app.table_state.selected().is_none());
    }

    // ── handle_input (non-repo paths) ──

    #[test]
    fn handle_input_q_quits() {
        // handle_input needs repo for reload, but q/esc don't touch it
        // We test indirectly by checking the return value
        let entries = vec![make_entry("ls", Some("claude"), "/tmp")];
        let mut app = make_app(entries);
        // We can't call handle_input without a repo for period changes,
        // but we can test simple key responses by checking state changes
        assert!(app.detail_open);
        // Toggle detail directly (handle_input needs repo for period changes)
        app.detail_open = !app.detail_open;
        assert!(!app.detail_open);
    }

    #[test]
    fn visible_high_risk_count_tracked() {
        let entries = vec![
            make_entry("ls", Some("claude"), "/tmp"),
            make_entry("rm -rf /", Some("claude"), "/tmp"),
        ];
        let app = make_app(entries);
        // The rm -rf should be counted as high risk
        assert!(app.visible_high_risk_count >= 1);
    }
}

// ── Public entry: Agent Dashboard ────────────────────────────

pub fn run_agent_ui<B: Backend>(
    terminal: &mut Terminal<B>,
    repo: &Repository,
    initial_after_ms: Option<i64>,
    executor: Option<&str>,
    cwd: Option<&str>,
) -> io::Result<()>
where
    io::Error: From<B::Error>,
{
    let mut app = AgentApp::new(repo, initial_after_ms, executor, cwd);

    loop {
        terminal.draw(|f| app.render(f))?;

        // Poll with timeout so stale status messages get cleared even without user input
        let timeout = if app.status_message.is_some() {
            std::time::Duration::from_secs(2)
        } else {
            std::time::Duration::from_mins(1)
        };
        if !event::poll(timeout)? {
            continue; // timeout — re-render to clear stale status
        }
        if let Event::Key(key) = event::read()? {
            if key.kind != KeyEventKind::Press {
                continue;
            }
            match app.handle_input(key, repo) {
                DashboardAction::Quit => return Ok(()),
                DashboardAction::OpenPrompts => {
                    super::prompts::run_prompt_explorer(terminal, &app.entries, Some(repo))?;
                }
                DashboardAction::Continue => {}
            }
        }
    }
}
