//! Minimal-mode below-prompt **list panels**: `/resume` (session picker) and
//! `/mcps` (MCP server status), rendered as simple lists *below the input bar*
//! instead of centered modal windows (design nit: "the mcps / resume lists
//! should not be in a modal").
//!
//! ## Why this is a render-only change
//!
//! Input routing is unchanged — the existing `handle_modal_key`
//! (`ActiveModal::SessionPicker`) and `handle_extensions_modal_key`
//! (`extensions_modal`) own navigation and close-on-Esc. Two different coupling
//! contracts are honored here:
//!
//! * **Session picker** rebuilds its entry map from data on every keypress
//!   (render-independent), so we just reuse the *same* builders
//!   ([`build_grouped_picker_entries`]) — the rendered order then matches the
//!   handler's `selected`.
//! * **Extensions modal** reads render-stored state (`entry_data_indices`,
//!   `entry_group_keys`, `entry_non_selectable*`). The MCP renderer repopulates
//!   those exactly as the full modal does (via the shared
//!   [`build_mcp_servers_picker_rows`]), so keyboard nav + section fold stay in
//!   sync without touching the input handler.
//!
//! Both reuse [`picker::render_picker_content`] for the rows, so row look +
//! selection highlight match the full TUI; only the modal-window chrome (border,
//! tabs, footer bar) is dropped.

use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::Span;

use kigi_tui::app::agent_view::AgentView;
use kigi_tui::minimal_api;
use kigi_tui::theme::Theme;
use kigi_tui::views::extensions_modal::{ExtensionsTab, TabDataState};
use kigi_tui::views::modal::ActiveModal;
use kigi_tui::views::picker::{self, PickerEntry, PickerField, PickerHitAreas, PickerRow};

/// Rows of chrome around the scrolling list: title + subtitle/search + divider
/// + footer.
const CHROME_ROWS: u16 = 4;

/// Which below-prompt list panel is active for the focused agent.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ListPanel {
    /// `/resume` session picker (`ActiveModal::SessionPicker`).
    Resume,
    /// `/mcps` MCP server status (extensions modal on the McpServers tab).
    Mcps,
}

/// Detect an active below-prompt list panel, or `None`.
///
/// Only the session picker and the MCP-servers tab are hosted as simple lists;
/// every other modal keeps its existing (centered) rendering. Callers must check
/// this *before* `overlay::app_modal_active`, since `SessionPicker` is also an
/// `active_modal`.
pub(super) fn active(agent: &AgentView) -> Option<ListPanel> {
    if matches!(agent.active_modal, Some(ActiveModal::SessionPicker { .. })) {
        return Some(ListPanel::Resume);
    }
    if minimal_api::extensions_modal(agent)
        .is_some_and(|s| s.active_tab == ExtensionsTab::McpServers)
    {
        return Some(ListPanel::Mcps);
    }
    None
}

/// Target viewport height for the active list panel: chrome + the exact body
/// height, clamped to `[CHROME_ROWS + 1, ceiling]`. Sizing to the exact content
/// height keeps the footer directly under the last row (no blank band); when the
/// body exceeds `ceiling` the list scrolls internally.
pub(super) fn panel_height(agent: &AgentView, kind: ListPanel, width: u16, ceiling: u16) -> u16 {
    let body = match kind {
        ListPanel::Resume => resume_body_rows(agent, width),
        ListPanel::Mcps => mcps_body_rows(agent),
    };
    CHROME_ROWS
        .saturating_add(body)
        .clamp(CHROME_ROWS + 1, ceiling.max(CHROME_ROWS + 1))
}

/// Render the active list panel into `area` (the whole live region). Returns the
/// text cursor for the panel's search bar when search is focused, else `None`.
pub(super) fn render(
    buf: &mut Buffer,
    area: Rect,
    agent: &mut AgentView,
    kind: ListPanel,
    theme: &Theme,
) -> Option<(u16, u16)> {
    if area.height < 2 || area.width < 8 {
        return None;
    }
    match kind {
        ListPanel::Resume => render_resume(buf, area, agent, theme),
        ListPanel::Mcps => render_mcps(buf, area, agent, theme),
    }
}

