//! Command palette do TUI (T-07): catálogo de comandos, filtro fuzzy e máquina
//! de estado do palette. Módulo autossuficiente — não depende de nenhum outro
//! módulo do crate. A integração `CommandId → Action` é feita por outra tarefa.

#![allow(dead_code)]

// ---------------------------------------------------------------------------
// Identificadores de comando
// ---------------------------------------------------------------------------

/// Identificador opaco de cada comando do palette. Desacoplado de `Action`
/// para que este módulo não dependa de `event.rs`.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum CommandId {
    NewOrchestration,
    OpenOrchestration,
    SwitchProvider,
    OpenStage,
    Approve,
    Regenerate,
    Edit,
    ToggleTelemetry,
    Help,
    Quit,
}

// ---------------------------------------------------------------------------
// Estrutura de comando e catálogo estático
// ---------------------------------------------------------------------------

/// Entrada do catálogo: identificador, rótulo visível em PT-BR e tecla de atalho.
pub struct Command {
    pub id: CommandId,
    pub label: &'static str,
    pub shortcut: &'static str,
}

/// Catálogo completo de comandos disponíveis no palette.
/// Ordem: fluxo principal → configuração → utilitários.
pub const COMMANDS: &[Command] = &[
    Command {
        id: CommandId::NewOrchestration,
        label: "Nova orquestração",
        shortcut: "n",
    },
    Command {
        id: CommandId::OpenOrchestration,
        label: "Abrir orquestração",
        shortcut: "Enter",
    },
    Command {
        id: CommandId::SwitchProvider,
        label: "Trocar provider/modelo",
        shortcut: "p",
    },
    Command {
        id: CommandId::OpenStage,
        label: "Abrir/executar etapa",
        shortcut: "Enter",
    },
    Command {
        id: CommandId::Approve,
        label: "Aprovar checkpoint",
        shortcut: "a",
    },
    Command {
        id: CommandId::Regenerate,
        label: "Regenerar via provider",
        shortcut: "r",
    },
    Command {
        id: CommandId::Edit,
        label: "Editar no $EDITOR",
        shortcut: "e",
    },
    Command {
        id: CommandId::ToggleTelemetry,
        label: "Telemetria",
        shortcut: "t",
    },
    Command {
        id: CommandId::Help,
        label: "Ajuda",
        shortcut: "?",
    },
    Command {
        id: CommandId::Quit,
        label: "Sair",
        shortcut: "q",
    },
];

// ---------------------------------------------------------------------------
// Filtro fuzzy
// ---------------------------------------------------------------------------

/// Classifica a qualidade do match de `query` contra `label` (ambos em lowercase).
/// Retorna `Some(grupo)` onde grupo 0 = prefixo exato, 1 = substring contígua,
/// 2 = subsequência espalhada; `None` se não casa.
fn match_quality(label_lower: &str, query_lower: &str) -> Option<u8> {
    if query_lower.is_empty() {
        // Query vazia: todos os itens pertencem ao grupo 0 (ordem original).
        return Some(0);
    }
    if label_lower.starts_with(query_lower) {
        return Some(0);
    }
    if label_lower.contains(query_lower) {
        return Some(1);
    }
    // Verifica subsequência espalhada: todos os chars de query aparecem em
    // ordem dentro de label.
    let mut chars = label_lower.chars();
    let matched = query_lower.chars().all(|qc| chars.any(|lc| lc == qc));
    if matched {
        return Some(2);
    }
    None
}

/// Retorna os índices em [`COMMANDS`] que casam com `query` por subsequência
/// case-insensitive, ordenados por qualidade:
///   - grupo 0: prefixo exato do label
///   - grupo 1: substring contígua
///   - grupo 2: subsequência espalhada
///
/// Dentro do mesmo grupo, a ordem original de `COMMANDS` é preservada.
/// Query vazia devolve todos os índices na ordem original.
/// Sem match devolve vetor vazio.
pub fn filter(query: &str) -> Vec<usize> {
    let query_lower = query.to_lowercase();

    // Coleta (índice, grupo) para os itens que casam.
    let mut matched: Vec<(usize, u8)> = COMMANDS
        .iter()
        .enumerate()
        .filter_map(|(i, cmd)| {
            let label_lower = cmd.label.to_lowercase();
            match_quality(&label_lower, &query_lower).map(|g| (i, g))
        })
        .collect();

    // Ordena por grupo; dentro do grupo, a posição original (índice) já está
    // em ordem crescente porque `enumerate` percorre na sequência de COMMANDS.
    matched.sort_by_key(|&(_, g)| g);

    matched.into_iter().map(|(i, _)| i).collect()
}

// ---------------------------------------------------------------------------
// Máquina de estado do palette
// ---------------------------------------------------------------------------

