//! Command palette state and fuzzy matching.
//!
//! The command palette is a modal overlay (Ctrl+P) that provides fuzzy-match
//! access to all commands. Populated from [`CommandRegistry::palette_entries`].

use crate::commands::{CommandId, CommandRegistry};
use crate::message::Screen;

// ---------------------------------------------------------------------------
// PaletteEntry
// ---------------------------------------------------------------------------

/// A single entry in the filtered palette list.
#[derive(Debug, Clone)]
pub struct PaletteEntry {
    /// Command ID for execution.
    pub id: CommandId,
    /// Human-readable label.
    pub label: &'static str,
    /// Keybinding display string (e.g., "g i").
    pub keybinding: Option<String>,
    /// Help text / description.
    pub help_text: &'static str,
}

// ---------------------------------------------------------------------------
// CommandPaletteState
// ---------------------------------------------------------------------------

/// State for the command palette overlay.
#[derive(Debug, Default)]
pub struct CommandPaletteState {
    /// Current query text.
    pub query: String,
    /// Whether the query input is focused.
    pub query_focused: bool,
    /// Cursor position within the query string (byte offset).
    pub cursor: usize,
    /// Index of the currently selected entry in `filtered`.
    pub selected_index: usize,
    /// Filtered and scored palette entries.
    pub filtered: Vec<PaletteEntry>,
}

impl CommandPaletteState {
    /// Open the palette: reset query, focus input, populate with all commands.
    pub fn open(&mut self, registry: &CommandRegistry, screen: &Screen) {
        self.query.clear();
        self.cursor = 0;
        self.query_focused = true;
        self.selected_index = 0;
        self.refilter(registry, screen);
    }

    /// Close the palette: unfocus and clear state.
    pub fn close(&mut self) {
        self.query_focused = false;
        self.query.clear();
        self.cursor = 0;
        self.selected_index = 0;
        self.filtered.clear();
    }

    /// Insert a character at the cursor position.
    pub fn insert_char(&mut self, c: char, registry: &CommandRegistry, screen: &Screen) {
        self.query.insert(self.cursor, c);
        self.cursor += c.len_utf8();
        self.selected_index = 0;
        self.refilter(registry, screen);
    }

    /// Delete the character before the cursor.
    pub fn delete_back(&mut self, registry: &CommandRegistry, screen: &Screen) {
        if self.cursor > 0 {
            // Find the previous character boundary.
            let prev = self.query[..self.cursor]
                .char_indices()
                .next_back()
                .map_or(0, |(i, _)| i);
            self.query.drain(prev..self.cursor);
            self.cursor = prev;
            self.selected_index = 0;
            self.refilter(registry, screen);
        }
    }

    /// Move selection up by one.
    pub fn select_prev(&mut self) {
        self.selected_index = self.selected_index.saturating_sub(1);
    }

    /// Move selection down by one.
    pub fn select_next(&mut self) {
        if !self.filtered.is_empty() {
            self.selected_index = (self.selected_index + 1).min(self.filtered.len() - 1);
        }
    }

    /// Get the currently selected entry's command ID.
    #[must_use]
    pub fn selected_command_id(&self) -> Option<CommandId> {
        self.filtered.get(self.selected_index).map(|e| e.id)
    }

    /// Whether the palette is visible/active.
    #[must_use]
    pub fn is_open(&self) -> bool {
        self.query_focused
    }

    /// Recompute the filtered list from the registry.
    fn refilter(&mut self, registry: &CommandRegistry, screen: &Screen) {
        let entries = registry.palette_entries(screen);
        let query_lower = self.query.to_lowercase();

        self.filtered = entries
            .into_iter()
            .filter(|cmd| {
                if query_lower.is_empty() {
                    return true;
                }
                fuzzy_match(&query_lower, cmd.label) || fuzzy_match(&query_lower, cmd.help_text)
            })
            .map(|cmd| PaletteEntry {
                id: cmd.id,
                label: cmd.label,
                keybinding: cmd.keybinding.as_ref().map(|kb| kb.display()),
                help_text: cmd.help_text,
            })
            .collect();

        // Clamp selection.
        if !self.filtered.is_empty() {
            self.selected_index = self.selected_index.min(self.filtered.len() - 1);
        } else {
            self.selected_index = 0;
        }
    }
}

// ---------------------------------------------------------------------------
// Fuzzy matching
// ---------------------------------------------------------------------------