// ─────────────────────────────── chrome ─────────────────────────────────────

/// Split `area` into (title_row, second_row, divider_row, list_area, footer_row).
/// `second_row` hosts the subtitle (mcps) or the search bar (resume).
fn chrome_layout(area: Rect) -> (Rect, Rect, Rect, Rect, Rect) {
    let row = |dy: u16| Rect {
        x: area.x,
        y: area.y + dy,
        width: area.width,
        height: 1,
    };
    let title = row(0);
    let second = row(1);
    let divider = row(2);
    let footer = Rect {
        x: area.x,
        y: area.y + area.height - 1,
        ..row(0)
    };
    let list = Rect {
        x: area.x,
        y: area.y + 3,
        width: area.width,
        height: area.height.saturating_sub(CHROME_ROWS),
    };
    (title, second, divider, list, footer)
}

fn render_title(buf: &mut Buffer, row: Rect, theme: &Theme, title: &str) {
    buf.set_style(row, Style::default().bg(Color::Reset));
    let style = Style::default()
        .fg(theme.accent_user)
        .bg(Color::Reset)
        .add_modifier(Modifier::BOLD);
    buf.set_span(row.x + 1, row.y, &Span::styled(title, style), row.width);
}

fn render_dim_line(buf: &mut Buffer, row: Rect, theme: &Theme, text: &str) {
    buf.set_style(row, Style::default().bg(Color::Reset));
    let style = theme.dim().bg(Color::Reset);
    buf.set_span(row.x + 1, row.y, &Span::styled(text, style), row.width);
}

/// `/resume` session picker: Enter picks a session.
const RESUME_FOOTER: &str = "\u{2191}/\u{2193} navigate \u{00b7} enter confirm \u{00b7} esc cancel";

/// `/mcps` list: Enter expands tools; reconnect is space (off then on); `r` re-lists status.
const MCPS_FOOTER: &str = "\u{2191}/\u{2193} navigate \u{00b7} space enable/disable \u{00b7} r refresh \u{00b7} enter expand \u{00b7} esc cancel";

fn render_footer(buf: &mut Buffer, row: Rect, theme: &Theme, text: &str) {
    render_dim_line(buf, row, theme, text);
}

fn render_divider(buf: &mut Buffer, row: Rect, theme: &Theme) {
    picker::render_divider(buf, row.x, row.y, row.width, theme, None);
}

// ─────────────────────────────── resume ─────────────────────────────────────

/// Exact body height (display rows) for the session-picker list.
fn resume_body_rows(agent: &AgentView, width: u16) -> u16 {
    let Some(ActiveModal::SessionPicker {
        entries,
        state,
        source_filter,
        ..
    }) = &agent.active_modal
    else {
        return 0;
    };
    let entries_data = entries.as_deref().unwrap_or(&[]);
    let content_width = width.saturating_sub(2);
    let filtered =
        minimal_api::filter_session_entries(entries.as_deref(), &state.query, *source_filter);
    let built =
        minimal_api::build_session_entry_data(entries_data, &filtered, state, content_width);
    let fields_vecs: Vec<Vec<PickerField>> = built
        .iter()
        .map(|b| {
            b.field_data
                .iter()
                .map(|(l, v)| PickerField { label: l, value: v })
                .collect()
        })
        .collect();
    let current_repo = minimal_api::repo_name_from_cwd(&agent.session.cwd.to_string_lossy());
    let (picker_entries, _) = minimal_api::build_grouped_picker_entries(
        entries_data,
        &filtered,
        &built,
        &fields_vecs,
        state,
        Some(current_repo.as_str()),
    );
    measure_entries(&picker_entries)
}

