use std::collections::BTreeMap;

use ratatui::Frame;
use ratatui::crossterm::event::KeyCode;
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::widgets::Paragraph;

use crate::components::Component;
use crate::components::event_state::EventState;
use crate::components::events::{AppEvent, AppTx, InputEvent};
use crate::components::panel::{ModalSpec, modal_chrome};
use crate::keys::KeyBindings;
use crate::keys::action_shortcuts::ShortcutCategory;
use crate::settings::themes::Theme;

// ---------------------------------------------------------------------------
// HelpRow
// ---------------------------------------------------------------------------

pub enum HelpRow {
    Header(String),
    Separator,
    Binding { keys: String, label: String },
    Blank,
}

// ---------------------------------------------------------------------------
// HelpDialog
// ---------------------------------------------------------------------------

pub struct HelpDialog {
    pub rows: Vec<HelpRow>,
    /// Window title — distinguishes the flat F1 help from the leader-tree
    /// cheatsheet, which share this widget.
    title: &'static str,
    scroll: usize,
    /// Cached body height from last render, used for PageUp/PageDown page size.
    last_body_height: u16,
}

impl HelpDialog {
    pub fn new(key_bindings: &KeyBindings) -> Self {
        let mut by_category: BTreeMap<ShortcutCategory, Vec<(String, String)>> = BTreeMap::new();

        let map = key_bindings.to_hashmap();
        let mut entries: Vec<_> = map.into_iter().collect();
        entries.sort_by_key(|(action, _)| action.to_string());

        for (action, mut combos) in entries {
            combos.sort();
            let keys = combos
                .iter()
                .map(|c| c.to_string())
                .collect::<Vec<_>>()
                .join(" / ");
            let label = action.label();
            by_category
                .entry(action.category())
                .or_default()
                .push((keys, label));
        }

        let mut rows: Vec<HelpRow> = Vec::new();
        for (category, bindings) in by_category {
            if bindings.is_empty() {
                continue;
            }
            rows.push(HelpRow::Blank);
            rows.push(HelpRow::Header(category.to_string()));
            rows.push(HelpRow::Separator);
            for (keys, label) in bindings {
                rows.push(HelpRow::Binding { keys, label });
            }
        }
        rows.push(HelpRow::Blank);

        Self {
            rows,
            title: " Keyboard Shortcuts ",
            scroll: 0,
            last_body_height: 20,
        }
    }

    /// The full leader-tree cheatsheet (leader `?`): every sequence in the
    /// tree as `gateway keys → description`, grouped per top-level group,
    /// followed by the flat Tier-0 bindings. Built from the same
    /// `leader_tree()` the engine and the which-key overlay walk — one
    /// source, three surfaces.
    pub fn cheatsheet(settings: &crate::settings::AppSettings) -> Self {
        use crate::keys::action_shortcuts::ActionShortcuts;
        use crate::keys::leader::LeaderNode;

        let key_bindings = &settings.key_bindings;
        let gateway = key_bindings
            .first_combo_for(&ActionShortcuts::Leader)
            .unwrap_or_else(|| "leader".to_string());

        fn walk(node: &LeaderNode, prefix: &str, rows: &mut Vec<HelpRow>) {
            for (key, child) in node.children() {
                let keys = format!("{prefix} {key}");
                match child {
                    LeaderNode::Leaf { label, .. } => rows.push(HelpRow::Binding {
                        keys,
                        label: (*label).to_string(),
                    }),
                    LeaderNode::Group { .. } => walk(child, &keys, rows),
                }
            }
        }

        let tree = settings.leader_tree();
        let mut rows: Vec<HelpRow> = Vec::new();
        // Current configuration up top (spec phase-10: surface theme + keys).
        rows.push(HelpRow::Header("Configuration".to_string()));
        rows.push(HelpRow::Separator);
        rows.push(HelpRow::Binding {
            keys: settings.get_theme().name,
            label: "active theme (leader v c to switch)".to_string(),
        });
        rows.push(HelpRow::Binding {
            keys: gateway.clone(),
            label: "leader gateway".to_string(),
        });
        rows.push(HelpRow::Binding {
            keys: format!("{} ms", settings.leader_timeout_ms),
            label: "which-key reveal timeout".to_string(),
        });
        rows.push(HelpRow::Binding {
            keys: "F1 in Find".to_string(),
            label: "search query syntax".to_string(),
        });
        for (key, child) in tree.children() {
            match child {
                LeaderNode::Group { label, .. } => {
                    rows.push(HelpRow::Blank);
                    rows.push(HelpRow::Header(format!("{gateway} {key}  {label}")));
                    rows.push(HelpRow::Separator);
                    walk(child, &format!("{gateway} {key}"), &mut rows);
                }
                LeaderNode::Leaf { label, .. } => {
                    rows.push(HelpRow::Blank);
                    rows.push(HelpRow::Binding {
                        keys: format!("{gateway} {key}"),
                        label: (*label).to_string(),
                    });
                }
            }
        }

        // Tier-0: the flat always-on bindings, from the same help builder.
        let flat = Self::new(key_bindings);
        rows.push(HelpRow::Blank);
        rows.push(HelpRow::Header("Always-on shortcuts".to_string()));
        rows.push(HelpRow::Separator);
        rows.extend(
            flat.rows
                .into_iter()
                .filter(|r| matches!(r, HelpRow::Binding { .. })),
        );
        rows.push(HelpRow::Blank);

        Self {
            rows,
            title: " Cheatsheet — leader keys ",
            scroll: 0,
            last_body_height: 20,
        }
    }