/// Estado do command palette: aberto/fechado, query atual e item selecionado.
#[derive(Clone, Debug, Default)]
pub struct CommandPalette {
    pub open: bool,
    pub query: String,
    pub selected: usize,
}

impl CommandPalette {
    /// Abre o palette, limpa query e volta o cursor ao primeiro item.
    pub fn open(&mut self) {
        self.open = true;
        self.query.clear();
        self.selected = 0;
    }

    /// Fecha o palette, limpa query e reseta cursor.
    pub fn close(&mut self) {
        self.open = false;
        self.query.clear();
        self.selected = 0;
    }

    /// Adiciona um caractere à query e faz clamp de `selected` ao novo tamanho
    /// da lista visível (evita cursor fora dos limites).
    pub fn push_char(&mut self, c: char) {
        self.query.push(c);
        self.clamp_selected();
    }

    /// Remove o último caractere da query e faz clamp de `selected`.
    pub fn backspace(&mut self) {
        self.query.pop();
        self.clamp_selected();
    }

    /// Move o cursor para cima (sem wrap; ignora se lista vazia).
    pub fn move_up(&mut self) {
        if self.selected > 0 {
            self.selected -= 1;
        }
    }

    /// Move o cursor para baixo (sem wrap; clampado ao último item).
    pub fn move_down(&mut self) {
        let len = self.visible().len();
        if len > 0 && self.selected + 1 < len {
            self.selected += 1;
        }
    }

    /// Retorna os índices dos comandos visíveis com a query atual.
    pub fn visible(&self) -> Vec<usize> {
        filter(&self.query)
    }

    /// Retorna o `CommandId` do item atualmente selecionado, ou `None` se a
    /// lista visível estiver vazia ou o índice for inválido.
    pub fn selected_command(&self) -> Option<CommandId> {
        let vis = self.visible();
        vis.get(self.selected).map(|&i| COMMANDS[i].id)
    }

    // --- utilitário interno ---

    /// Ajusta `selected` para que nunca ultrapasse o último índice válido.
    fn clamp_selected(&mut self) {
        let len = self.visible().len();
        if len == 0 {
            self.selected = 0;
        } else if self.selected >= len {
            self.selected = len - 1;
        }
    }
}