fn render_resume(
    buf: &mut Buffer,
    area: Rect,
    agent: &mut AgentView,
    theme: &Theme,
) -> Option<(u16, u16)> {
    let cwd = agent.session.cwd.to_string_lossy().to_string();
    let Some(ActiveModal::SessionPicker {
        entries,
        state,
        source_filter,
        ..
    }) = &mut agent.active_modal
    else {
        return None;
    };
    let (title_row, search_row, divider_row, list_area, footer_row) = chrome_layout(area);

    let entries_data = entries.as_deref().unwrap_or(&[]);
    let content_width = area.width.saturating_sub(2);
    let filtered =
        minimal_api::filter_session_entries(entries.as_deref(), &state.query, *source_filter);
    let built =
        minimal_api::build_session_entry_data(entries_data, &filtered, state, content_width);
    let fields_vecs: Vec<Vec<PickerField>> = built
        .iter()
        .map(|b| {
            b.field_data
                .iter()
                .map(|(l, v)| PickerField { label: l, value: v })
                .collect()
        })
        .collect();
    let current_repo = minimal_api::repo_name_from_cwd(&cwd);
    let (picker_entries, non_sel) = minimal_api::build_grouped_picker_entries(
        entries_data,
        &filtered,
        &built,
        &fields_vecs,
        state,
        Some(current_repo.as_str()),
    );

    render_title(buf, title_row, theme, "Resume session");
    // Focus-aware search bar (cursor only when search is focused).
    picker::render_search_bar(
        buf,
        search_row.x + 1,
        search_row.y,
        search_row.width.saturating_sub(1),
        theme,
        &state.query,
        state.search_active,
        true,
        state.query_cursor,
        None,
    );
    render_divider(buf, divider_row, theme);

    let nsc = vec![false; picker_entries.len()];
    let hit = picker::render_picker_content(
        buf,
        list_area,
        theme,
        state,
        &picker_entries,
        &non_sel,
        &nsc,
        None,
        false,
    );
    state.hit_areas = Some(PickerHitAreas {
        close_button: Rect::default(),
        search_bar: search_row,
        item_rects: hit.item_rects,
        entry_indices: hit.entry_indices,
        tab_rects: vec![],
        filter_rect: None,
    });

    render_footer(buf, footer_row, theme, RESUME_FOOTER);
    None
}

// ──────────────────────────────── mcps ──────────────────────────────────────

/// Exact body height (display rows) for the MCP list: one line per row.
fn mcps_body_rows(agent: &AgentView) -> u16 {
    let Some(s) = minimal_api::extensions_modal(agent) else {
        return 0;
    };
    let servers = match &s.mcps_data {
        TabDataState::Loaded(v) => v.as_slice(),
        _ => return 1, // a single "loading…" / error row
    };
    let rows = minimal_api::build_mcp_picker_rows(
        servers,
        &s.picker_state.query,
        s.mcps_filter,
        &s.mcps_collapsed_sections,
        &s.mcps_tools_expanded,
    );
    rows.0.len() as u16
}