    /// Reference card for the search query language (F1 over the Find drawer
    /// view). Operators, modifiers, and a few worked examples — mirrors the
    /// canonical table in `docs/.../search.md` and ADR-0005, condensed to the
    /// keys|label shape the help widget already renders. The full prose guide
    /// lives on the docs site, not here.
    pub fn query_syntax() -> Self {
        // (short / long, meaning) — sourced from search.md's operator table.
        const OPERATORS: &[(&str, &str)] = &[
            ("(type text)", "full-text body search"),
            ("= / name:", "by note name"),
            ("@ / in:", "by section heading"),
            ("/ / pt:", "by path / folder"),
            ("# / lb:", "by label (tag)"),
            ("< / lk:", "links TO it (backlinks)"),
            ("> / fwd:", "it links to (forward)"),
            ("^ / or:", "sort results (-^ = desc)"),
        ];
        const MODIFIERS: &[(&str, &str)] = &[
            ("- prefix", "exclude (e.g. -#draft)"),
            ("*", "wildcard prefix (screen*)"),
            ("\" \"", "quote values with spaces"),
        ];
        const EXAMPLES: &[(&str, &str)] = &[
            ("#finance report", "labelled finance + text"),
            ("@work -cancelled", "Work section, not cancelled"),
            ("<kimun #project", "kimun backlinks + label"),
        ];

        fn section(rows: &mut Vec<HelpRow>, header: &str, entries: &[(&str, &str)]) {
            rows.push(HelpRow::Header(header.to_string()));
            rows.push(HelpRow::Separator);
            for (keys, label) in entries {
                rows.push(HelpRow::Binding {
                    keys: (*keys).to_string(),
                    label: (*label).to_string(),
                });
            }
            rows.push(HelpRow::Blank);
        }

        let mut rows: Vec<HelpRow> = Vec::new();
        section(&mut rows, "Operators", OPERATORS);
        section(&mut rows, "Modifiers", MODIFIERS);
        section(&mut rows, "Examples", EXAMPLES);

        Self {
            rows,
            title: " Search Query Syntax ",
            scroll: 0,
            last_body_height: 20,
        }
    }

    fn scroll_up(&mut self) {
        self.scroll = self.scroll.saturating_sub(1);
    }

    fn scroll_down(&mut self) {
        // Clamped to rows.len() so render's slice is always valid even if called
        // between renders.
        self.scroll = self
            .scroll
            .saturating_add(1)
            .min(self.rows.len().saturating_sub(1));
    }

    fn page_up(&mut self) {
        let page = (self.last_body_height as usize).max(1);
        self.scroll = self.scroll.saturating_sub(page);
    }

    fn page_down(&mut self) {
        let page = (self.last_body_height as usize).max(1);
        self.scroll = self
            .scroll
            .saturating_add(page)
            .min(self.rows.len().saturating_sub(1));
    }

    /// Key handler — mirrors the `handle_key` pattern used by all other dialog types.
    pub fn handle_key(
        &mut self,
        key: ratatui::crossterm::event::KeyEvent,
        tx: &AppTx,
    ) -> EventState {
        match key.code {
            KeyCode::Esc => {
                tx.send(AppEvent::CloseOverlay).ok();
            }
            KeyCode::Up => self.scroll_up(),
            KeyCode::Down => self.scroll_down(),
            KeyCode::PageUp => self.page_up(),
            KeyCode::PageDown => self.page_down(),
            _ => {}
        }
        EventState::Consumed
    }
}

const OUTER_WIDTH: u16 = 50;
const KEYS_COL_WIDTH: u16 = 18;

impl Component for HelpDialog {
    fn handle_input(&mut self, event: &InputEvent, tx: &AppTx) -> EventState {
        let InputEvent::Key(key) = event else {
            return EventState::NotConsumed;
        };
        self.handle_key(*key, tx)
    }

