use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;

use crossterm::event::{KeyCode, KeyModifiers};

use crate::ui::app::AppMode;
use crate::ui::effect::Effect;

use super::cmd::{self, CmdContext, CmdRegistry};

/// Format a KeyCode as a short human-readable label for which-key display.
fn format_key_label(code: &KeyCode) -> String {
    match code {
        KeyCode::Char(c) => c.to_string(),
        KeyCode::Enter => "Enter".to_string(),
        KeyCode::Esc => "Esc".to_string(),
        KeyCode::Tab => "Tab".to_string(),
        KeyCode::BackTab => "S-Tab".to_string(),
        KeyCode::Backspace => "BS".to_string(),
        KeyCode::Delete => "Del".to_string(),
        KeyCode::Left => "←".to_string(),
        KeyCode::Right => "→".to_string(),
        KeyCode::Up => "↑".to_string(),
        KeyCode::Down => "↓".to_string(),
        KeyCode::Home => "Home".to_string(),
        KeyCode::End => "End".to_string(),
        KeyCode::PageUp => "PgUp".to_string(),
        KeyCode::PageDown => "PgDn".to_string(),
        KeyCode::F(n) => format!("F{n}"),
        _ => format!("{code:?}"),
    }
}
// `cmd` module imported for `default_registry()` in default_keymaps()

/// A key pattern that can be matched against a KeyEvent.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum KeyPattern {
    /// Single key with modifiers
    Key(KeyCode, KeyModifiers),
    /// Matches any Char key (for text-entry modes).
    AnyChar,
    /// Matches any key at all (lowest priority fallback).
    Any,
}

/// Identifies which mode a binding applies to.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ModeKey {
    Normal,
    Help,
    FormulaPanel,
    CategoryPanel,
    ViewPanel,
    TileSelect,
    Editing,
    FormulaEdit,
    CategoryAdd,
    ItemAdd,
    ExportPrompt,
    CommandMode,
    SearchMode,
    ImportWizard,
    RecordsNormal,
    RecordsEditing,
}

impl ModeKey {
    pub fn from_app_mode(mode: &AppMode, search_mode: bool) -> Option<Self> {
        match mode {
            AppMode::Normal if search_mode => Some(ModeKey::SearchMode),
            AppMode::Normal => Some(ModeKey::Normal),
            AppMode::Help => Some(ModeKey::Help),
            AppMode::FormulaPanel => Some(ModeKey::FormulaPanel),
            AppMode::CategoryPanel => Some(ModeKey::CategoryPanel),
            AppMode::ViewPanel => Some(ModeKey::ViewPanel),
            AppMode::TileSelect => Some(ModeKey::TileSelect),
            AppMode::Editing { .. } => Some(ModeKey::Editing),
            AppMode::FormulaEdit { .. } => Some(ModeKey::FormulaEdit),
            AppMode::CategoryAdd { .. } => Some(ModeKey::CategoryAdd),
            AppMode::ItemAdd { .. } => Some(ModeKey::ItemAdd),
            AppMode::ExportPrompt { .. } => Some(ModeKey::ExportPrompt),
            AppMode::CommandMode { .. } => Some(ModeKey::CommandMode),
            AppMode::ImportWizard => Some(ModeKey::ImportWizard),
            AppMode::RecordsNormal => Some(ModeKey::RecordsNormal),
            AppMode::RecordsEditing { .. } => Some(ModeKey::RecordsEditing),
            _ => None,
        }
    }
}

/// What a key binding resolves to.
#[derive(Debug, Clone)]
pub enum Binding {
    /// A command name + arguments, looked up in the registry at dispatch time.
    Cmd {
        name: &'static str,
        args: Vec<String>,
    },
    /// A prefix sub-keymap (Emacs-style).
    Prefix(Arc<Keymap>),
    /// A sequence of commands executed in order, concatenating their effects.
    Sequence(Vec<(&'static str, Vec<String>)>),
}

/// A keymap maps key patterns to bindings (command names or prefix sub-keymaps).
/// Supports Emacs-style parent keymap inheritance: if a key is not found in this
/// keymap, lookup falls through to the parent.
#[derive(Default)]
pub struct Keymap {
    bindings: HashMap<KeyPattern, Binding>,
    parent: Option<Arc<Keymap>>,
}

impl fmt::Debug for Keymap {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Keymap")
            .field("binding_count", &self.bindings.len())
            .finish()
    }
}

impl Keymap {
    pub fn new() -> Self {
        Self {
            bindings: HashMap::new(),
            parent: None,
        }
    }

    /// Create a keymap that inherits from a parent. Keys not found here
    /// fall through to the parent (Emacs-style keymap inheritance).
    pub fn with_parent(parent: Arc<Keymap>) -> Self {
        Self {
            bindings: HashMap::new(),
            parent: Some(parent),
        }
    }

    /// Bind a key to a command name (no args).
    pub fn bind(&mut self, key: KeyCode, mods: KeyModifiers, name: &'static str) {
        self.bindings.insert(
            KeyPattern::Key(key, mods),
            Binding::Cmd { name, args: vec![] },
        );
    }

    /// Bind a key to a command name with arguments.
    pub fn bind_args(
        &mut self,
        key: KeyCode,
        mods: KeyModifiers,
        name: &'static str,
        args: Vec<String>,
    ) {
        self.bindings
            .insert(KeyPattern::Key(key, mods), Binding::Cmd { name, args });
    }