fn render_mcps(
    buf: &mut Buffer,
    area: Rect,
    agent: &mut AgentView,
    theme: &Theme,
) -> Option<(u16, u16)> {
    let (title_row, subtitle_row, divider_row, list_area, footer_row) = chrome_layout(area);
    render_title(buf, title_row, theme, "Manage MCP servers");

    // Phase 1 (immutable): build the row mapping + owned per-row render data.
    let labels: Vec<String>;
    let group_keys: Vec<Option<String>>;
    let data_indices: Vec<Option<usize>>;
    let badges: Vec<String>;
    let badge_colors: Vec<Option<Color>>;
    let right_labels: Vec<String>;
    let indents: Vec<u8>;
    let collapsibles: Vec<bool>;
    let expandeds: Vec<bool>;
    let subtitle: String;
    let loading;
    {
        let s = minimal_api::extensions_modal(agent)?;
        let searching = !s.picker_state.query.is_empty();
        loading = matches!(s.mcps_data, TabDataState::Loading);
        match &s.mcps_data {
            TabDataState::Loaded(servers) => {
                let (row_labels, row_group_keys, row_data_indices) =
                    minimal_api::build_mcp_picker_rows(
                        servers,
                        &s.picker_state.query,
                        s.mcps_filter,
                        &s.mcps_collapsed_sections,
                        &s.mcps_tools_expanded,
                    );
                let n = row_labels.len();
                let mut b = vec![String::new(); n];
                let mut bc: Vec<Option<Color>> = vec![None; n];
                let mut rl = vec![String::new(); n];
                let mut ind = vec![0u8; n];
                let mut col = vec![false; n];
                let mut exp = vec![false; n];
                for i in 0..n {
                    let gk = row_group_keys[i].as_deref();
                    if gk.is_some_and(|k| k.starts_with("mcp-section:")) {
                        col[i] = true;
                        exp[i] = !minimal_api::mcp_section_children_hidden(
                            &s.mcps_collapsed_sections,
                            gk.unwrap(),
                            searching,
                        );
                    } else if gk.is_some_and(|k| k.starts_with("mcp-tools:")) {
                        ind[i] = 1;
                        col[i] = true;
                        if let Some(si) = row_data_indices[i] {
                            exp[i] = s.mcps_tools_expanded.contains(&si);
                            if let Some(srv) = servers.get(si) {
                                if !srv.enabled {
                                    b[i] = "disabled".to_string();
                                    bc[i] = Some(theme.accent_error);
                                } else {
                                    b[i] = minimal_api::mcp_status_label(&srv.status).to_string();
                                    bc[i] = Some(minimal_api::mcp_status_theme_color(
                                        &srv.status,
                                        theme,
                                    ));
                                }
                                rl[i] = if srv.tool_count == 1 {
                                    "1 tool".to_string()
                                } else {
                                    format!("{} tools", srv.tool_count)
                                };
                            }
                        }
                    } else {
                        ind[i] = 2; // tool child
                    }
                }
                subtitle = format!(
                    "{} server{}",
                    servers.len(),
                    if servers.len() == 1 { "" } else { "s" }
                );
                labels = row_labels;
                group_keys = row_group_keys;
                data_indices = row_data_indices;
                badges = b;
                badge_colors = bc;
                right_labels = rl;
                indents = ind;
                collapsibles = col;
                expandeds = exp;
            }
            TabDataState::Loading => {
                subtitle = "loading\u{2026}".to_string();
                labels = vec![];
                group_keys = vec![];
                data_indices = vec![];
                badges = vec![];
                badge_colors = vec![];
                right_labels = vec![];
                indents = vec![];
                collapsibles = vec![];
                expandeds = vec![];
            }
            TabDataState::Error(msg) => {
                subtitle = format!("error: {msg}");
                labels = vec![];
                group_keys = vec![];
                data_indices = vec![];
                badges = vec![];
                badge_colors = vec![];
                right_labels = vec![];
                indents = vec![];
                collapsibles = vec![];
                expandeds = vec![];
            }
        }
    }
    let n = labels.len();

    render_dim_line(buf, subtitle_row, theme, &subtitle);
    render_divider(buf, divider_row, theme);

    // Phase 2 (mutable): mirror the row mapping onto state for the input handler.
    {
        let s = minimal_api::extensions_modal_mut(agent)?;
        s.entry_data_indices = data_indices;
        s.entry_group_keys = group_keys;
        s.entry_labels_cache = labels.clone();
        s.entry_non_selectable = vec![false; n];
        s.entry_non_selectable_clickable = vec![false; n];
        if n == 0 {
            s.picker_state.selected = 0;
        } else if s.picker_state.selected >= n {
            s.picker_state.selected = n - 1;
        }
    }

    // Phase 3 (mutable picker_state): build PickerEntry from owned data + render.
    let s = minimal_api::extensions_modal_mut(agent)?;
    let selected = s.picker_state.selected;
    let search_active = s.picker_state.search_active;
    let empty_fields: [PickerField; 0] = [];
    let no_lines: [&str; 0] = [];
    let entries: Vec<PickerEntry> = (0..n)
        .map(|i| {
            PickerEntry::Row(PickerRow {
                label: labels[i].as_str(),
                right_label: right_labels[i].as_str(),
                selected: !search_active && i == selected,
                expanded: expandeds[i],
                fields: &empty_fields,
                description_lines: &no_lines,
                summary_lines: &no_lines,
                dimmed: false,
                indent: indents[i],
                badge: badges[i].as_str(),
                badge_color: badge_colors[i],
                collapsible: collapsibles[i],
            })
        })
        .collect();
    let non_sel = vec![false; n];
    let hit = picker::render_picker_content(
        buf,
        list_area,
        theme,
        &mut s.picker_state,
        &entries,
        &non_sel,
        &non_sel,
        None,
        loading,
    );
    s.picker_state.hit_areas = Some(PickerHitAreas {
        close_button: Rect::default(),
        search_bar: Rect::default(),
        item_rects: hit.item_rects,
        entry_indices: hit.entry_indices,
        tab_rects: vec![],
        filter_rect: None,
    });

    render_footer(buf, footer_row, theme, MCPS_FOOTER);
    None
}