    fn render(&mut self, f: &mut Frame, rect: Rect, theme: &Theme, _focused: bool) {
        let content_rows = self.rows.len() as u16;
        let desired_height = content_rows + 4; // borders(2) + footer(1) + bottom blank(1)
        let max_height = (rect.height * 60 / 100).max(10);
        let outer_height = desired_height.min(max_height);

        let popup_area = super::fixed_centered_rect(OUTER_WIDTH, outer_height, rect);
        let inner = modal_chrome(
            f,
            popup_area,
            theme,
            ModalSpec {
                title: Some(self.title),
                border: Some(Style::default().fg(theme.fg.to_ratatui())),
                ..Default::default()
            },
        );

        if inner.height < 2 {
            return;
        }

        let chunks = Layout::default()
            .direction(Direction::Vertical)
            .constraints([Constraint::Min(1), Constraint::Length(1)])
            .split(inner);

        let body_area = chunks[0];
        let footer_area = chunks[1];

        let bg = theme.bg_panel.to_ratatui();
        let fg = theme.fg.to_ratatui();
        let gray = theme.gray.to_ratatui();
        let fg_accent = theme.selection_fg.to_ratatui();

        // Cache for PageUp/PageDown.
        self.last_body_height = body_area.height;

        // Clamp scroll.
        let body_height = body_area.height as usize;
        let max_scroll = self.rows.len().saturating_sub(body_height);
        self.scroll = self.scroll.min(max_scroll);

        // Render visible rows.
        let visible = &self.rows[self.scroll..];
        for (y, row) in (body_area.y..).zip(visible.iter()) {
            if y >= body_area.y + body_area.height {
                break;
            }
            let row_rect = Rect {
                x: body_area.x,
                y,
                width: body_area.width,
                height: 1,
            };
            match row {
                HelpRow::Blank => {}
                HelpRow::Header(title) => {
                    f.render_widget(
                        Paragraph::new(format!("  {title}")).style(
                            Style::default()
                                .fg(fg_accent)
                                .bg(bg)
                                .add_modifier(Modifier::BOLD),
                        ),
                        row_rect,
                    );
                }
                HelpRow::Separator => {
                    super::render_separator(f, row_rect, gray, bg);
                }
                HelpRow::Binding { keys, label } => {
                    let cols = Layout::default()
                        .direction(Direction::Horizontal)
                        .constraints([
                            Constraint::Length(2),
                            Constraint::Length(KEYS_COL_WIDTH),
                            Constraint::Min(1),
                        ])
                        .split(row_rect);
                    f.render_widget(
                        Paragraph::new(keys.as_str()).style(Style::default().fg(fg_accent).bg(bg)),
                        cols[1],
                    );
                    f.render_widget(
                        Paragraph::new(label.as_str()).style(Style::default().fg(fg).bg(bg)),
                        cols[2],
                    );
                }
            }
        }

        f.render_widget(
            Paragraph::new("  [↑↓ PgUp/PgDn] Scroll   [Esc] Close")
                .style(Style::default().fg(gray).bg(bg)),
            footer_area,
        );
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::keys::KeyBindings;
    use crate::keys::action_shortcuts::{ActionShortcuts, TextAction};
    use crate::keys::key_strike::KeyStrike;

    fn bindings_with_bold_and_quit() -> KeyBindings {
        let mut kb = KeyBindings::empty();
        kb.batch_add()
            .with_ctrl()
            .add(KeyStrike::KeyB, ActionShortcuts::Text(TextAction::Bold))
            .add(KeyStrike::KeyQ, ActionShortcuts::Quit);
        kb
    }

    #[test]
    fn rows_contain_both_categories() {
        let dialog = HelpDialog::new(&bindings_with_bold_and_quit());
        let headers: Vec<String> = dialog
            .rows
            .iter()
            .filter_map(|r| {
                if let HelpRow::Header(s) = r {
                    Some(s.clone())
                } else {
                    None
                }
            })
            .collect();
        assert!(headers.contains(&"Text Editing".to_string()));
        assert!(headers.contains(&"Other".to_string()));
        assert!(!headers.contains(&"Navigation".to_string()));
        assert!(!headers.contains(&"Notes".to_string()));
    }

    #[test]
    fn binding_row_has_correct_keys_and_label() {
        let dialog = HelpDialog::new(&bindings_with_bold_and_quit());
        let binding = dialog.rows.iter().find_map(|r| {
            if let HelpRow::Binding { keys, label } = r
                && label == "Bold"
            {
                return Some(keys.clone());
            }
            None
        });
        assert!(binding.is_some(), "expected a Bold binding row");
        assert_eq!(binding.unwrap(), "ctrl&B");
    }

    #[test]
    fn empty_keybindings_produces_no_rows() {
        let dialog = HelpDialog::new(&KeyBindings::empty());
        assert!(
            !dialog
                .rows
                .iter()
                .any(|r| matches!(r, HelpRow::Binding { .. }))
        );
        assert!(!dialog.rows.iter().any(|r| matches!(r, HelpRow::Header(_))));
    }
}