/// Subsequence fuzzy match: every character in `query` must appear in `text`
/// in order, case-insensitive.
fn fuzzy_match(query: &str, text: &str) -> bool {
    let text_lower = text.to_lowercase();
    let mut text_chars = text_lower.chars();
    for qc in query.chars() {
        if !text_chars.any(|tc| tc == qc) {
            return false;
        }
    }
    true
}

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

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

    #[test]
    fn test_fuzzy_match_exact() {
        assert!(fuzzy_match("quit", "Quit"));
    }

    #[test]
    fn test_fuzzy_match_subsequence() {
        assert!(fuzzy_match("gi", "Go to Issues"));
        assert!(fuzzy_match("iss", "Go to Issues"));
    }

    #[test]
    fn test_fuzzy_match_case_insensitive() {
        assert!(fuzzy_match("help", "Show keybinding help overlay"));
    }

    #[test]
    fn test_fuzzy_match_no_match() {
        assert!(!fuzzy_match("xyz", "Quit"));
    }

    #[test]
    fn test_fuzzy_match_empty_query() {
        assert!(fuzzy_match("", "anything"));
    }

    #[test]
    fn test_palette_open_populates_all() {
        let registry = build_registry();
        let mut state = CommandPaletteState::default();
        state.open(&registry, &Screen::Dashboard);

        assert!(state.query_focused);
        assert!(state.query.is_empty());
        assert!(!state.filtered.is_empty());
        // All palette-eligible commands for Dashboard should be present.
        let palette_count = registry.palette_entries(&Screen::Dashboard).len();
        assert_eq!(state.filtered.len(), palette_count);
    }

    #[test]
    fn test_palette_filter_narrows_results() {
        let registry = build_registry();
        let mut state = CommandPaletteState::default();
        state.open(&registry, &Screen::Dashboard);

        let all_count = state.filtered.len();
        state.insert_char('i', &registry, &Screen::Dashboard);
        state.insert_char('s', &registry, &Screen::Dashboard);
        state.insert_char('s', &registry, &Screen::Dashboard);

        // "iss" should match "Go to Issues" but not most other commands.
        assert!(state.filtered.len() < all_count);
        assert!(state.filtered.iter().any(|e| e.label == "Go to Issues"));
    }

    #[test]
    fn test_palette_delete_back_widens_results() {
        let registry = build_registry();
        let mut state = CommandPaletteState::default();
        state.open(&registry, &Screen::Dashboard);

        state.insert_char('q', &registry, &Screen::Dashboard);
        let narrow_count = state.filtered.len();
        state.delete_back(&registry, &Screen::Dashboard);
        // After deleting, query is empty — should show all commands again.
        assert!(state.filtered.len() > narrow_count);
    }

    #[test]
    fn test_palette_select_navigation() {
        let registry = build_registry();
        let mut state = CommandPaletteState::default();
        state.open(&registry, &Screen::Dashboard);

        assert_eq!(state.selected_index, 0);
        state.select_next();
        assert_eq!(state.selected_index, 1);
        state.select_next();
        assert_eq!(state.selected_index, 2);
        state.select_prev();
        assert_eq!(state.selected_index, 1);
        state.select_prev();
        assert_eq!(state.selected_index, 0);
        state.select_prev(); // Should not go below 0.
        assert_eq!(state.selected_index, 0);
    }

    #[test]
    fn test_palette_selected_command_id() {
        let registry = build_registry();
        let mut state = CommandPaletteState::default();
        state.open(&registry, &Screen::Dashboard);

        assert!(state.selected_command_id().is_some());
    }

    #[test]
    fn test_palette_close_resets() {
        let registry = build_registry();
        let mut state = CommandPaletteState::default();
        state.open(&registry, &Screen::Dashboard);
        state.insert_char('q', &registry, &Screen::Dashboard);
        state.select_next();

        state.close();
        assert!(!state.query_focused);
        assert!(state.query.is_empty());
        assert_eq!(state.selected_index, 0);
        assert!(state.filtered.is_empty());
    }

    #[test]
    fn test_palette_empty_query_no_match_returns_empty() {
        let registry = build_registry();
        let mut state = CommandPaletteState::default();
        state.open(&registry, &Screen::Dashboard);

        // Type something that matches nothing.
        for c in "zzzzzz".chars() {
            state.insert_char(c, &registry, &Screen::Dashboard);
        }
        assert!(state.filtered.is_empty());
        assert!(state.selected_command_id().is_none());
    }

    #[test]
    fn test_palette_keybinding_display() {
        let registry = build_registry();
        let mut state = CommandPaletteState::default();
        state.open(&registry, &Screen::Dashboard);

        // "Quit" should have keybinding "q".
        let quit_entry = state.filtered.iter().find(|e| e.id == "quit");
        assert!(quit_entry.is_some());
        assert_eq!(quit_entry.unwrap().keybinding.as_deref(), Some("q"));
    }
}