// ─────────────────────────────── helpers ────────────────────────────────────

/// Sum the display height of grouped picker entries: a header is one row; a row
/// is its label line plus its collapsed summary lines (what the picker draws
/// when the row is not expanded).
fn measure_entries(entries: &[PickerEntry<'_>]) -> u16 {
    entries
        .iter()
        .map(|e| match e {
            PickerEntry::Header { .. } => 1u16,
            PickerEntry::Row(r) => {
                if r.expanded {
                    1u16.saturating_add(r.description_lines.len() as u16)
                        .saturating_add(r.fields.len() as u16)
                } else {
                    1u16.saturating_add(r.summary_lines.len() as u16)
                }
            }
        })
        .fold(0u16, |acc, h| acc.saturating_add(h))
}

#[cfg(test)]
mod tests {
    use super::*;
    use kigi_tui::views::extensions_modal::ExtensionsModalState;
    use kigi_tui::views::mcps_modal::{McpServerDisplayStatus, McpServerInfo};
    use ratatui::layout::Rect;

    fn agent() -> AgentView {
        minimal_api::test_agent_view(Some("s1"), std::path::PathBuf::from("/tmp/repo"))
    }

    fn mcp_server(name: &str, status: McpServerDisplayStatus, tools: usize) -> McpServerInfo {
        McpServerInfo {
            name: name.to_string(),
            display_name: None,
            status,
            tool_count: tools,
            auth_required: false,
            tools: Vec::new(),
            enabled: true,
            source: "local".to_string(),
            plugin_name: None,
        }
    }

    fn with_mcps(servers: Vec<McpServerInfo>) -> AgentView {
        let mut a = agent();
        minimal_api::set_extensions_modal(
            &mut a,
            Some(ExtensionsModalState {
                active_tab: ExtensionsTab::McpServers,
                mcps_data: TabDataState::Loaded(servers),
                ..Default::default()
            }),
        );
        a
    }

    fn session_entry(id: &str) -> kigi_tui::app::app_view::SessionPickerEntry {
        kigi_tui::app::app_view::SessionPickerEntry {
            id: id.into(),
            summary: id.into(),
            updated_at: chrono::Utc::now(),
            created_at: chrono::Utc::now(),
            cwd: "/tmp/repo".into(),
            hostname: None,
            source: String::new(),
            model_id: None,
            num_messages: 0,
            last_active_at: None,
            branch: None,
            repo_name: "repo".into(),
            worktree_label: None,
            card_detail: None,
        }
    }

    fn with_resume(entries: Vec<kigi_tui::app::app_view::SessionPickerEntry>) -> AgentView {
        let mut a = agent();
        a.active_modal = Some(ActiveModal::SessionPicker {
            state: picker::PickerState::default(),
            entries: Some(entries),
            loading: false,
            lanes: Default::default(),
            previous_palette: None,
            window: kigi_tui::views::modal_window::ModalWindowState::new(),
            content_results: None,
            content_loading: false,
            deep_search_seq: 0,
            source_filter: kigi_tui::views::session_picker::SourceFilter::default(),
            pending_delete: None,
            entries_query: None,
        });
        a
    }

    fn buffer_text(buf: &Buffer) -> String {
        let area = buf.area;
        let mut out = String::new();
        for y in area.y..area.y + area.height {
            for x in area.x..area.x + area.width {
                out.push_str(buf.cell((x, y)).map(|c| c.symbol()).unwrap_or(" "));
            }
            out.push('\n');
        }
        out
    }

    #[test]
    fn active_detects_resume_mcps_and_none() {
        assert_eq!(active(&agent()), None);
        assert_eq!(
            active(&with_mcps(vec![mcp_server(
                "alpha",
                McpServerDisplayStatus::Ready,
                3
            )])),
            Some(ListPanel::Mcps)
        );
        assert_eq!(
            active(&with_resume(vec![session_entry("hello")])),
            Some(ListPanel::Resume)
        );
    }

    #[test]
    fn mcps_panel_renders_list_and_mirrors_handler_state() {
        let mut a = with_mcps(vec![
            mcp_server("alpha", McpServerDisplayStatus::Ready, 3),
            mcp_server("bravo", McpServerDisplayStatus::Unavailable, 0),
        ]);
        let theme = Theme::current();
        let area = Rect::new(0, 0, 80, 24);
        let mut buf = Buffer::empty(area);
        render(&mut buf, area, &mut a, ListPanel::Mcps, &theme);

        let text = buffer_text(&buf);
        assert!(text.contains("Manage MCP servers"), "title:\n{text}");
        assert!(text.contains("2 servers"), "subtitle:\n{text}");
        assert!(text.contains("alpha"), "server row:\n{text}");
        assert!(text.contains("bravo"), "server row:\n{text}");
        assert!(text.contains("space enable/disable"), "footer:\n{text}");
        assert!(text.contains("r refresh"), "footer:\n{text}");
        assert!(text.contains("enter expand"), "footer:\n{text}");
        assert!(
            !text.contains("enter confirm"),
            "MCP footer must not reuse resume confirm copy:\n{text}"
        );

        // The input handler reads these render-stored fields; the panel must
        // mirror them (section header + 2 servers = 3 rows) so keyboard nav and
        // fold stay correct without touching the handler.
        let s = minimal_api::extensions_modal(&a).unwrap();
        assert_eq!(s.entry_data_indices.len(), 3, "section + 2 servers");
        assert_eq!(
            s.entry_data_indices.iter().filter(|d| d.is_some()).count(),
            2,
            "two selectable server rows map to catalog indices"
        );
        assert_eq!(s.entry_non_selectable.len(), 3);
    }

    #[test]
    fn resume_panel_renders_title_rows_and_footer() {
        let mut a = with_resume(vec![session_entry("first task"), session_entry("second")]);
        let theme = Theme::current();
        let area = Rect::new(0, 0, 80, 24);
        let mut buf = Buffer::empty(area);
        render(&mut buf, area, &mut a, ListPanel::Resume, &theme);

        let text = buffer_text(&buf);
        assert!(text.contains("Resume session"), "title:\n{text}");
        assert!(text.contains("first task"), "session row:\n{text}");
        assert!(text.contains("enter confirm"), "resume footer:\n{text}");
        assert!(
            !text.contains("r refresh"),
            "resume footer must stay session-picker copy:\n{text}"
        );
    }

    #[test]
    fn mcps_panel_height_is_chrome_plus_rows() {
        // One section header + 2 server rows = 3 body rows; + 4 chrome = 7.
        let a = with_mcps(vec![
            mcp_server("alpha", McpServerDisplayStatus::Ready, 1),
            mcp_server("bravo", McpServerDisplayStatus::Ready, 2),
        ]);
        assert_eq!(panel_height(&a, ListPanel::Mcps, 80, 40), 7);
        // Clamps to the screen ceiling when content is taller.
        assert_eq!(panel_height(&a, ListPanel::Mcps, 80, 5), 5);
    }
}