    /// Bind a prefix key that activates a sub-keymap.
    pub fn bind_prefix(&mut self, key: KeyCode, mods: KeyModifiers, sub: Arc<Keymap>) {
        self.bindings
            .insert(KeyPattern::Key(key, mods), Binding::Prefix(sub));
    }

    /// Bind a key to a sequence of commands (executed in order).
    pub fn bind_seq(
        &mut self,
        key: KeyCode,
        mods: KeyModifiers,
        steps: Vec<(&'static str, Vec<String>)>,
    ) {
        self.bindings
            .insert(KeyPattern::Key(key, mods), Binding::Sequence(steps));
    }

    /// Bind a catch-all for any Char key.
    pub fn bind_any_char(&mut self, name: &'static str, args: Vec<String>) {
        self.bindings
            .insert(KeyPattern::AnyChar, Binding::Cmd { name, args });
    }

    /// Bind a catch-all for any key at all.
    pub fn bind_any(&mut self, name: &'static str) {
        self.bindings
            .insert(KeyPattern::Any, Binding::Cmd { name, args: vec![] });
    }

    /// Return human-readable hints for all concrete bindings in this keymap.
    /// Used by the which-key popup to show available completions after a prefix key.
    pub fn binding_hints(&self) -> Vec<(String, &'static str)> {
        let mut hints: Vec<(String, &'static str)> = self
            .bindings
            .iter()
            .filter_map(|(pattern, binding)| {
                let label = match pattern {
                    KeyPattern::Key(code, _) => format_key_label(code),
                    KeyPattern::AnyChar | KeyPattern::Any => return None,
                };
                let name = match binding {
                    Binding::Cmd { name, .. } => *name,
                    Binding::Prefix(_) => return None,
                    Binding::Sequence(steps) => {
                        if let Some((name, _)) = steps.first() {
                            *name
                        } else {
                            return None;
                        }
                    }
                };
                Some((label, name))
            })
            .collect();
        hints.sort_by(|a, b| a.0.cmp(&b.0));
        hints
    }

    /// Look up the binding for a key in this keymap's own bindings.
    fn lookup_local(&self, key: KeyCode, mods: KeyModifiers) -> Option<&Binding> {
        self.bindings
            .get(&KeyPattern::Key(key, mods))
            .or_else(|| {
                // Retry Char keys without modifiers (shift is implicit in the char)
                if matches!(key, KeyCode::Char(_)) && mods != KeyModifiers::NONE {
                    self.bindings.get(&KeyPattern::Key(key, KeyModifiers::NONE))
                } else {
                    None
                }
            })
            .or_else(|| {
                if matches!(key, KeyCode::Char(_)) {
                    self.bindings.get(&KeyPattern::AnyChar)
                } else {
                    None
                }
            })
            .or_else(|| self.bindings.get(&KeyPattern::Any))
    }

    /// Look up the binding for a key, falling through to parent keymaps.
    pub fn lookup(&self, key: KeyCode, mods: KeyModifiers) -> Option<&Binding> {
        self.lookup_local(key, mods)
            .or_else(|| self.parent.as_ref().and_then(|p| p.lookup(key, mods)))
    }

    /// Dispatch a key: look up binding, resolve through registry, return effects.
    pub fn dispatch(
        &self,
        registry: &CmdRegistry,
        ctx: &CmdContext,
        key: KeyCode,
        mods: KeyModifiers,
    ) -> Option<Vec<Box<dyn Effect>>> {
        let binding = self.lookup(key, mods)?;
        match binding {
            Binding::Cmd { name, args } => {
                let cmd = registry.interactive(name, args, ctx).ok()?;
                Some(cmd.execute(ctx))
            }
            Binding::Prefix(sub) => Some(vec![Box::new(SetTransientKeymap(sub.clone()))]),
            Binding::Sequence(steps) => {
                let mut effects: Vec<Box<dyn Effect>> = Vec::new();
                for (name, args) in steps {
                    let cmd = registry.interactive(name, args, ctx).ok()?;
                    effects.extend(cmd.execute(ctx));
                }
                Some(effects)
            }
        }
    }
}

/// Effect that sets the transient keymap on the App.
#[derive(Debug)]
pub struct SetTransientKeymap(pub Arc<Keymap>);

impl Effect for SetTransientKeymap {
    fn apply(&self, app: &mut crate::ui::app::App) {
        app.view_state.transient_keymap = Some(self.0.clone());
    }
}

/// Maps modes to their root keymaps + owns the command registry.
pub struct KeymapSet {
    mode_maps: HashMap<ModeKey, Arc<Keymap>>,
    registry: CmdRegistry,
}

impl KeymapSet {
    pub fn new(registry: CmdRegistry) -> Self {
        Self {
            mode_maps: HashMap::new(),
            registry,
        }
    }

    pub fn registry(&self) -> &CmdRegistry {
        &self.registry
    }

    pub fn insert(&mut self, mode: ModeKey, keymap: Arc<Keymap>) {
        self.mode_maps.insert(mode, keymap);
    }

    /// Dispatch a key event: returns effects if a binding matched.
    pub fn dispatch(
        &self,
        ctx: &CmdContext,
        key: KeyCode,
        mods: KeyModifiers,
    ) -> Option<Vec<Box<dyn Effect>>> {
        let mode_key = ModeKey::from_app_mode(ctx.mode, ctx.search_mode)?;
        let keymap = self.mode_maps.get(&mode_key)?;
        keymap.dispatch(&self.registry, ctx, key, mods)
    }

    /// Dispatch against a specific keymap (for transient/prefix keymaps).
    pub fn dispatch_transient(
        &self,
        keymap: &Keymap,
        ctx: &CmdContext,
        key: KeyCode,
        mods: KeyModifiers,
    ) -> Option<Vec<Box<dyn Effect>>> {
        keymap.dispatch(&self.registry, ctx, key, mods)
    }

    /// Build the default keymap set with all bindings.
    pub fn default_keymaps() -> Self {
        let registry = cmd::default_registry();
        let mut set = Self::new(registry);
        let none = KeyModifiers::NONE;
        let ctrl = KeyModifiers::CONTROL;

        // ── Normal mode ──────────────────────────────────────────────────
        let mut normal = Keymap::new();

        // Navigation
        for (key, dr, dc) in [
            (KeyCode::Up, -1, 0),
            (KeyCode::Down, 1, 0),
            (KeyCode::Left, 0, -1),
            (KeyCode::Right, 0, 1),
        ] {
            normal.bind_args(
                key,
                none,
                "move-selection",
                vec![dr.to_string(), dc.to_string()],
            );
        }
        for (ch, dr, dc) in [('k', -1, 0), ('j', 1, 0), ('h', 0, -1), ('l', 0, 1)] {
            normal.bind_args(
                KeyCode::Char(ch),
                none,
                "move-selection",
                vec![dr.to_string(), dc.to_string()],
            );
        }

        // Jump to boundaries
        normal.bind(KeyCode::Char('G'), none, "jump-last-row");
        normal.bind(KeyCode::Char('0'), none, "jump-first-col");
        normal.bind(KeyCode::Char('$'), none, "jump-last-col");
        normal.bind(KeyCode::Home, none, "jump-first-col");
        normal.bind(KeyCode::End, none, "jump-last-col");

        // Scroll
        normal.bind_args(KeyCode::Char('d'), ctrl, "scroll-rows", vec!["5".into()]);
        normal.bind_args(KeyCode::Char('u'), ctrl, "scroll-rows", vec!["-5".into()]);
        normal.bind_args(KeyCode::PageDown, none, "page-scroll", vec!["1".into()]);
        normal.bind_args(KeyCode::PageUp, none, "page-scroll", vec!["-1".into()]);

        // Cell operations
        normal.bind(KeyCode::Char('x'), none, "clear-cell");
        normal.bind(KeyCode::Char('p'), none, "paste");

        // View
        normal.bind(KeyCode::Char('t'), none, "transpose");

        // Mode changes
        normal.bind(KeyCode::Char('q'), ctrl, "force-quit");
        normal.bind_args(
            KeyCode::Char(':'),
            none,
            "enter-mode",
            vec!["command".into()],
        );
        normal.bind(KeyCode::Char('/'), none, "search");
        normal.bind(KeyCode::Char('s'), ctrl, "save");
        normal.bind(KeyCode::F(1), none, "enter-mode");
        normal.bind_args(KeyCode::F(1), none, "enter-mode", vec!["help".into()]);
        normal.bind_args(KeyCode::Char('?'), none, "enter-mode", vec!["help".into()]);

        // Panel toggles
        normal.bind_args(
            KeyCode::Char('F'),
            none,
            "toggle-panel-and-focus",
            vec!["formula".into()],
        );
        normal.bind_args(
            KeyCode::Char('C'),
            none,
            "toggle-panel-and-focus",
            vec!["category".into()],
        );
        normal.bind_args(
            KeyCode::Char('V'),
            none,
            "toggle-panel-and-focus",
            vec!["view".into()],
        );
        normal.bind_args(
            KeyCode::Char('f'),
            ctrl,
            "toggle-panel-visibility",
            vec!["formula".into()],
        );
        normal.bind_args(
            KeyCode::Char('c'),
            ctrl,
            "toggle-panel-visibility",
            vec!["category".into()],
        );
        normal.bind_args(
            KeyCode::Char('v'),
            ctrl,
            "toggle-panel-visibility",
            vec!["view".into()],
        );
        normal.bind(KeyCode::Tab, none, "cycle-panel-focus");

        // Editing entry — i/a drill into aggregated cells, else edit.
        // The mode arg controls which editing mode is entered; records-normal
        // overrides these to "records-editing" via its own bindings.
        normal.bind_args(
            KeyCode::Char('i'),
            none,
            "edit-or-drill",
            vec!["editing".into()],
        );
        normal.bind_args(
            KeyCode::Char('a'),
            none,
            "edit-or-drill",
            vec!["editing".into()],
        );
        normal.bind(KeyCode::Enter, none, "enter-advance");
        normal.bind(KeyCode::Char('e'), ctrl, "enter-export-prompt");

        // Search / category add
        normal.bind_args(
            KeyCode::Char('n'),
            none,
            "search-navigate",
            vec!["forward".into()],
        );
        normal.bind(KeyCode::Char('N'), none, "search-or-category-add");

        // Page navigation
        normal.bind(KeyCode::Char(']'), none, "page-next");
        normal.bind(KeyCode::Char('['), none, "page-prev");

        // Group / hide
        normal.bind(KeyCode::Char('z'), none, "toggle-group-under-cursor");
        normal.bind(KeyCode::Char('H'), none, "hide-selected-row-item");

        // Drill into aggregated cell / view history
        normal.bind(KeyCode::Char('>'), none, "drill-into-cell");
        normal.bind(KeyCode::Char('<'), none, "view-back");

        // Records mode toggle and prune toggle
        normal.bind(KeyCode::Char('R'), none, "toggle-records-mode");
        normal.bind(KeyCode::Char('P'), none, "toggle-prune-empty");

        // Tile select
        normal.bind(KeyCode::Char('T'), none, "enter-tile-select");
        normal.bind(KeyCode::Left, ctrl, "enter-tile-select");
        normal.bind(KeyCode::Right, ctrl, "enter-tile-select");
        normal.bind(KeyCode::Up, ctrl, "enter-tile-select");
        normal.bind(KeyCode::Down, ctrl, "enter-tile-select");

        // Prefix keys
        let mut g_map = Keymap::new();
        g_map.bind(KeyCode::Char('g'), none, "jump-first-row");
        g_map.bind(KeyCode::Char('z'), none, "toggle-col-group-under-cursor");
        normal.bind_prefix(KeyCode::Char('g'), none, Arc::new(g_map));

        let mut y_map = Keymap::new();
        y_map.bind(KeyCode::Char('y'), none, "yank");
        normal.bind_prefix(KeyCode::Char('y'), none, Arc::new(y_map));

        let mut z_map = Keymap::new();
        z_map.bind(KeyCode::Char('Z'), none, "wq");
        normal.bind_prefix(KeyCode::Char('Z'), none, Arc::new(z_map));

        let normal = Arc::new(normal);
        set.insert(ModeKey::Normal, normal.clone());

        // ── Records normal mode (inherits from normal) ────────────────────
        let mut rn = Keymap::with_parent(normal);
        // Override i/a so the edit branch produces records-editing mode
        // instead of inheriting the normal-mode "editing" arg.
        rn.bind_args(
            KeyCode::Char('i'),
            none,
            "edit-or-drill",
            vec!["records-editing".into()],
        );
        rn.bind_args(
            KeyCode::Char('a'),
            none,
            "edit-or-drill",
            vec!["records-editing".into()],
        );
        rn.bind_seq(
            KeyCode::Char('o'),
            none,
            vec![
                ("add-record-row", vec![]),
                ("enter-edit-at-cursor", vec!["records-editing".into()]),
            ],
        );
        set.insert(ModeKey::RecordsNormal, Arc::new(rn));

        // ── Help mode ────────────────────────────────────────────────────
        let mut help = Keymap::new();
        help.bind_args(KeyCode::Esc, none, "enter-mode", vec!["normal".into()]);
        help.bind_args(
            KeyCode::Char('q'),
            none,
            "enter-mode",
            vec!["normal".into()],
        );
        // Allow entering command mode from Help (important on empty-model launch)
        help.bind_args(
            KeyCode::Char(':'),
            none,
            "enter-mode",
            vec!["command".into()],
        );
        // Page navigation
        help.bind(KeyCode::Right, none, "help-page-next");
        help.bind(KeyCode::Char('l'), none, "help-page-next");
        help.bind(KeyCode::Char('n'), none, "help-page-next");
        help.bind(KeyCode::Tab, none, "help-page-next");
        help.bind(KeyCode::Left, none, "help-page-prev");
        help.bind(KeyCode::Char('h'), none, "help-page-prev");
        help.bind(KeyCode::Char('p'), none, "help-page-prev");
        help.bind(KeyCode::BackTab, none, "help-page-prev");
        set.insert(ModeKey::Help, Arc::new(help));

        // ── Formula panel ────────────────────────────────────────────────
        let mut fp = Keymap::new();
        fp.bind_args(KeyCode::Esc, none, "enter-mode", vec!["normal".into()]);
        fp.bind_args(KeyCode::Tab, none, "enter-mode", vec!["normal".into()]);
        for key in [KeyCode::Up, KeyCode::Char('k')] {
            fp.bind_args(
                key,
                none,
                "move-panel-cursor",
                vec!["formula".into(), "-1".into()],
            );
        }
        for key in [KeyCode::Down, KeyCode::Char('j')] {
            fp.bind_args(
                key,
                none,
                "move-panel-cursor",
                vec!["formula".into(), "1".into()],
            );
        }
        fp.bind(KeyCode::Char('a'), none, "enter-formula-edit");
        fp.bind(KeyCode::Char('n'), none, "enter-formula-edit");
        fp.bind(KeyCode::Char('o'), none, "enter-formula-edit");
        fp.bind(KeyCode::Char('d'), none, "delete-formula-at-cursor");
        fp.bind(KeyCode::Delete, none, "delete-formula-at-cursor");
        fp.bind_args(
            KeyCode::Char('F'),
            none,
            "toggle-panel-and-focus",
            vec!["formula".into()],
        );
        fp.bind_args(
            KeyCode::Char('C'),
            none,
            "toggle-panel-and-focus",
            vec!["category".into()],
        );
        fp.bind_args(
            KeyCode::Char('V'),
            none,
            "toggle-panel-and-focus",
            vec!["view".into()],
        );
        fp.bind_args(
            KeyCode::Char(':'),
            none,
            "enter-mode",
            vec!["command".into()],
        );
        set.insert(ModeKey::FormulaPanel, Arc::new(fp));

        // ── Category panel ───────────────────────────────────────────────
        let mut cp = Keymap::new();
        cp.bind_args(KeyCode::Esc, none, "enter-mode", vec!["normal".into()]);
        cp.bind_args(KeyCode::Tab, none, "enter-mode", vec!["normal".into()]);
        for key in [KeyCode::Up, KeyCode::Char('k')] {
            cp.bind_args(
                key,
                none,
                "move-panel-cursor",
                vec!["category".into(), "-1".into()],
            );
        }
        for key in [KeyCode::Down, KeyCode::Char('j')] {
            cp.bind_args(
                key,
                none,
                "move-panel-cursor",
                vec!["category".into(), "1".into()],
            );
        }
        cp.bind(KeyCode::Enter, none, "filter-to-item");
        cp.bind(KeyCode::Char(' '), none, "cycle-axis-at-cursor");
        cp.bind_args(
            KeyCode::Char('n'),
            none,
            "enter-mode",
            vec!["category-add".into()],
        );
        cp.bind(KeyCode::Char('a'), none, "open-item-add-at-cursor");
        cp.bind(KeyCode::Char('o'), none, "open-item-add-at-cursor");
        cp.bind(KeyCode::Char('d'), none, "delete-category-at-cursor");
        cp.bind(KeyCode::Delete, none, "delete-category-at-cursor");
        // C/F/V in panel modes: close panel (toggle-panel-and-focus sees focused=true)
        cp.bind_args(
            KeyCode::Char('C'),
            none,
            "toggle-panel-and-focus",
            vec!["category".into()],
        );
        cp.bind_args(
            KeyCode::Char('F'),
            none,
            "toggle-panel-and-focus",
            vec!["formula".into()],
        );
        cp.bind_args(
            KeyCode::Char('V'),
            none,
            "toggle-panel-and-focus",
            vec!["view".into()],
        );
        cp.bind_args(
            KeyCode::Char(':'),
            none,
            "enter-mode",
            vec!["command".into()],
        );
        set.insert(ModeKey::CategoryPanel, Arc::new(cp));

        // ── View panel ───────────────────────────────────────────────────
        let mut vp = Keymap::new();
        vp.bind_args(KeyCode::Esc, none, "enter-mode", vec!["normal".into()]);
        vp.bind_args(KeyCode::Tab, none, "enter-mode", vec!["normal".into()]);
        for key in [KeyCode::Up, KeyCode::Char('k')] {
            vp.bind_args(
                key,
                none,
                "move-panel-cursor",
                vec!["view".into(), "-1".into()],
            );
        }
        for key in [KeyCode::Down, KeyCode::Char('j')] {
            vp.bind_args(
                key,
                none,
                "move-panel-cursor",
                vec!["view".into(), "1".into()],
            );
        }
        vp.bind(KeyCode::Enter, none, "switch-view-at-cursor");
        vp.bind(KeyCode::Char('n'), none, "create-and-switch-view");
        vp.bind(KeyCode::Char('o'), none, "create-and-switch-view");
        vp.bind(KeyCode::Char('d'), none, "delete-view-at-cursor");
        vp.bind(KeyCode::Delete, none, "delete-view-at-cursor");
        vp.bind_args(
            KeyCode::Char('V'),
            none,
            "toggle-panel-and-focus",
            vec!["view".into()],
        );
        vp.bind_args(
            KeyCode::Char('C'),
            none,
            "toggle-panel-and-focus",
            vec!["category".into()],
        );
        vp.bind_args(
            KeyCode::Char('F'),
            none,
            "toggle-panel-and-focus",
            vec!["formula".into()],
        );
        vp.bind_args(
            KeyCode::Char(':'),
            none,
            "enter-mode",
            vec!["command".into()],
        );
        set.insert(ModeKey::ViewPanel, Arc::new(vp));

        // ── Tile select ──────────────────────────────────────────────────
        let mut ts = Keymap::new();
        ts.bind_args(KeyCode::Esc, none, "enter-mode", vec!["normal".into()]);
        ts.bind_args(KeyCode::Tab, none, "enter-mode", vec!["normal".into()]);
        ts.bind_args(KeyCode::Left, none, "move-tile-cursor", vec!["-1".into()]);
        ts.bind_args(
            KeyCode::Char('h'),
            none,
            "move-tile-cursor",
            vec!["-1".into()],
        );
        ts.bind_args(KeyCode::Right, none, "move-tile-cursor", vec!["1".into()]);
        ts.bind_args(
            KeyCode::Char('l'),
            none,
            "move-tile-cursor",
            vec!["1".into()],
        );
        ts.bind(KeyCode::Enter, none, "cycle-axis-for-tile");
        ts.bind(KeyCode::Char(' '), none, "cycle-axis-for-tile");
        ts.bind_args(
            KeyCode::Char('r'),
            none,
            "set-axis-for-tile",
            vec!["row".into()],
        );
        ts.bind_args(
            KeyCode::Char('c'),
            none,
            "set-axis-for-tile",
            vec!["column".into()],
        );
        ts.bind_args(
            KeyCode::Char('p'),
            none,
            "set-axis-for-tile",
            vec!["page".into()],
        );
        ts.bind_args(
            KeyCode::Char('n'),
            none,
            "set-axis-for-tile",
            vec!["none".into()],
        );
        ts.bind_args(
            KeyCode::Char(':'),
            none,
            "enter-mode",
            vec!["command".into()],
        );
        set.insert(ModeKey::TileSelect, Arc::new(ts));

        // ── Editing mode ─────────────────────────────────────────────────
        // commit-* takes the target edit-mode arg so the command stays
        // mode-agnostic; records-editing overrides Enter/Tab below.
        let mut ed = Keymap::new();
        ed.bind_seq(
            KeyCode::Esc,
            none,
            vec![
                ("clear-buffer", vec!["edit".into()]),
                ("enter-mode", vec!["normal".into()]),
            ],
        );
        ed.bind_seq(
            KeyCode::Enter,
            none,
            vec![
                ("commit-cell-edit", vec!["editing".into()]),
                ("clear-buffer", vec!["edit".into()]),
            ],
        );
        ed.bind_seq(
            KeyCode::Tab,
            none,
            vec![
                ("commit-and-advance-right", vec!["editing".into()]),
                ("clear-buffer", vec!["edit".into()]),
            ],
        );
        ed.bind_args(KeyCode::Backspace, none, "pop-char", vec!["edit".into()]);
        ed.bind_any_char("append-char", vec!["edit".into()]);
        let ed = Arc::new(ed);
        set.insert(ModeKey::Editing, ed.clone());

        // ── Records editing mode (inherits from editing) ──────────────────
        // Override Enter/Tab so the post-commit re-enter targets records-editing.
        let mut re = Keymap::with_parent(ed);
        re.bind_seq(
            KeyCode::Esc,
            none,
            vec![
                ("clear-buffer", vec!["edit".into()]),
                ("enter-mode", vec!["records-normal".into()]),
            ],
        );
        re.bind_seq(
            KeyCode::Enter,
            none,
            vec![
                ("commit-cell-edit", vec!["records-editing".into()]),
                ("clear-buffer", vec!["edit".into()]),
            ],
        );
        re.bind_seq(
            KeyCode::Tab,
            none,
            vec![
                ("commit-and-advance-right", vec!["records-editing".into()]),
                ("clear-buffer", vec!["edit".into()]),
            ],
        );
        set.insert(ModeKey::RecordsEditing, Arc::new(re));

        // ── Formula edit ─────────────────────────────────────────────────
        let mut fe = Keymap::new();
        fe.bind_seq(
            KeyCode::Esc,
            none,
            vec![
                ("clear-buffer", vec!["formula".into()]),
                ("enter-mode", vec!["formula-panel".into()]),
            ],
        );
        fe.bind_seq(
            KeyCode::Enter,
            none,
            vec![
                ("commit-formula", vec![]),
                ("clear-buffer", vec!["formula".into()]),
            ],
        );
        fe.bind_args(KeyCode::Backspace, none, "pop-char", vec!["formula".into()]);
        fe.bind_any_char("append-char", vec!["formula".into()]);
        set.insert(ModeKey::FormulaEdit, Arc::new(fe));

        // ── Category add ─────────────────────────────────────────────────
        let mut ca = Keymap::new();
        ca.bind_seq(
            KeyCode::Esc,
            none,
            vec![
                ("clear-buffer", vec!["category".into()]),
                ("enter-mode", vec!["category-panel".into()]),
            ],
        );
        ca.bind_seq(
            KeyCode::Enter,
            none,
            vec![
                ("commit-category-add", vec![]),
                ("clear-buffer", vec!["category".into()]),
            ],
        );
        ca.bind_seq(
            KeyCode::Tab,
            none,
            vec![
                ("commit-category-add", vec![]),
                ("clear-buffer", vec!["category".into()]),
            ],
        );
        ca.bind_args(
            KeyCode::Backspace,
            none,
            "pop-char",
            vec!["category".into()],
        );
        ca.bind_any_char("append-char", vec!["category".into()]);
        set.insert(ModeKey::CategoryAdd, Arc::new(ca));

        // ── Item add ─────────────────────────────────────────────────────
        let mut ia = Keymap::new();
        ia.bind_seq(
            KeyCode::Esc,
            none,
            vec![
                ("clear-buffer", vec!["item".into()]),
                ("enter-mode", vec!["category-panel".into()]),
            ],
        );
        ia.bind_seq(
            KeyCode::Enter,
            none,
            vec![
                ("commit-item-add", vec![]),
                ("clear-buffer", vec!["item".into()]),
            ],
        );
        ia.bind_seq(
            KeyCode::Tab,
            none,
            vec![
                ("commit-item-add", vec![]),
                ("clear-buffer", vec!["item".into()]),
            ],
        );
        ia.bind_args(KeyCode::Backspace, none, "pop-char", vec!["item".into()]);
        ia.bind_any_char("append-char", vec!["item".into()]);
        set.insert(ModeKey::ItemAdd, Arc::new(ia));

        // ── Export prompt ────────────────────────────────────────────────
        let mut ep = Keymap::new();
        ep.bind_seq(
            KeyCode::Esc,
            none,
            vec![
                ("clear-buffer", vec!["export".into()]),
                ("enter-mode", vec!["normal".into()]),
            ],
        );
        ep.bind_seq(
            KeyCode::Enter,
            none,
            vec![
                ("commit-export", vec![]),
                ("clear-buffer", vec!["export".into()]),
            ],
        );
        ep.bind_args(KeyCode::Backspace, none, "pop-char", vec!["export".into()]);
        ep.bind_any_char("append-char", vec!["export".into()]);
        set.insert(ModeKey::ExportPrompt, Arc::new(ep));

        // ── Command mode ─────────────────────────────────────────────────
        let mut cm = Keymap::new();
        cm.bind_seq(
            KeyCode::Esc,
            none,
            vec![
                ("clear-buffer", vec!["command".into()]),
                ("enter-mode", vec!["normal".into()]),
            ],
        );
        cm.bind_seq(
            KeyCode::Enter,
            none,
            vec![
                ("execute-command", vec![]),
                ("clear-buffer", vec!["command".into()]),
            ],
        );
        cm.bind(KeyCode::Backspace, none, "command-mode-backspace");
        cm.bind_any_char("append-char", vec!["command".into()]);
        set.insert(ModeKey::CommandMode, Arc::new(cm));

        // ── Search mode ──────────────────────────────────────────────────
        let mut sm = Keymap::new();
        sm.bind(KeyCode::Esc, none, "exit-search-mode");
        sm.bind(KeyCode::Enter, none, "exit-search-mode");
        sm.bind_args(KeyCode::Backspace, none, "pop-char", vec!["search".into()]);
        sm.bind_any_char("append-char", vec!["search".into()]);
        set.insert(ModeKey::SearchMode, Arc::new(sm));

        // ── Import wizard ────────────────────────────────────────────────
        let mut wiz = Keymap::new();
        wiz.bind_any("handle-wizard-key");
        set.insert(ModeKey::ImportWizard, Arc::new(wiz));

        set
    }
}

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

    // ── Keymap::lookup fallback chain ──────────────────────────────────

    #[test]
    fn lookup_exact_match() {
        let mut km = Keymap::new();
        km.bind(KeyCode::Char('a'), KeyModifiers::NONE, "cmd-a");
        let b = km.lookup(KeyCode::Char('a'), KeyModifiers::NONE);
        assert!(matches!(b, Some(Binding::Cmd { name: "cmd-a", .. })));
    }

    #[test]
    fn lookup_exact_with_ctrl() {
        let mut km = Keymap::new();
        km.bind(KeyCode::Char('s'), KeyModifiers::CONTROL, "save");
        let b = km.lookup(KeyCode::Char('s'), KeyModifiers::CONTROL);
        assert!(matches!(b, Some(Binding::Cmd { name: "save", .. })));
    }

    #[test]
    fn lookup_char_falls_back_to_none_mods() {
        // Bind 'A' with NONE. Lookup with SHIFT should still match because
        // terminals vary in whether they send SHIFT for uppercase chars.
        let mut km = Keymap::new();
        km.bind(KeyCode::Char('A'), KeyModifiers::NONE, "cmd-A");
        let b = km.lookup(KeyCode::Char('A'), KeyModifiers::SHIFT);
        assert!(matches!(b, Some(Binding::Cmd { name: "cmd-A", .. })));
    }

    #[test]
    fn lookup_non_char_does_not_fall_back_to_none_mods() {
        // Arrow keys with CTRL should NOT fall back to NONE when no match
        let mut km = Keymap::new();
        km.bind(KeyCode::Up, KeyModifiers::NONE, "move-up");
        let b = km.lookup(KeyCode::Up, KeyModifiers::CONTROL);
        // Should NOT match — no AnyChar or Any fallback registered
        assert!(b.is_none());
    }

    #[test]
    fn lookup_char_falls_to_any_char() {
        let mut km = Keymap::new();
        km.bind_any_char("append-char", vec![]);
        let b = km.lookup(KeyCode::Char('z'), KeyModifiers::NONE);
        assert!(matches!(
            b,
            Some(Binding::Cmd {
                name: "append-char",
                ..
            })
        ));
    }

    #[test]
    fn lookup_non_char_skips_any_char() {
        // AnyChar should only match Char keys, not Enter/Esc/arrows
        let mut km = Keymap::new();
        km.bind_any_char("append-char", vec![]);
        let b = km.lookup(KeyCode::Enter, KeyModifiers::NONE);
        assert!(b.is_none());
    }

    #[test]
    fn lookup_any_matches_everything() {
        let mut km = Keymap::new();
        km.bind_any("catchall");
        let b = km.lookup(KeyCode::F(12), KeyModifiers::NONE);
        assert!(matches!(
            b,
            Some(Binding::Cmd {
                name: "catchall",
                ..
            })
        ));
    }

    #[test]
    fn lookup_exact_takes_priority_over_any_char() {
        let mut km = Keymap::new();
        km.bind(KeyCode::Char('n'), KeyModifiers::NONE, "specific");
        km.bind_any_char("generic", vec![]);
        let b = km.lookup(KeyCode::Char('n'), KeyModifiers::NONE);
        assert!(matches!(
            b,
            Some(Binding::Cmd {
                name: "specific",
                ..
            })
        ));
    }

    #[test]
    fn lookup_any_char_takes_priority_over_any() {
        let mut km = Keymap::new();
        km.bind_any_char("char-catch", vec![]);
        km.bind_any("total-catch");
        let b = km.lookup(KeyCode::Char('x'), KeyModifiers::NONE);
        assert!(matches!(
            b,
            Some(Binding::Cmd {
                name: "char-catch",
                ..
            })
        ));
    }

    #[test]
    fn lookup_non_char_falls_to_any_not_any_char() {
        let mut km = Keymap::new();
        km.bind_any_char("char-catch", vec![]);
        km.bind_any("total-catch");
        let b = km.lookup(KeyCode::Esc, KeyModifiers::NONE);
        assert!(matches!(
            b,
            Some(Binding::Cmd {
                name: "total-catch",
                ..
            })
        ));
    }

    #[test]
    fn lookup_ctrl_char_with_only_none_binding_falls_through() {
        // Ctrl+S bound; plain 's' should NOT match Ctrl+S
        let mut km = Keymap::new();
        km.bind(KeyCode::Char('s'), KeyModifiers::CONTROL, "save");
        let b = km.lookup(KeyCode::Char('s'), KeyModifiers::NONE);
        assert!(b.is_none());
    }

    // ── ModeKey::from_app_mode ─────────────────────────────────────────

    #[test]
    fn mode_key_normal_no_search() {
        let mk = ModeKey::from_app_mode(&AppMode::Normal, false);
        assert_eq!(mk, Some(ModeKey::Normal));
    }

    #[test]
    fn mode_key_normal_with_search_overrides() {
        let mk = ModeKey::from_app_mode(&AppMode::Normal, true);
        assert_eq!(mk, Some(ModeKey::SearchMode));
    }

    #[test]
    fn mode_key_help() {
        let mk = ModeKey::from_app_mode(&AppMode::Help, false);
        assert_eq!(mk, Some(ModeKey::Help));
    }

    #[test]
    fn mode_key_quit_returns_none() {
        let mk = ModeKey::from_app_mode(&AppMode::Quit, false);
        assert_eq!(mk, None);
    }

    // ── Prefix bindings ────────────────────────────────────────────────

    #[test]
    fn lookup_returns_prefix_binding() {
        let mut km = Keymap::new();
        let sub = Arc::new(Keymap::new());
        km.bind_prefix(KeyCode::Char('g'), KeyModifiers::NONE, sub);
        let b = km.lookup(KeyCode::Char('g'), KeyModifiers::NONE);
        assert!(matches!(b, Some(Binding::Prefix(_))));
    }

    #[test]
    fn lookup_returns_sequence_binding() {
        let mut km = Keymap::new();
        km.bind_seq(
            KeyCode::Char('o'),
            KeyModifiers::NONE,
            vec![("step1", vec![]), ("step2", vec![])],
        );
        let b = km.lookup(KeyCode::Char('o'), KeyModifiers::NONE);
        assert!(matches!(b, Some(Binding::Sequence(steps)) if steps.len() == 2));
    }

    // ── default_keymaps smoke tests ────────────────────────────────────

    #[test]
    fn default_keymaps_has_all_modes() {
        let ks = KeymapSet::default_keymaps();
        let expected_modes = vec![
            ModeKey::Normal,
            ModeKey::Help,
            ModeKey::FormulaPanel,
            ModeKey::CategoryPanel,
            ModeKey::ViewPanel,
            ModeKey::TileSelect,
            ModeKey::Editing,
            ModeKey::FormulaEdit,
            ModeKey::CategoryAdd,
            ModeKey::ItemAdd,
            ModeKey::ExportPrompt,
            ModeKey::CommandMode,
            ModeKey::SearchMode,
            ModeKey::ImportWizard,
            ModeKey::RecordsNormal,
            ModeKey::RecordsEditing,
        ];
        for mode in &expected_modes {
            assert!(
                ks.mode_maps.contains_key(mode),
                "Missing keymap for mode {:?}",
                mode
            );
        }
    }

    #[test]
    fn normal_mode_has_basic_movement() {
        let ks = KeymapSet::default_keymaps();
        let normal = ks.mode_maps.get(&ModeKey::Normal).unwrap();
        // hjkl should be bound
        for key in ['h', 'j', 'k', 'l'] {
            assert!(
                normal
                    .lookup(KeyCode::Char(key), KeyModifiers::NONE)
                    .is_some(),
                "Normal mode missing binding for '{}'",
                key
            );
        }
        // Arrow keys
        for key in [KeyCode::Up, KeyCode::Down, KeyCode::Left, KeyCode::Right] {
            assert!(
                normal.lookup(key, KeyModifiers::NONE).is_some(),
                "Normal mode missing binding for {:?}",
                key
            );
        }
    }

    #[test]
    fn editing_mode_has_any_char_and_esc() {
        let ks = KeymapSet::default_keymaps();
        let editing = ks.mode_maps.get(&ModeKey::Editing).unwrap();
        // Should have AnyChar for text input
        assert!(
            editing
                .lookup(KeyCode::Char('z'), KeyModifiers::NONE)
                .is_some()
        );
        // Should have Esc to exit
        assert!(editing.lookup(KeyCode::Esc, KeyModifiers::NONE).is_some());
    }

    #[test]
    fn search_mode_has_any_char_and_esc() {
        let ks = KeymapSet::default_keymaps();
        let search = ks.mode_maps.get(&ModeKey::SearchMode).unwrap();
        assert!(
            search
                .lookup(KeyCode::Char('a'), KeyModifiers::NONE)
                .is_some()
        );
        assert!(search.lookup(KeyCode::Esc, KeyModifiers::NONE).is_some());
    }

    #[test]
    fn import_wizard_has_any_catchall() {
        let ks = KeymapSet::default_keymaps();
        let wiz = ks.mode_maps.get(&ModeKey::ImportWizard).unwrap();
        // Should catch any key at all
        assert!(wiz.lookup(KeyCode::F(5), KeyModifiers::NONE).is_some());
    }
}