// ---------------------------------------------------------------------------
// Testes
// ---------------------------------------------------------------------------

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

    // Índice de cada variante no catálogo (para asserções legíveis).
    const IDX_NEW_ORCH: usize = 0; // "Nova orquestração"
    const IDX_OPEN_ORCH: usize = 1; // "Abrir orquestração"
    const IDX_SWITCH: usize = 2; // "Trocar provider/modelo"
    const IDX_OPEN_STAGE: usize = 3; // "Abrir/executar etapa"
    const IDX_APPROVE: usize = 4; // "Aprovar checkpoint"
    const IDX_REGEN: usize = 5; // "Regenerar via provider"
    const IDX_EDIT: usize = 6; // "Editar no $EDITOR"
    const IDX_TELEMETRY: usize = 7; // "Telemetria"
    const IDX_HELP: usize = 8; // "Ajuda"
    const IDX_QUIT: usize = 9; // "Sair"

    #[test]
    fn filter_vazio_retorna_todos() {
        let result = filter("");
        assert_eq!(result.len(), COMMANDS.len());
        // Deve preservar a ordem original quando query é vazia.
        let expected: Vec<usize> = (0..COMMANDS.len()).collect();
        assert_eq!(result, expected);
    }

    #[test]
    fn filter_nov_inclui_nova_orquestracao() {
        let result = filter("nov");
        assert!(
            result.contains(&IDX_NEW_ORCH),
            "\"nov\" deve casar \"Nova orquestração\" (idx {IDX_NEW_ORCH})"
        );
    }

    #[test]
    fn filter_zzz_retorna_vazio() {
        assert!(filter("zzz").is_empty());
    }

    #[test]
    fn ranking_prefixo_antes_de_subsequencia() {
        // "apr" é prefixo de "Aprovar checkpoint" (grupo 0)
        // e subsequência espalhada de "Abrir orquestração" (grupo 2, se casar).
        // Verifica apenas que "Aprovar" aparece antes de qualquer resultado de
        // grupo superior que também case "apr".
        let result = filter("apr");
        let pos_aprovar = result.iter().position(|&i| i == IDX_APPROVE);
        // "Aprovar checkpoint" deve aparecer (prefixo).
        assert!(
            pos_aprovar.is_some(),
            "\"Aprovar checkpoint\" deve casar \"apr\""
        );

        // Se "Abrir orquestração" também casar (subsequência), deve vir depois.
        if let Some(pos_abrir) = result.iter().position(|&i| i == IDX_OPEN_ORCH) {
            assert!(
                pos_aprovar.unwrap() < pos_abrir,
                "prefixo (aprovar) deve preceder subsequência (abrir) no ranking"
            );
        }
    }

    #[test]
    fn filter_case_insensitive() {
        // "NOVA" deve casar "Nova orquestração".
        let result = filter("NOVA");
        assert!(result.contains(&IDX_NEW_ORCH));
    }

    #[test]
    fn filter_substring_contigua() {
        // "provider" é substring de "Trocar provider/modelo" e "Regenerar via provider".
        let result = filter("provider");
        assert!(result.contains(&IDX_SWITCH));
        assert!(result.contains(&IDX_REGEN));
    }

    #[test]
    fn navegacao_clamp_nao_estoura() {
        let mut p = CommandPalette::default();
        p.open();

        // move_up no topo não faz nada.
        p.move_up();
        assert_eq!(p.selected, 0);

        // move_down até o fim.
        let total = COMMANDS.len();
        for _ in 0..total + 5 {
            p.move_down();
        }
        assert_eq!(p.selected, total - 1, "não deve ultrapassar o último item");

        // move_up voltando ao início.
        for _ in 0..total + 5 {
            p.move_up();
        }
        assert_eq!(p.selected, 0, "não deve ir abaixo de zero");
    }

    #[test]
    fn navegacao_lista_vazia_nao_entra_em_indice_invalido() {
        let mut p = CommandPalette::default();
        p.open();
        // Query que não casa nada.
        p.query = "zzz".to_string();
        assert_eq!(p.visible().len(), 0);

        p.move_down();
        assert_eq!(p.selected, 0);

        p.move_up();
        assert_eq!(p.selected, 0);

        assert!(p.selected_command().is_none());
    }

    #[test]
    fn push_char_e_backspace_atualizam_query_e_clampam() {
        let mut p = CommandPalette::default();
        p.open();

        // Vai até o último item.
        let total = COMMANDS.len();
        for _ in 0..total {
            p.move_down();
        }
        assert_eq!(p.selected, total - 1);

        // Push de char que filtra a lista para menos itens deve clampar selected.
        p.push_char('z');
        p.push_char('z');
        p.push_char('z');
        assert_eq!(p.query, "zzz");
        assert_eq!(p.visible().len(), 0);
        assert_eq!(p.selected, 0, "clamp para 0 quando lista vazia");

        // Backspace restaura query e a lista volta a ter itens.
        p.backspace();
        p.backspace();
        p.backspace();
        assert_eq!(p.query, "");
        assert_eq!(p.visible().len(), COMMANDS.len());
    }

    #[test]
    fn selected_command_coerente_apos_filtrar() {
        let mut p = CommandPalette::default();
        p.open();

        // Filtra por "nova" — deve ter ao menos "Nova orquestração".
        p.query = "nova".to_string();
        let vis = p.visible();
        assert!(!vis.is_empty());

        // O item selecionado (0) deve ser o primeiro da lista filtrada.
        let expected_id = COMMANDS[vis[0]].id;
        assert_eq!(p.selected_command(), Some(expected_id));
    }

    #[test]
    fn default_tem_open_false() {
        let p = CommandPalette::default();
        assert!(!p.open);
        assert!(p.query.is_empty());
        assert_eq!(p.selected, 0);
    }

    #[test]
    fn open_e_close_resetam_estado() {
        let mut p = CommandPalette::default();
        p.open();
        assert!(p.open);
        assert!(p.query.is_empty());
        assert_eq!(p.selected, 0);

        // Simula uso: digita e navega.
        p.push_char('a');
        p.move_down();

        p.close();
        assert!(!p.open);
        assert!(p.query.is_empty());
        assert_eq!(p.selected, 0);
    }

    #[test]
    fn todos_os_command_ids_representados_no_catalogo() {
        // Sanidade: garante que os índices constantes batem com o catálogo.
        assert_eq!(COMMANDS[IDX_NEW_ORCH].id, CommandId::NewOrchestration);
        assert_eq!(COMMANDS[IDX_OPEN_ORCH].id, CommandId::OpenOrchestration);
        assert_eq!(COMMANDS[IDX_SWITCH].id, CommandId::SwitchProvider);
        assert_eq!(COMMANDS[IDX_OPEN_STAGE].id, CommandId::OpenStage);
        assert_eq!(COMMANDS[IDX_APPROVE].id, CommandId::Approve);
        assert_eq!(COMMANDS[IDX_REGEN].id, CommandId::Regenerate);
        assert_eq!(COMMANDS[IDX_EDIT].id, CommandId::Edit);
        assert_eq!(COMMANDS[IDX_TELEMETRY].id, CommandId::ToggleTelemetry);
        assert_eq!(COMMANDS[IDX_HELP].id, CommandId::Help);
        assert_eq!(COMMANDS[IDX_QUIT].id, CommandId::Quit);
    }
}
