//! Command palette overlay — modal fuzzy-match command picker.
//!
//! Renders a centered modal with a query input at the top and a scrollable
//! list of matching commands below. Keybinding hints are right-aligned.

use ftui::core::geometry::Rect;
use ftui::render::cell::Cell;
use ftui::render::drawing::{BorderChars, Draw};
use ftui::render::frame::Frame;

use crate::state::command_palette::CommandPaletteState;
use crate::text_width::cursor_cell_offset;

use super::{ACCENT, BG_SURFACE, BORDER, TEXT, TEXT_MUTED};

fn text_cell_width(text: &str) -> u16 {
    text.chars().count().min(u16::MAX as usize) as u16
}

// ---------------------------------------------------------------------------
// render_command_palette
// ---------------------------------------------------------------------------

/// Render the command palette overlay centered on the screen.
///
/// Only renders if `state.is_open()`. The modal is 60% width, 50% height,
/// capped at 60x20.
pub fn render_command_palette(frame: &mut Frame<'_>, state: &CommandPaletteState, area: Rect) {
    if !state.is_open() {
        return;
    }
    if area.height < 5 || area.width < 20 {
        return;
    }

    // Modal dimensions: 60% of screen, capped.
    let modal_width = (area.width * 3 / 5).clamp(30, 60);
    let modal_height = (area.height / 2).clamp(6, 20);

    let modal_x = area.x + (area.width.saturating_sub(modal_width)) / 2;
    let modal_y = area.y + (area.height.saturating_sub(modal_height)) / 2;
    let modal_rect = Rect::new(modal_x, modal_y, modal_width, modal_height);

    // Clear background.
    let bg_cell = Cell {
        fg: TEXT,
        bg: BG_SURFACE,
        ..Cell::default()
    };
    for y in modal_rect.y..modal_rect.bottom() {
        for x in modal_rect.x..modal_rect.right() {
            frame.buffer.set(x, y, bg_cell);
        }
    }

    // Border.
    let border_cell = Cell {
        fg: BORDER,
        bg: BG_SURFACE,
        ..Cell::default()
    };
    frame.draw_border(modal_rect, BorderChars::ROUNDED, border_cell);

    // Title.
    let title = " Command Palette ";
    let title_x = modal_x + (modal_width.saturating_sub(title.len() as u16)) / 2;
    let title_cell = Cell {
        fg: ACCENT,
        bg: BG_SURFACE,
        ..Cell::default()
    };
    frame.print_text_clipped(title_x, modal_y, title, title_cell, modal_rect.right());

    // Inner content area (inside border).
    let inner = Rect::new(
        modal_x + 2,
        modal_y + 1,
        modal_width.saturating_sub(4),
        modal_height.saturating_sub(2),
    );
    if inner.width < 4 || inner.height < 2 {
        return;
    }

    // --- Query input line ---
    let query_y = inner.y;
    let prompt = "> ";
    let prompt_cell = Cell {
        fg: ACCENT,
        bg: BG_SURFACE,
        ..Cell::default()
    };
    let query_start =
        frame.print_text_clipped(inner.x, query_y, prompt, prompt_cell, inner.right());

    let query_display = if state.query.is_empty() {
        "Type to filter..."
    } else {
        &state.query
    };
    let query_fg = if state.query.is_empty() {
        TEXT_MUTED
    } else {
        TEXT
    };
    let q_cell = Cell {
        fg: query_fg,
        bg: BG_SURFACE,
        ..Cell::default()
    };
    frame.print_text_clipped(query_start, query_y, query_display, q_cell, inner.right());

    // Cursor indicator (if query focused and not showing placeholder).
    if !state.query.is_empty() {
        let cursor_x = query_start.saturating_add(cursor_cell_offset(&state.query, state.cursor));
        if cursor_x < inner.right() {
            let cursor_cell = Cell {
                fg: BG_SURFACE,
                bg: TEXT,
                ..Cell::default()
            };
            // Draw cursor block. If at end of text, draw a space.
            let cursor_char = state
                .query
                .get(state.cursor..)
                .and_then(|s| s.chars().next())
                .unwrap_or(' ');
            frame.print_text_clipped(
                cursor_x,
                query_y,
                &cursor_char.to_string(),
                cursor_cell,
                inner.right(),
            );
        }
    }

    // --- Separator ---
    let sep_y = query_y + 1;
    if sep_y >= inner.bottom() {
        return;
    }
    let sep_cell = Cell {
        fg: BORDER,
        bg: BG_SURFACE,
        ..Cell::default()
    };
    let sep_line = "─".repeat(inner.width as usize);
    frame.print_text_clipped(inner.x, sep_y, &sep_line, sep_cell, inner.right());

    // --- Results list ---
    let list_y = sep_y + 1;
    let list_height = inner.bottom().saturating_sub(list_y) as usize;
    if list_height == 0 {
        return;
    }

    if state.filtered.is_empty() {
        let msg = if state.query.is_empty() {
            "No commands available"
        } else {
            "No matching commands"
        };
        let msg_cell = Cell {
            fg: TEXT_MUTED,
            bg: BG_SURFACE,
            ..Cell::default()
        };
        frame.print_text_clipped(inner.x, list_y, msg, msg_cell, inner.right());
        return;
    }

    // Scroll so the selected item is always visible.
    let scroll_offset = if state.selected_index >= list_height {
        state.selected_index - list_height + 1
    } else {
        0
    };

    let normal_cell = Cell {
        fg: TEXT,
        bg: BG_SURFACE,
        ..Cell::default()
    };
    let selected_cell = Cell {
        fg: BG_SURFACE,
        bg: ACCENT,
        ..Cell::default()
    };
    let key_cell = Cell {
        fg: TEXT_MUTED,
        bg: BG_SURFACE,
        ..Cell::default()
    };
    let key_selected_cell = Cell {
        fg: BG_SURFACE,
        bg: ACCENT,
        ..Cell::default()
    };

    for (i, entry) in state
        .filtered
        .iter()
        .skip(scroll_offset)
        .enumerate()
        .take(list_height)
    {
        let y = list_y + i as u16;
        let is_selected = i + scroll_offset == state.selected_index;

        let (label_style, kb_style) = if is_selected {
            (selected_cell, key_selected_cell)
        } else {
            (normal_cell, key_cell)
        };

        // Fill row background for selected item.
        if is_selected {
            for x in inner.x..inner.right() {
                frame.buffer.set(x, y, selected_cell);
            }
        }

        // Label (left-aligned).
        frame.print_text_clipped(inner.x, y, entry.label, label_style, inner.right());

        // Keybinding (right-aligned).
        if let Some(ref kb) = entry.keybinding {
            let kb_width = text_cell_width(kb);
            let kb_x = inner.right().saturating_sub(kb_width);
            if kb_x > inner.x + text_cell_width(entry.label).saturating_add(1) {
                frame.print_text_clipped(kb_x, y, kb, kb_style, inner.right());
            }
        }
    }

    // Scroll indicator.
    if state.filtered.len() > list_height {
        let indicator = format!(
            " {}/{} ",
            (scroll_offset + list_height).min(state.filtered.len()),
            state.filtered.len()
        );
        let ind_x = modal_rect
            .right()
            .saturating_sub(indicator.len() as u16 + 1);
        let ind_y = modal_rect.bottom().saturating_sub(1);
        let ind_cell = Cell {
            fg: TEXT_MUTED,
            bg: BG_SURFACE,
            ..Cell::default()
        };
        frame.print_text_clipped(ind_x, ind_y, &indicator, ind_cell, modal_rect.right());
    }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use crate::commands::build_registry;
    use crate::message::Screen;
    use crate::state::command_palette::CommandPaletteState;
    use ftui::render::grapheme_pool::GraphemePool;

    macro_rules! with_frame {
        ($width:expr, $height:expr, |$frame:ident| $body:block) => {{
            let mut pool = GraphemePool::new();
            let mut $frame = Frame::new($width, $height, &mut pool);
            $body
        }};
    }

    #[test]
    fn test_render_palette_closed_is_noop() {
        with_frame!(80, 24, |frame| {
            let state = CommandPaletteState::default();
            render_command_palette(&mut frame, &state, Rect::new(0, 0, 80, 24));
            // No content rendered when palette is closed.
        });
    }

    #[test]
    fn test_render_palette_open_no_panic() {
        with_frame!(80, 24, |frame| {
            let registry = build_registry();
            let mut state = CommandPaletteState::default();
            state.open(&registry, &Screen::Dashboard);
            render_command_palette(&mut frame, &state, Rect::new(0, 0, 80, 24));

            // Should have rendered content in center area.
            let has_content = (25..55u16).any(|x| {
                (8..16u16).any(|y| {
                    let cell = frame.buffer.get(x, y).unwrap();
                    !cell.is_empty()
                })
            });
            assert!(has_content, "Expected palette overlay in center area");
        });
    }

    #[test]
    fn test_render_palette_with_query() {
        with_frame!(80, 24, |frame| {
            let registry = build_registry();
            let mut state = CommandPaletteState::default();
            state.open(&registry, &Screen::Dashboard);
            state.insert_char('q', &registry, &Screen::Dashboard);
            render_command_palette(&mut frame, &state, Rect::new(0, 0, 80, 24));
        });
    }

    #[test]
    fn test_render_palette_unicode_cursor_uses_char_offset() {
        with_frame!(80, 24, |frame| {
            let registry = build_registry();
            let mut state = CommandPaletteState::default();
            state.open(&registry, &Screen::Dashboard);
            state.insert_char('é', &registry, &Screen::Dashboard);
            render_command_palette(&mut frame, &state, Rect::new(0, 0, 80, 24));

            let area = Rect::new(0, 0, 80, 24);
            let modal_width = (area.width * 3 / 5).clamp(30, 60);
            let modal_height = (area.height / 2).clamp(6, 20);
            let modal_x = area.x + (area.width.saturating_sub(modal_width)) / 2;
            let modal_y = area.y + (area.height.saturating_sub(modal_height)) / 2;
            let inner = Rect::new(
                modal_x + 2,
                modal_y + 1,
                modal_width.saturating_sub(4),
                modal_height.saturating_sub(2),
            );

            // Prompt "> " is two cells; one unicode scalar should place cursor at +1.
            let query_y = inner.y;
            let cursor_x = inner.x + 3;
            let cell = frame
                .buffer
                .get(cursor_x, query_y)
                .expect("cursor position must be in bounds");
            assert_eq!(cell.bg, TEXT);
        });
    }

    #[test]
    fn test_render_palette_with_selection() {
        with_frame!(80, 24, |frame| {
            let registry = build_registry();
            let mut state = CommandPaletteState::default();
            state.open(&registry, &Screen::Dashboard);
            state.select_next();
            state.select_next();
            render_command_palette(&mut frame, &state, Rect::new(0, 0, 80, 24));
        });
    }

    #[test]
    fn test_render_palette_tiny_terminal_noop() {
        with_frame!(15, 4, |frame| {
            let registry = build_registry();
            let mut state = CommandPaletteState::default();
            state.open(&registry, &Screen::Dashboard);
            render_command_palette(&mut frame, &state, Rect::new(0, 0, 15, 4));
        });
    }

    #[test]
    fn test_render_palette_no_results() {
        with_frame!(80, 24, |frame| {
            let registry = build_registry();
            let mut state = CommandPaletteState::default();
            state.open(&registry, &Screen::Dashboard);
            for c in "zzzzzz".chars() {
                state.insert_char(c, &registry, &Screen::Dashboard);
            }
            render_command_palette(&mut frame, &state, Rect::new(0, 0, 80, 24));
        });
    }
}
