use std::{
    collections::HashSet,
    path::PathBuf,
    time::{Duration, Instant},
};

use anyhow::{Context, Result, bail};
use chrono::{Local, NaiveDate};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
use ratatui::layout::Rect;

use crate::{
    db::Database,
    filter,
    macro_lang::{
        MacroAction, MacroContext, MacroRuntime, MenuChoice, PromptKind, key_to_source, macro_name,
        parse_key_binding,
    },
    model::{
        Category, CategoryKind, DateOrder, DocumentSettings, DonePolicy, Item, ItemChanges,
        MacroDef, TrashPolicy, ViewDef, ViewKind, WeekStart,
    },
    preferences::AppPreferences,
};

mod editor;
mod forms;

use editor::{
    delete_at_cursor, delete_before_cursor, insert_at_cursor, insert_str_at_cursor, line_end,
    line_start, move_cursor_vertically, next_boundary, previous_boundary,
};
pub(crate) use forms::form_choices;
use forms::{decode_space, display_space, form_prompt, parse_yes, parse_yes_no, yes_no};

#[derive(Debug, Clone)]
pub enum InputKind {
    NewItem,
    EditItem(i64),
    Note(i64),
    Search,
    MacroEditor(Option<i64>),
    MacroBinding(i64),
    MacroPrompt(String),
    CategoryValue {
        item_id: i64,
        category_id: i64,
        kind: CategoryKind,
    },
}

#[derive(Debug, Clone)]
pub struct InputState {
    pub kind: InputKind,
    pub title: String,
    pub value: String,
    pub multiline: bool,
    /// UTF-8 byte offset kept on a character boundary.
    pub cursor: usize,
}

#[derive(Debug, Clone)]
pub struct PropsState {
    pub item_id: i64,
    pub values: [String; 4],
    pub field: usize,
}

#[derive(Debug, Clone)]
pub enum FormKind {
    Category(Option<i64>),
    View(Option<i64>),
    Preferences,
    DocumentSettings,
}

#[derive(Debug, Clone)]
pub struct FormState {
    pub kind: FormKind,
    pub values: Vec<String>,
    pub field: usize,
}

#[derive(Debug, Clone)]
pub struct ChoiceOption {
    pub label: String,
    pub value: String,
}

#[derive(Debug, Clone)]
pub struct ChoiceState {
    pub form: FormState,
    pub options: Vec<ChoiceOption>,
    pub selected: usize,
}

#[derive(Debug, Clone)]
pub enum Mode {
    Normal,
    Help,
    Input(InputState),
    Properties(PropsState),
    Form(FormState),
    Dependencies {
        item_id: i64,
        choices: Vec<Item>,
        selected: usize,
    },
    Categories,
    Views,
    CategoryManager,
    Menu,
    Execute {
        selected: usize,
    },
    RuleConflicts,
    Confirm {
        prompt: String,
        action: ConfirmAction,
    },
    Choice(ChoiceState),
    MacroManager,
    MacroMenu {
        title: String,
        prompt: String,
        choices: Vec<MenuChoice>,
        selected: usize,
    },
    MacroMessage {
        title: String,
        message: String,
    },
    MacroGetKey {
        variable: String,
    },
}

#[derive(Debug, Clone)]
pub struct RecordingState {
    pub macro_id: i64,
    pub name: String,
    pub source: String,
}

#[derive(Debug, Clone)]
pub enum ConfirmAction {
    DeleteView(i64),
    DeleteCategory(i64),
    EmptyTrash,
    PermanentlyDiscard(Vec<i64>),
    DeleteMacro(i64),
}

pub struct App {
    pub(crate) db: Database,
    pub(crate) path: PathBuf,
    pub(crate) preferences: AppPreferences,
    pub(crate) preferences_path: PathBuf,
    pub(crate) document_settings: DocumentSettings,
    pub(crate) views: Vec<ViewDef>,
    pub(crate) view_index: usize,
    pub(crate) items: Vec<Item>,
    pub(crate) selected: usize,
    pub(crate) selected_column: usize,
    pub(crate) scroll: usize,
    pub(crate) search: String,
    pub(crate) marked: HashSet<i64>,
    pub(crate) categories: Vec<Category>,
    pub(crate) category_selected: usize,
    pub(crate) view_selected: usize,
    pub(crate) mode: Mode,
    pub(crate) status: String,
    pub(crate) should_quit: bool,
    pub(crate) item_rows: Vec<(Rect, i64)>,
    pub(crate) item_columns: Vec<(Rect, usize)>,
    pub(crate) command_regions: Vec<Rect>,
    pub(crate) view_regions: Vec<Rect>,
    pub(crate) choice_regions: Vec<Rect>,
    pub(crate) macros: Vec<MacroDef>,
    pub(crate) macro_selected: usize,
    pub(crate) recording: Option<RecordingState>,
    pub(crate) macro_capture_key: bool,
    macro_runtime: Option<MacroRuntime>,
    macro_globals: std::collections::HashMap<String, String>,
    macro_return_mode: Option<Mode>,
    macro_key_hit: bool,
    macro_origin_mode: Option<Mode>,
    last_autosave: Instant,
    current_day: NaiveDate,
}

impl App {
    pub fn new(db: Database, path: PathBuf) -> Result<Self> {
        let (preferences, preferences_path) = AppPreferences::load()?;
        Self::build(db, path, preferences, preferences_path)
    }

    #[cfg(test)]
    pub fn new_with_preferences(
        db: Database,
        path: PathBuf,
        preferences: AppPreferences,
        preferences_path: PathBuf,
    ) -> Result<Self> {
        Self::build(db, path, preferences, preferences_path)
    }

    fn build(
        mut db: Database,
        path: PathBuf,
        preferences: AppPreferences,
        preferences_path: PathBuf,
    ) -> Result<Self> {
        db.apply_rules_to_all()?;
        let views = db.views()?;
        let categories = db.categories()?;
        let document_settings = db.document_settings()?;
        let macros = db.macros()?;
        let macro_globals = db.macro_variables()?;
        let mut app = Self {
            db,
            path,
            preferences,
            preferences_path,
            document_settings,
            views,
            view_index: 0,
            items: vec![],
            selected: 0,
            selected_column: 0,
            scroll: 0,
            search: String::new(),
            marked: HashSet::new(),
            categories,
            category_selected: 0,
            view_selected: 0,
            mode: Mode::Normal,
            status: "Ready — capture first, organize later".into(),
            should_quit: false,
            item_rows: vec![],
            item_columns: vec![],
            command_regions: vec![],
            view_regions: vec![],
            choice_regions: vec![],
            macros,
            macro_selected: 0,
            recording: None,
            macro_capture_key: false,
            macro_runtime: None,
            macro_globals,
            macro_return_mode: None,
            macro_key_hit: false,
            macro_origin_mode: None,
            last_autosave: Instant::now(),
            current_day: Local::now().date_naive(),
        };
        app.refresh()?;
        if app
            .macros
            .iter()
            .any(|definition| definition.name.eq_ignore_ascii_case("AUTOEXEC"))
            && let Err(error) = app.start_macro("AUTOEXEC")
        {
            app.status = format!("AUTOEXEC error: {error:#}");
        }
        Ok(app)
    }

    pub fn current_view(&self) -> &ViewDef {
        &self.views[self.view_index]
    }
    pub fn selected_item(&self) -> Option<&Item> {
        self.items.get(self.selected)
    }

    pub fn should_quit(&self) -> bool {
        self.should_quit
    }

    pub fn refresh(&mut self) -> Result<()> {
        let keep = self.selected_item().map(|i| i.id);
        self.views = self.db.views()?;
        self.categories = self.db.categories()?;
        self.view_index = self.view_index.min(self.views.len().saturating_sub(1));
        self.items = self.db.items(&self.views[self.view_index], &self.search)?;
        self.selected_column = self
            .selected_column
            .min(self.current_view().columns.len().saturating_sub(1));
        self.selected = keep
            .and_then(|id| self.items.iter().position(|i| i.id == id))
            .unwrap_or(self.selected.min(self.items.len().saturating_sub(1)));
        if self.selected < self.scroll {
            self.scroll = self.selected;
        }
        Ok(())
    }

    fn chosen_ids(&self) -> Vec<i64> {
        if self.marked.is_empty() {
            self.selected_item().map(|i| vec![i.id]).unwrap_or_default()
        } else {
            self.marked.iter().copied().collect()
        }
    }

    pub fn handle_key(&mut self, key: KeyEvent) -> Result<()> {
        if self.macro_runtime.is_some() {
            self.macro_key_hit = true;
            if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) {
                let handled = if let Some(runtime) = &mut self.macro_runtime {
                    runtime.trigger_break()?
                } else {
                    false
                };
                if handled {
                    self.status = "Macro break handler invoked".into();
                    return Ok(());
                }
                return self.abort_macro("Macro interrupted");
            }
        }
        if self.recording.is_some() && (is_macro_shortcut(key) || is_legacy_macro_shortcut(key)) {
            return self.stop_recording();
        }
        if let Mode::MacroMenu { .. } = self.mode {
            return self.handle_macro_menu(key);
        }
        if let Mode::MacroMessage { .. } = self.mode {
            if let Some(runtime) = &mut self.macro_runtime {
                runtime.resume_message();
            }
            self.mode = self.macro_return_mode.take().unwrap_or(Mode::Normal);
            return Ok(());
        }
        if let Mode::MacroGetKey { variable } = self.mode.clone() {
            if key.code == KeyCode::Esc {
                return self.abort_macro("Macro canceled");
            }
            if let Some(runtime) = &mut self.macro_runtime {
                runtime.resume_value(&variable, key_to_source(key))?;
            }
            self.mode = self.macro_return_mode.take().unwrap_or(Mode::Normal);
            return Ok(());
        }
        if self.macro_capture_key {
            if let Mode::Input(mut input) = self.mode.clone() {
                match input.kind {
                    InputKind::MacroEditor(_) => {
                        insert_str_at_cursor(&mut input, &key_to_source(key));
                        self.mode = Mode::Input(input);
                        self.macro_capture_key = false;
                        self.status = "Captured key in macro source".into();
                        return Ok(());
                    }
                    InputKind::MacroBinding(_) => {
                        input.value = key_to_source(key);
                        input.cursor = input.value.len();
                        self.mode = Mode::Input(input);
                        self.macro_capture_key = false;
                        self.status = "Captured macro key binding".into();
                        return Ok(());
                    }
                    _ => {}
                }
            }
            self.macro_capture_key = false;
        }
        if matches!(
            self.mode,
            Mode::Input(InputState {
                kind: InputKind::MacroEditor(_) | InputKind::MacroBinding(_),
                ..
            })
        ) && (is_key_capture_shortcut(key) || is_legacy_key_capture_shortcut(key))
        {
            self.macro_capture_key = true;
            self.status = "KEY CAPTURE — press the key to insert".into();
            return Ok(());
        }
        if is_macro_shortcut(key) || is_legacy_macro_shortcut(key) {
            self.macro_origin_mode = Some(self.mode.clone());
            self.mode = Mode::MacroManager;
            return Ok(());
        }
        if let Some(recording) = &mut self.recording {
            recording.source.push_str(&key_to_source(key));
        }
        if matches!(self.mode, Mode::Normal) && self.macro_runtime.is_none() {
            let bound = self.macros.iter().find(|definition| {
                !definition.key_binding.is_empty()
                    && parse_key_binding(&definition.key_binding).is_ok_and(|binding| {
                        binding.code == key.code && binding.modifiers == key.modifiers
                    })
            });
            if let Some(name) = bound.map(|definition| definition.name.clone()) {
                return self.start_macro(&name);
            }
        }
        self.dispatch_key(key)
    }

    fn dispatch_key(&mut self, key: KeyEvent) -> Result<()> {
        match self.mode.clone() {
            Mode::Normal => self.handle_normal(key),
            Mode::Help => {
                self.mode = Mode::Normal;
                Ok(())
            }
            Mode::Input(_) => self.handle_input(key),
            Mode::Properties(_) => self.handle_properties(key),
            Mode::Form(_) => self.handle_form(key),
            Mode::Dependencies { .. } => self.handle_dependencies(key),
            Mode::Categories => self.handle_categories(key),
            Mode::Views => self.handle_views(key),
            Mode::CategoryManager => self.handle_category_manager(key),
            Mode::Menu => self.handle_menu(key),
            Mode::Execute { .. } => self.handle_execute(key),
            Mode::RuleConflicts => {
                self.mode = Mode::Normal;
                Ok(())
            }
            Mode::Confirm { .. } => self.handle_confirm(key),
            Mode::Choice(_) => self.handle_choice(key),
            Mode::MacroManager => self.handle_macro_manager(key),
            Mode::MacroMenu { .. } | Mode::MacroMessage { .. } | Mode::MacroGetKey { .. } => Ok(()),
        }
    }

    pub fn tick(&mut self) -> Result<()> {
        self.advance_macro()?;
        let interval = self.preferences.autosave_minutes;
        if interval > 0 && self.last_autosave.elapsed() >= Duration::from_secs(interval * 60) {
            self.db.checkpoint()?;
            self.last_autosave = Instant::now();
            self.status = "Autosaved and checkpointed".into();
        }
        let today = Local::now().date_naive();
        if today != self.current_day {
            self.current_day = today;
            self.db.apply_rules_to_all()?;
            self.refresh()?;
            if self.document_settings.trash_policy == TrashPolicy::EndOfDay {
                let count = self.db.empty_trash()?;
                if count > 0 {
                    self.status = format!("Emptied {count} Trash item(s) at end of day");
                    self.refresh()?;
                }
            }
        }
        Ok(())
    }

    fn macro_context(&self) -> MacroContext {
        let now = Local::now();
        let highlighted = self.selected_item();
        let column = self.current_view().columns.get(self.selected_column);
        let (highlight_type, highlight_value) = match (highlighted, column) {
            (Some(item), Some(column)) => {
                let kind = match column.field.as_str() {
                    "numeric" | "value" | "priority" => "NUMERIC",
                    "date" | "when" | "done" | "alarm" | "created" | "updated" => "DATE",
                    "categories" => "CATEGORY",
                    "item" => "ITEM",
                    _ => "TEXT",
                };
                (kind, column.raw_value(item))
            }
            _ => ("NONE", String::new()),
        };
        MacroContext {
            date: now.format("%Y-%m-%d").to_string(),
            time: now.format("%H:%M:%S").to_string(),
            filename: self
                .path
                .file_name()
                .map(|name| name.to_string_lossy().into_owned())
                .unwrap_or_default(),
            filepath: self
                .path
                .parent()
                .map(|path| path.display().to_string())
                .unwrap_or_default(),
            view_name: self.current_view().name.clone(),
            mode: match self.mode {
                Mode::Normal => "VIEW",
                Mode::Input(_) => "EDITOR",
                Mode::CategoryManager | Mode::Categories => "CATEGORY",
                Mode::MacroManager => "MACRO",
                _ => "BOX",
            }
            .into(),
            highlight_type: highlight_type.into(),
            highlight_value,
            mark_count: self.marked.len(),
            marked_in_view: self
                .items
                .iter()
                .filter(|item| self.marked.contains(&item.id))
                .count(),
            clipboard: String::new(),
            depth: self.category_depth(),
            key_hit: self.macro_key_hit,
            prompt: self.current_prompt(),
        }
    }

    fn category_depth(&self) -> usize {
        let Some(mut parent) = self
            .categories
            .get(self.category_selected)
            .and_then(|category| category.parent_id)
        else {
            return 0;
        };
        let mut depth = 1;
        while let Some(category) = self
            .categories
            .iter()
            .find(|category| category.id == parent)
        {
            let Some(next) = category.parent_id else {
                break;
            };
            parent = next;
            depth += 1;
        }
        depth
    }

    fn current_prompt(&self) -> String {
        match &self.mode {
            Mode::Properties(properties) => {
                ["Priority", "When", "Alarm", "Recurrence"][properties.field].into()
            }
            Mode::Form(form) => form_prompt(&form.kind, form.field).into(),
            Mode::Input(input) => input.title.clone(),
            _ => String::new(),
        }
    }

    fn advance_macro(&mut self) -> Result<()> {
        for _ in 0..512 {
            let Some(mut runtime) = self.macro_runtime.take() else {
                return Ok(());
            };
            let action = match runtime.next_action(&self.macro_context()) {
                Ok(action) => action,
                Err(error) => {
                    self.macro_globals = runtime.globals().clone();
                    self.db.save_macro_variables(&self.macro_globals)?;
                    self.mode = Mode::Normal;
                    self.macro_return_mode = None;
                    self.macro_key_hit = false;
                    self.status = format!("Macro error: {error:#}");
                    return Ok(());
                }
            };
            match action {
                None => {
                    self.macro_runtime = Some(runtime);
                    return Ok(());
                }
                Some(MacroAction::Finished) => {
                    self.macro_globals = runtime.globals().clone();
                    self.db.save_macro_variables(&self.macro_globals)?;
                    if matches!(
                        self.mode,
                        Mode::MacroMenu { .. }
                            | Mode::MacroMessage { .. }
                            | Mode::MacroGetKey { .. }
                    ) {
                        self.mode = Mode::Normal;
                    }
                    self.status = "Macro finished".into();
                    self.macro_return_mode = None;
                    self.macro_key_hit = false;
                    return Ok(());
                }
                Some(MacroAction::Key(key)) => {
                    self.macro_runtime = Some(runtime);
                    self.dispatch_key(key)?;
                }
                Some(MacroAction::Prompt {
                    kind,
                    prompt,
                    variable,
                }) => {
                    self.macro_runtime = Some(runtime);
                    self.macro_return_mode = Some(self.mode.clone());
                    let title = match kind {
                        PromptKind::Text => prompt,
                        PromptKind::Category => format!("{prompt}  (category)"),
                        PromptKind::File => format!("{prompt}  (file)"),
                    };
                    self.open_input(
                        InputKind::MacroPrompt(variable),
                        &title,
                        String::new(),
                        false,
                    );
                    return Ok(());
                }
                Some(MacroAction::Menu {
                    title,
                    prompt,
                    choices,
                }) => {
                    self.macro_runtime = Some(runtime);
                    self.macro_return_mode = Some(self.mode.clone());
                    self.mode = Mode::MacroMenu {
                        title,
                        prompt,
                        choices,
                        selected: 0,
                    };
                    return Ok(());
                }
                Some(MacroAction::Message { title, message }) => {
                    self.macro_runtime = Some(runtime);
                    self.macro_return_mode = Some(self.mode.clone());
                    self.mode = Mode::MacroMessage { title, message };
                    return Ok(());
                }
                Some(MacroAction::GetKey { variable }) => {
                    self.macro_runtime = Some(runtime);
                    self.macro_return_mode = Some(self.mode.clone());
                    self.mode = Mode::MacroGetKey { variable };
                    return Ok(());
                }
                Some(MacroAction::IfKey { target }) => {
                    runtime.resume_if_key(&target, self.macro_key_hit)?;
                    self.macro_key_hit = false;
                    self.macro_runtime = Some(runtime);
                }
                Some(MacroAction::Selection { prompt }) => {
                    self.select_setting(&prompt);
                    self.macro_runtime = Some(runtime);
                }
            }
        }
        Ok(())
    }

    fn start_macro(&mut self, name: &str) -> Result<()> {
        let runtime = MacroRuntime::new(name, &self.macros, self.macro_globals.clone())?;
        self.macro_runtime = Some(runtime);
        self.mode = self
            .macro_origin_mode
            .take()
            .unwrap_or_else(|| self.mode.clone());
        self.macro_return_mode = None;
        self.macro_key_hit = false;
        self.status = format!("Running macro {name}");
        Ok(())
    }

    fn abort_macro(&mut self, status: &str) -> Result<()> {
        if let Some(runtime) = self.macro_runtime.take() {
            self.macro_globals = runtime.globals().clone();
            self.db.save_macro_variables(&self.macro_globals)?;
        }
        let current_mode = match &self.mode {
            Mode::Input(InputState {
                kind: InputKind::MacroPrompt(_),
                ..
            })
            | Mode::MacroMenu { .. }
            | Mode::MacroMessage { .. }
            | Mode::MacroGetKey { .. } => Mode::Normal,
            _ => self.mode.clone(),
        };
        self.mode = self.macro_return_mode.take().unwrap_or(current_mode);
        self.macro_key_hit = false;
        self.status = status.into();
        Ok(())
    }

    fn refresh_macros(&mut self) -> Result<()> {
        self.macros = self.db.macros()?;
        self.macro_selected = self.macro_selected.min(self.macros.len().saturating_sub(1));
        Ok(())
    }

    fn select_setting(&mut self, requested: &str) {
        let requested = requested.trim().to_lowercase();
        match &mut self.mode {
            Mode::Properties(properties) => {
                let prompts = ["priority", "when", "alarm", "numeric value", "recurrence"];
                if let Some(index) = prompts
                    .iter()
                    .position(|prompt| prompt.starts_with(&requested))
                {
                    properties.field = index;
                }
            }
            Mode::Form(form) => {
                if let Some(index) = (0..form.values.len()).position(|index| {
                    form_prompt(&form.kind, index)
                        .to_lowercase()
                        .starts_with(&requested)
                }) {
                    form.field = index;
                }
            }
            _ => {}
        }
    }

    fn handle_macro_manager(&mut self, key: KeyEvent) -> Result<()> {
        match key.code {
            KeyCode::Esc => {
                self.mode = self.macro_origin_mode.take().unwrap_or(Mode::Normal);
            }
            KeyCode::Up | KeyCode::Char('k') => {
                self.macro_selected = self.macro_selected.saturating_sub(1)
            }
            KeyCode::Down | KeyCode::Char('j') => {
                self.macro_selected =
                    (self.macro_selected + 1).min(self.macros.len().saturating_sub(1))
            }
            KeyCode::Insert | KeyCode::Char('n') => {
                let mut number = self.macros.len() + 1;
                let name = loop {
                    let candidate = format!("macro{number}");
                    if self
                        .macros
                        .iter()
                        .all(|definition| !definition.name.eq_ignore_ascii_case(&candidate))
                    {
                        break candidate;
                    }
                    number += 1;
                };
                self.open_input(
                    InputKind::MacroEditor(None),
                    "Macro Editor  (F5/Ctrl-S save · Ctrl-V capture key · Esc cancel)",
                    format!("{{{name}}}\n"),
                    true,
                );
            }
            KeyCode::F(2) | KeyCode::Char('e') => {
                if let Some(definition) = self.macros.get(self.macro_selected).cloned() {
                    self.open_input(
                        InputKind::MacroEditor(Some(definition.id)),
                        "Macro Editor  (F5/Ctrl-S save · Ctrl-V capture key · Esc cancel)",
                        definition.source,
                        true,
                    );
                }
            }
            KeyCode::Enter => {
                if let Some(name) = self
                    .macros
                    .get(self.macro_selected)
                    .map(|definition| definition.name.clone())
                {
                    self.start_macro(&name)?;
                }
            }
            KeyCode::F(7) | KeyCode::Char('r') => {
                self.start_recording(key.modifiers.contains(KeyModifiers::SHIFT))
            }
            KeyCode::Char('a') => self.start_recording(true),
            KeyCode::F(6) | KeyCode::Char('b') => {
                if let Some(definition) = self.macros.get(self.macro_selected).cloned() {
                    self.open_input(
                        InputKind::MacroBinding(definition.id),
                        "Attach macro to key  (Ctrl-V captures the next key)",
                        definition.key_binding,
                        false,
                    );
                }
            }
            KeyCode::Delete | KeyCode::F(4) => {
                if let Some(definition) = self.macros.get(self.macro_selected) {
                    if self.preferences.confirm_destructive {
                        self.mode = Mode::Confirm {
                            prompt: format!("Delete macro “{}”?", definition.name),
                            action: ConfirmAction::DeleteMacro(definition.id),
                        };
                    } else {
                        self.delete_macro(definition.id)?;
                    }
                }
            }
            _ => {}
        }
        Ok(())
    }

    fn start_recording(&mut self, append: bool) {
        if let Some(definition) = self.macros.get(self.macro_selected).cloned() {
            self.recording = Some(RecordingState {
                macro_id: definition.id,
                name: definition.name.clone(),
                source: if append {
                    definition.source
                } else {
                    format!("{{{}}}\n", definition.name)
                },
            });
            self.mode = self.macro_origin_mode.clone().unwrap_or(Mode::Normal);
            self.status = format!("LEARN {} — Ctrl-G stops and saves", definition.name);
        }
    }

    fn handle_macro_menu(&mut self, key: KeyEvent) -> Result<()> {
        let Mode::MacroMenu {
            title,
            prompt,
            choices,
            mut selected,
        } = self.mode.clone()
        else {
            return Ok(());
        };
        match key.code {
            KeyCode::Esc => return self.abort_macro("Macro menu canceled"),
            KeyCode::Up | KeyCode::Char('k') => selected = selected.saturating_sub(1),
            KeyCode::Down | KeyCode::Char('j') => {
                selected = (selected + 1).min(choices.len().saturating_sub(1))
            }
            KeyCode::Enter | KeyCode::Char(' ') => {
                if let Some(choice) = choices.get(selected)
                    && let Some(runtime) = &mut self.macro_runtime
                {
                    runtime.resume_menu(&choice.target)?;
                }
                self.mode = self.macro_return_mode.take().unwrap_or(Mode::Normal);
                return Ok(());
            }
            KeyCode::Char(character) => {
                if let Some(index) = choices.iter().position(|choice| {
                    choice
                        .label
                        .chars()
                        .next()
                        .is_some_and(|first| first.eq_ignore_ascii_case(&character))
                }) {
                    selected = index;
                }
            }
            _ => {}
        }
        self.mode = Mode::MacroMenu {
            title,
            prompt,
            choices,
            selected,
        };
        Ok(())
    }

    fn stop_recording(&mut self) -> Result<()> {
        let Some(recording) = self.recording.take() else {
            return Ok(());
        };
        let binding = self
            .macros
            .iter()
            .find(|definition| definition.id == recording.macro_id)
            .map(|definition| definition.key_binding.clone())
            .unwrap_or_default();
        macro_name(&recording.source)?;
        self.db.save_macro(
            Some(recording.macro_id),
            &recording.name,
            &recording.source,
            &binding,
        )?;
        self.refresh_macros()?;
        self.mode = Mode::MacroManager;
        self.status = format!("LEARN saved macro {}", recording.name);
        Ok(())
    }

    fn delete_macro(&mut self, id: i64) -> Result<()> {
        self.db.delete_macro(id)?;
        self.refresh_macros()?;
        self.mode = Mode::MacroManager;
        self.status = "Macro deleted".into();
        Ok(())
    }

    pub fn shutdown(&self) -> Result<()> {
        self.db.close_maintenance()
    }

    fn handle_normal(&mut self, key: KeyEvent) -> Result<()> {
        if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('s') {
            self.db.checkpoint()?;
            self.status = "Saved and checkpointed".into();
            return Ok(());
        }
        if key.code == KeyCode::F(6) && key.modifiers.contains(KeyModifiers::ALT) {
            self.open_input(
                InputKind::Search,
                "Search current view",
                self.search.clone(),
                false,
            );
            return Ok(());
        }
        match key.code {
            KeyCode::Char('q') => self.should_quit = true,
            KeyCode::Up | KeyCode::Char('k') => self.move_selection(-1),
            KeyCode::Down | KeyCode::Char('j') => self.move_selection(1),
            KeyCode::Left => self.selected_column = self.selected_column.saturating_sub(1),
            KeyCode::Right => {
                self.selected_column = (self.selected_column + 1)
                    .min(self.current_view().columns.len().saturating_sub(1))
            }
            KeyCode::PageUp => self.move_selection(-10),
            KeyCode::PageDown => self.move_selection(10),
            KeyCode::Home => self.selected = 0,
            KeyCode::End => self.selected = self.items.len().saturating_sub(1),
            KeyCode::Insert | KeyCode::Char('n') => {
                self.open_input(InputKind::NewItem, "New item", String::new(), false)
            }
            KeyCode::F(2) | KeyCode::Char('e') | KeyCode::Enter => self.edit_highlighted(),
            KeyCode::F(3) | KeyCode::Char('c') => {
                if self.selected_item().is_some() {
                    self.category_selected = 0;
                    self.mode = Mode::Categories;
                }
            }
            KeyCode::F(4) | KeyCode::Char('d') => self.toggle_done()?,
            KeyCode::F(5) => self.edit_note(),
            KeyCode::F(6) | KeyCode::Char('p') => self.edit_properties(),
            KeyCode::F(7) | KeyCode::Char(' ') => self.toggle_mark(),
            KeyCode::F(8) | KeyCode::Char('v') => {
                self.view_selected = self.view_index;
                self.mode = Mode::Views;
            }
            KeyCode::F(9) => {
                self.category_selected = 0;
                self.mode = Mode::CategoryManager;
            }
            KeyCode::F(10) | KeyCode::Char('m') => self.mode = Mode::Menu,
            KeyCode::F(1) | KeyCode::Char('?') => self.mode = Mode::Help,
            KeyCode::Char('r') => self.open_dependencies()?,
            KeyCode::Char('/') => self.open_input(
                InputKind::Search,
                "Search current view",
                self.search.clone(),
                false,
            ),
            KeyCode::Char('l') | KeyCode::Char(']') => self.switch_view(1)?,
            KeyCode::Char('h') | KeyCode::Char('[') => self.switch_view(-1)?,
            KeyCode::Delete => self.discard_or_restore()?,
            KeyCode::Esc if !self.search.is_empty() => {
                self.search.clear();
                self.refresh()?;
                self.status = "Search cleared".into();
            }
            _ => {}
        }
        Ok(())
    }

    fn move_selection(&mut self, delta: isize) {
        if self.items.is_empty() {
            return;
        }
        self.selected =
            (self.selected as isize + delta).clamp(0, self.items.len() as isize - 1) as usize;
    }
    fn switch_view(&mut self, delta: isize) -> Result<()> {
        let len = self.views.len() as isize;
        self.view_index = (self.view_index as isize + delta).rem_euclid(len) as usize;
        self.selected = 0;
        self.selected_column = 0;
        self.scroll = 0;
        self.search.clear();
        self.refresh()?;
        self.status = format!("View: {}", self.current_view().name);
        Ok(())
    }
    fn open_input(&mut self, kind: InputKind, title: &str, value: String, multiline: bool) {
        let cursor = value.len();
        self.mode = Mode::Input(InputState {
            kind,
            title: title.into(),
            value,
            multiline,
            cursor,
        });
    }
    fn edit_highlighted(&mut self) {
        let Some(item) = self.selected_item().cloned() else {
            return;
        };
        let Some(column) = self
            .current_view()
            .columns
            .get(self.selected_column)
            .cloned()
        else {
            return;
        };
        if matches!(column.field.as_str(), "numeric" | "value" | "date")
            && let Some(category_name) = column.category
            && let Some(category) = self
                .categories
                .iter()
                .find(|category| category.name.eq_ignore_ascii_case(&category_name))
                .cloned()
        {
            let value = match category.kind {
                CategoryKind::Numeric => item
                    .numeric_value_for(&category.name)
                    .map(|value| value.to_string())
                    .unwrap_or_default(),
                CategoryKind::Date => item
                    .date_value_for(&category.name)
                    .unwrap_or_default()
                    .into(),
                CategoryKind::Standard => String::new(),
            };
            self.open_input(
                InputKind::CategoryValue {
                    item_id: item.id,
                    category_id: category.id,
                    kind: category.kind,
                },
                &format!("{} value", category.name),
                value,
                false,
            );
        } else {
            self.open_input(InputKind::EditItem(item.id), "Edit item", item.text, false);
        }
    }
    fn edit_note(&mut self) {
        if let Some(i) = self.selected_item() {
            self.open_input(
                InputKind::Note(i.id),
                "Item note  (Ctrl-S saves; Enter adds a line)",
                i.note.clone(),
                true,
            );
        }
    }
    fn edit_properties(&mut self) {
        if let Some(i) = self.selected_item() {
            self.mode = Mode::Properties(PropsState {
                item_id: i.id,
                values: [
                    i.priority.to_string(),
                    i.when_at.clone().unwrap_or_default(),
                    i.alarm_at.clone().unwrap_or_default(),
                    i.recurrence.clone(),
                ],
                field: 0,
            });
        }
    }
    fn toggle_mark(&mut self) {
        if let Some(id) = self.selected_item().map(|i| i.id) {
            if !self.marked.remove(&id) {
                self.marked.insert(id);
            }
            self.status = format!("{} item(s) marked", self.marked.len());
        }
    }
    fn toggle_done(&mut self) -> Result<()> {
        let ids = self.chosen_ids();
        if !ids.is_empty() {
            self.db.toggle_done(&ids)?;
            self.marked.clear();
            self.refresh()?;
            self.status = format!("Updated {} item(s)", ids.len());
        }
        Ok(())
    }
    fn discard_or_restore(&mut self) -> Result<()> {
        let ids = self.chosen_ids();
        if ids.is_empty() {
            return Ok(());
        }
        let restore = self.current_view().kind == ViewKind::Trash;
        if !restore
            && self.document_settings.trash_policy == TrashPolicy::Immediate
            && self.preferences.confirm_destructive
        {
            self.mode = Mode::Confirm {
                prompt: format!("Permanently delete {} item(s)?", ids.len()),
                action: ConfirmAction::PermanentlyDiscard(ids),
            };
            return Ok(());
        }
        self.apply_discard(ids, restore)
    }

    fn apply_discard(&mut self, ids: Vec<i64>, restore: bool) -> Result<()> {
        let permanent = !restore && self.document_settings.trash_policy == TrashPolicy::Immediate;
        self.db.discard(&ids, !restore)?;
        self.marked.clear();
        self.refresh()?;
        self.status = if restore {
            format!("Restored {} item(s)", ids.len())
        } else if permanent {
            format!("Permanently deleted {} item(s)", ids.len())
        } else {
            format!("Moved {} item(s) to Trash", ids.len())
        };
        Ok(())
    }

    fn handle_input(&mut self, key: KeyEvent) -> Result<()> {
        let Mode::Input(mut input) = self.mode.clone() else {
            return Ok(());
        };
        if key.code == KeyCode::Esc {
            return match input.kind {
                InputKind::MacroEditor(_) | InputKind::MacroBinding(_) => {
                    self.mode = Mode::MacroManager;
                    self.status = "Macro changes canceled".into();
                    Ok(())
                }
                InputKind::MacroPrompt(_) => self.abort_macro("Macro input canceled"),
                _ => {
                    self.mode = Mode::Normal;
                    self.status = "Canceled".into();
                    Ok(())
                }
            };
        }
        if input.multiline
            && key.modifiers.contains(KeyModifiers::CONTROL)
            && key.code == KeyCode::Char('s')
        {
            return self.accept_input(input);
        }
        if matches!(input.kind, InputKind::MacroEditor(_)) && key.code == KeyCode::F(5) {
            return self.accept_input(input);
        }
        match key.code {
            KeyCode::Enter if input.multiline => insert_at_cursor(&mut input, '\n'),
            KeyCode::Enter => return self.accept_input(input),
            KeyCode::Tab if input.multiline => {
                for _ in 0..self.document_settings.note_tab_width {
                    insert_at_cursor(&mut input, ' ');
                }
            }
            KeyCode::Backspace => delete_before_cursor(&mut input),
            KeyCode::Delete => delete_at_cursor(&mut input),
            KeyCode::Left => input.cursor = previous_boundary(&input.value, input.cursor),
            KeyCode::Right => input.cursor = next_boundary(&input.value, input.cursor),
            KeyCode::Home => input.cursor = line_start(&input.value, input.cursor),
            KeyCode::End => input.cursor = line_end(&input.value, input.cursor),
            KeyCode::Up if input.multiline => move_cursor_vertically(&mut input, -1),
            KeyCode::Down if input.multiline => move_cursor_vertically(&mut input, 1),
            KeyCode::Char('a') if key.modifiers.contains(KeyModifiers::CONTROL) => input.cursor = 0,
            KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::CONTROL) => {
                input.cursor = input.value.len()
            }
            KeyCode::Char(c) if !key.modifiers.contains(KeyModifiers::CONTROL) => {
                insert_at_cursor(&mut input, c)
            }
            _ => {}
        }
        self.mode = Mode::Input(input);
        Ok(())
    }
    fn accept_input(&mut self, input: InputState) -> Result<()> {
        match input.kind {
            InputKind::NewItem => {
                self.db.add_item(&input.value)?;
                self.status = "Item captured and conditions/actions evaluated".into();
            }
            InputKind::EditItem(id) => {
                self.db.update_item(
                    id,
                    &ItemChanges {
                        text: Some(input.value),
                        ..Default::default()
                    },
                )?;
                self.status = "Item updated and filing re-evaluated".into();
            }
            InputKind::Note(id) => {
                self.db.update_item(
                    id,
                    &ItemChanges {
                        note: Some(input.value),
                        ..Default::default()
                    },
                )?;
                self.status = "Note saved".into();
            }
            InputKind::Search => {
                self.search = input.value;
                self.status = if self.search.is_empty() {
                    "Search cleared".into()
                } else {
                    format!("Filtering for “{}”", self.search)
                };
            }
            InputKind::MacroEditor(id) => {
                let name = macro_name(&input.value)?;
                let binding = id
                    .and_then(|id| {
                        self.macros
                            .iter()
                            .find(|definition| definition.id == id)
                            .map(|definition| definition.key_binding.clone())
                    })
                    .unwrap_or_default();
                let saved = self.db.save_macro(id, &name, &input.value, &binding)?;
                self.refresh_macros()?;
                self.macro_selected = self
                    .macros
                    .iter()
                    .position(|definition| definition.id == saved)
                    .unwrap_or(0);
                self.mode = Mode::MacroManager;
                self.status = format!("Macro {name} saved");
                return Ok(());
            }
            InputKind::MacroBinding(id) => {
                if !input.value.trim().is_empty() {
                    parse_key_binding(&input.value)?;
                }
                let definition = self
                    .macros
                    .iter()
                    .find(|definition| definition.id == id)
                    .cloned()
                    .context("macro no longer exists")?;
                self.db.save_macro(
                    Some(id),
                    &definition.name,
                    &definition.source,
                    input.value.trim(),
                )?;
                self.refresh_macros()?;
                self.mode = Mode::MacroManager;
                self.status = "Macro key binding saved".into();
                return Ok(());
            }
            InputKind::MacroPrompt(variable) => {
                if let Some(runtime) = &mut self.macro_runtime {
                    runtime.resume_value(&variable, input.value)?;
                }
                self.mode = self.macro_return_mode.take().unwrap_or(Mode::Normal);
                return Ok(());
            }
            InputKind::CategoryValue {
                item_id,
                category_id,
                kind,
            } => match kind {
                CategoryKind::Numeric => {
                    let value = if input.value.trim().is_empty() {
                        None
                    } else {
                        Some(
                            input
                                .value
                                .trim()
                                .parse::<f64>()
                                .context("numeric category value must be a number")?,
                        )
                    };
                    self.db.set_numeric_value(item_id, category_id, value)?;
                    self.status = "Numeric category value saved".into();
                }
                CategoryKind::Date => {
                    let value = normalize_date(&self.db, &input.value)?;
                    self.db.set_date_value(item_id, category_id, value)?;
                    self.status = "Date category value saved".into();
                }
                CategoryKind::Standard => bail!("standard categories do not hold values"),
            },
        }
        self.mode = Mode::Normal;
        self.refresh()?;
        Ok(())
    }

    fn handle_properties(&mut self, key: KeyEvent) -> Result<()> {
        let Mode::Properties(mut props) = self.mode.clone() else {
            return Ok(());
        };
        match key.code {
            KeyCode::Esc => {
                self.mode = Mode::Normal;
                return Ok(());
            }
            KeyCode::Tab | KeyCode::Down => props.field = (props.field + 1) % 4,
            KeyCode::BackTab | KeyCode::Up => props.field = (props.field + 3) % 4,
            KeyCode::Backspace => {
                props.values[props.field].pop();
            }
            KeyCode::Enter => {
                let parsed = (|| -> Result<_> {
                    let priority = props.values[0]
                        .parse::<i64>()
                        .context("priority must be a number from 1 to 5")?;
                    if !(1..=5).contains(&priority) {
                        bail!("priority must be a number from 1 to 5");
                    }
                    let when_at = normalize_date(&self.db, &props.values[1])?;
                    let alarm_at = normalize_date(&self.db, &props.values[2])?;
                    Ok((priority, when_at, alarm_at))
                })();
                let (priority, when_at, alarm_at) = match parsed {
                    Ok(values) => values,
                    Err(error) => {
                        self.status = format!("Cannot save properties: {error:#}");
                        self.mode = Mode::Properties(props);
                        return Ok(());
                    }
                };
                self.db.update_item(
                    props.item_id,
                    &ItemChanges {
                        priority: Some(priority),
                        when_at: Some(when_at),
                        alarm_at: Some(alarm_at),
                        recurrence: Some(props.values[3].clone()),
                        ..Default::default()
                    },
                )?;
                self.mode = Mode::Normal;
                self.refresh()?;
                self.status = "Properties saved".into();
                return Ok(());
            }
            KeyCode::Char(c) => props.values[props.field].push(c),
            _ => {}
        }
        self.mode = Mode::Properties(props);
        Ok(())
    }
    fn handle_categories(&mut self, key: KeyEvent) -> Result<()> {
        match key.code {
            KeyCode::Esc | KeyCode::Enter => self.mode = Mode::Normal,
            KeyCode::Up | KeyCode::Char('k') => {
                self.category_selected = self.category_selected.saturating_sub(1)
            }
            KeyCode::Down | KeyCode::Char('j') => {
                self.category_selected =
                    (self.category_selected + 1).min(self.categories.len().saturating_sub(1))
            }
            KeyCode::Char(' ') => {
                if let (Some(item), Some(cat)) = (
                    self.selected_item(),
                    self.categories.get(self.category_selected),
                ) {
                    self.db.toggle_assignment(item.id, cat.id)?;
                    self.refresh()?;
                    self.status = "Category assignment changed".into();
                }
            }
            _ => {}
        }
        Ok(())
    }
    fn handle_views(&mut self, key: KeyEvent) -> Result<()> {
        match key.code {
            KeyCode::Esc => self.mode = Mode::Normal,
            KeyCode::Up if key.modifiers.contains(KeyModifiers::CONTROL) => {
                if let Some(view) = self.views.get(self.view_selected) {
                    let id = view.id;
                    self.db.move_view(id, -1)?;
                    self.refresh()?;
                    self.view_selected = self.views.iter().position(|v| v.id == id).unwrap_or(0);
                    self.status = "View moved up".into();
                }
            }
            KeyCode::Down if key.modifiers.contains(KeyModifiers::CONTROL) => {
                if let Some(view) = self.views.get(self.view_selected) {
                    let id = view.id;
                    self.db.move_view(id, 1)?;
                    self.refresh()?;
                    self.view_selected = self.views.iter().position(|v| v.id == id).unwrap_or(0);
                    self.status = "View moved down".into();
                }
            }
            KeyCode::Up | KeyCode::Char('k') => {
                self.view_selected = self.view_selected.saturating_sub(1)
            }
            KeyCode::Down | KeyCode::Char('j') => {
                self.view_selected =
                    (self.view_selected + 1).min(self.views.len().saturating_sub(1))
            }
            KeyCode::Enter => {
                self.view_index = self.view_selected;
                self.selected = 0;
                self.scroll = 0;
                self.search.clear();
                self.mode = Mode::Normal;
                self.refresh()?;
                self.status = format!("View: {}", self.current_view().name);
            }
            KeyCode::Char('n') | KeyCode::Insert => self.open_view_form(None),
            KeyCode::Char('e') | KeyCode::F(2) => {
                if let Some(view) = self.views.get(self.view_selected).cloned() {
                    self.open_view_form(Some(view));
                }
            }
            KeyCode::Delete => {
                if let Some(view) = self.views.get(self.view_selected) {
                    let id = view.id;
                    if self.preferences.confirm_destructive {
                        self.mode = Mode::Confirm {
                            prompt: format!("Delete view “{}”?", view.name),
                            action: ConfirmAction::DeleteView(id),
                        };
                    } else {
                        self.delete_view(id)?;
                    }
                }
            }
            _ => {}
        }
        Ok(())
    }
    fn handle_category_manager(&mut self, key: KeyEvent) -> Result<()> {
        match key.code {
            KeyCode::Esc => self.mode = Mode::Normal,
            KeyCode::Up | KeyCode::Char('k') => {
                self.category_selected = self.category_selected.saturating_sub(1)
            }
            KeyCode::Down | KeyCode::Char('j') => {
                self.category_selected =
                    (self.category_selected + 1).min(self.categories.len().saturating_sub(1))
            }
            KeyCode::Char('n') | KeyCode::Insert => self.open_category_form(None),
            KeyCode::Char('e') | KeyCode::Enter | KeyCode::F(2) => {
                if let Some(category) = self.categories.get(self.category_selected).cloned() {
                    self.open_category_form(Some(category));
                }
            }
            KeyCode::Delete => {
                if let Some(category) = self.categories.get(self.category_selected) {
                    let id = category.id;
                    if self.preferences.confirm_destructive {
                        self.mode = Mode::Confirm {
                            prompt: format!("Delete category “{}”?", category.name),
                            action: ConfirmAction::DeleteCategory(id),
                        };
                    } else {
                        self.delete_category(id)?;
                    }
                }
            }
            _ => {}
        }
        Ok(())
    }
    fn handle_menu(&mut self, key: KeyEvent) -> Result<()> {
        self.mode = Mode::Normal;
        match key.code {
            KeyCode::Char('n') => {
                self.open_input(InputKind::NewItem, "New item", String::new(), false)
            }
            KeyCode::Char('s') => {
                self.db.checkpoint()?;
                self.status = "Saved and checkpointed".into();
            }
            KeyCode::Char('h') => self.mode = Mode::Help,
            KeyCode::Char('r') => self.open_dependencies()?,
            KeyCode::Char('p') => self.open_preferences_form(),
            KeyCode::Char('d') => self.open_document_settings_form(),
            KeyCode::Char('x') => self.mode = Mode::MacroManager,
            KeyCode::Char('u') => self.mode = Mode::Execute { selected: 0 },
            KeyCode::Char('f') => self.mode = Mode::RuleConflicts,
            KeyCode::Char('b') => {
                let path = self.db.backup_now()?;
                self.status = format!("Backup written to {}", path.display());
            }
            KeyCode::Char('t') => {
                if self.preferences.confirm_destructive {
                    self.mode = Mode::Confirm {
                        prompt: "Permanently empty Trash?".into(),
                        action: ConfirmAction::EmptyTrash,
                    };
                } else {
                    self.empty_trash()?;
                }
            }
            KeyCode::Char('q') => self.should_quit = true,
            _ => {}
        }
        Ok(())
    }

    fn handle_execute(&mut self, key: KeyEvent) -> Result<()> {
        let Mode::Execute { mut selected } = self.mode else {
            return Ok(());
        };
        match key.code {
            KeyCode::Esc => self.mode = Mode::Normal,
            KeyCode::Up | KeyCode::Char('k') => {
                selected = selected.saturating_sub(1);
                self.mode = Mode::Execute { selected };
            }
            KeyCode::Down | KeyCode::Char('j') => {
                selected = (selected + 1).min(4);
                self.mode = Mode::Execute { selected };
            }
            KeyCode::Enter | KeyCode::Char(' ') => {
                let ids = match selected {
                    0 => self
                        .selected_item()
                        .map(|item| vec![item.id])
                        .unwrap_or_default(),
                    1 => self.marked.iter().copied().collect(),
                    2 => {
                        let selected_item = self.selected_item();
                        let section = self.current_view().sections.iter().find(|section| {
                            selected_item.is_some_and(|item| {
                                filter::matches(&section.filter_expr, item).unwrap_or(false)
                            })
                        });
                        self.items
                            .iter()
                            .filter(|item| {
                                section.is_some_and(|section| {
                                    filter::matches(&section.filter_expr, item).unwrap_or(false)
                                })
                            })
                            .map(|item| item.id)
                            .collect()
                    }
                    3 => self.items.iter().map(|item| item.id).collect(),
                    _ => self.db.all_item_ids()?,
                };
                let summary = self.db.execute_rules(&ids)?;
                self.refresh()?;
                self.mode = Mode::Normal;
                self.status = format!(
                    "Executed conditions/actions for {} item(s) — {} conflict(s)",
                    summary.items, summary.conflicts
                );
            }
            _ => self.mode = Mode::Execute { selected },
        }
        Ok(())
    }

    fn handle_confirm(&mut self, key: KeyEvent) -> Result<()> {
        let Mode::Confirm { action, .. } = self.mode.clone() else {
            return Ok(());
        };
        match key.code {
            KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Enter => {
                match action {
                    ConfirmAction::DeleteView(id) => self.delete_view(id)?,
                    ConfirmAction::DeleteCategory(id) => self.delete_category(id)?,
                    ConfirmAction::EmptyTrash => self.empty_trash()?,
                    ConfirmAction::PermanentlyDiscard(ids) => self.apply_discard(ids, false)?,
                    ConfirmAction::DeleteMacro(id) => self.delete_macro(id)?,
                }
                if matches!(self.mode, Mode::Confirm { .. }) {
                    self.mode = Mode::Normal;
                }
            }
            KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
                self.mode = Mode::Normal;
                self.status = "Canceled".into();
            }
            _ => {}
        }
        Ok(())
    }

    fn delete_view(&mut self, id: i64) -> Result<()> {
        self.db.delete_view(id)?;
        self.view_index = 0;
        self.view_selected = 0;
        self.mode = Mode::Views;
        self.refresh()?;
        self.status = "View deleted".into();
        Ok(())
    }

    fn delete_category(&mut self, id: i64) -> Result<()> {
        self.db.delete_category(id)?;
        self.category_selected = self.category_selected.saturating_sub(1);
        self.mode = Mode::CategoryManager;
        self.refresh()?;
        self.status = "Category deleted; its items were preserved".into();
        Ok(())
    }

    fn empty_trash(&mut self) -> Result<()> {
        let count = self.db.empty_trash()?;
        self.mode = Mode::Normal;
        self.refresh()?;
        self.status = format!("Permanently removed {count} Trash item(s)");
        Ok(())
    }

    fn open_preferences_form(&mut self) {
        self.mode = Mode::Form(FormState {
            kind: FormKind::Preferences,
            values: vec![
                self.preferences.theme.clone(),
                yes_no(self.preferences.show_command_bar),
                yes_no(self.preferences.show_rule_info),
                yes_no(self.preferences.show_carriage_returns),
                display_space(&self.preferences.item_marker),
                self.preferences.autosave_minutes.to_string(),
                yes_no(self.preferences.confirm_destructive),
                self.preferences.date_format.clone(),
                yes_no(self.preferences.clock_24h),
                self.preferences.decimal_separator.clone(),
                display_space(&self.preferences.thousands_separator),
            ],
            field: 0,
        });
    }

    fn open_document_settings_form(&mut self) {
        let settings = &self.document_settings;
        self.mode = Mode::Form(FormState {
            kind: FormKind::DocumentSettings,
            values: vec![
                settings.description.clone(),
                yes_no(settings.backup_on_open),
                settings.trash_policy.to_string(),
                settings.done_policy.to_string(),
                yes_no(settings.automatic_filing),
                settings.date_order.to_string(),
                settings.week_start.to_string(),
                settings.default_time.clone(),
                settings.morning_time.clone(),
                settings.afternoon_time.clone(),
                settings.evening_time.clone(),
                settings.note_tab_width.to_string(),
                yes_no(settings.report_headers),
            ],
            field: 0,
        });
    }

    fn open_category_form(&mut self, category: Option<Category>) {
        let id = category.as_ref().map(|c| c.id);
        let rule =
            id.and_then(|category_id| self.db.category_rules(category_id).ok()?.into_iter().next());
        let condition = rule
            .as_ref()
            .map(|r| r.condition_expr.clone())
            .unwrap_or_default();
        let action = rule
            .map(|r| format!("{}:{}", r.action_kind, r.action_value))
            .unwrap_or_default();
        let values = if let Some(c) = category {
            let parent = c
                .parent_id
                .and_then(|id| self.categories.iter().find(|p| p.id == id))
                .map(|p| p.name.clone())
                .unwrap_or_default();
            vec![
                c.name,
                parent,
                c.kind.to_string(),
                c.match_text,
                yes_no(c.exclusive),
                condition,
                action,
            ]
        } else {
            vec![
                String::new(),
                String::new(),
                "standard".into(),
                String::new(),
                "no".into(),
                String::new(),
                String::new(),
            ]
        };
        self.mode = Mode::Form(FormState {
            kind: FormKind::Category(id),
            values,
            field: 0,
        });
    }

    fn open_view_form(&mut self, view: Option<ViewDef>) {
        let id = view.as_ref().map(|v| v.id);
        let values = if let Some(v) = view {
            let columns = v.columns_spec();
            let sections = v.sections_spec();
            vec![
                v.name,
                v.kind.to_string(),
                v.filter_value,
                v.filter_expr,
                v.sort_key.to_string(),
                yes_no(v.show_done),
                columns,
                sections,
            ]
        } else {
            vec![
                String::new(),
                "list".into(),
                String::new(),
                String::new(),
                "manual".into(),
                "yes".into(),
                "item:48:Items:none,categories:27:Categories:none,when:20:When:none,priority:5:P:none".into(),
                String::new(),
            ]
        };
        self.mode = Mode::Form(FormState {
            kind: FormKind::View(id),
            values,
            field: 0,
        });
    }

    fn handle_form(&mut self, key: KeyEvent) -> Result<()> {
        let Mode::Form(mut form) = self.mode.clone() else {
            return Ok(());
        };
        let choices = form_choices(&form.kind, form.field);
        match key.code {
            KeyCode::Esc => {
                self.mode = match form.kind {
                    FormKind::Category(_) => Mode::CategoryManager,
                    FormKind::View(_) => Mode::Views,
                    FormKind::Preferences | FormKind::DocumentSettings => Mode::Normal,
                };
                return Ok(());
            }
            KeyCode::Tab | KeyCode::Down => form.field = (form.field + 1) % form.values.len(),
            KeyCode::BackTab | KeyCode::Up => {
                form.field = (form.field + form.values.len() - 1) % form.values.len()
            }
            KeyCode::Backspace => {
                if choices.is_none() {
                    form.values[form.field].pop();
                }
            }
            KeyCode::Char(' ') if choices.is_some() => {
                let options = choices.unwrap_or_default();
                let selected = options
                    .iter()
                    .position(|option| option.value == form.values[form.field])
                    .unwrap_or(0);
                self.mode = Mode::Choice(ChoiceState {
                    form,
                    options,
                    selected,
                });
                return Ok(());
            }
            KeyCode::Enter => {
                match form.kind {
                    FormKind::Category(id) => {
                        let parent_id = if form.values[1].trim().is_empty() {
                            None
                        } else {
                            self.categories
                                .iter()
                                .find(|c| c.name.eq_ignore_ascii_case(form.values[1].trim()))
                                .map(|c| c.id)
                        };
                        let category_id = self.db.save_category(
                            id,
                            &form.values[0],
                            parent_id,
                            form.values[2].trim(),
                            &form.values[3],
                            parse_yes(&form.values[4]),
                        )?;
                        self.db
                            .save_primary_rule(category_id, &form.values[5], &form.values[6])?;
                        self.db.reapply_automatic_assignments()?;
                        self.refresh()?;
                        self.mode = Mode::CategoryManager;
                        self.status = "Category definition saved".into();
                    }
                    FormKind::View(id) => {
                        let saved = self.db.save_view_design(
                            id,
                            &form.values[0],
                            form.values[1].trim(),
                            &form.values[2],
                            form.values[4].trim(),
                            parse_yes(&form.values[5]),
                            &form.values[3],
                            &form.values[6],
                            &form.values[7],
                        )?;
                        self.refresh()?;
                        self.view_selected =
                            self.views.iter().position(|v| v.id == saved).unwrap_or(0);
                        self.mode = Mode::Views;
                        self.status = "Live view saved".into();
                    }
                    FormKind::Preferences => {
                        let preferences = AppPreferences {
                            theme: form.values[0].trim().to_lowercase(),
                            show_command_bar: parse_yes_no(&form.values[1])?,
                            show_rule_info: parse_yes_no(&form.values[2])?,
                            show_carriage_returns: parse_yes_no(&form.values[3])?,
                            item_marker: decode_space(&form.values[4]),
                            autosave_minutes: form.values[5]
                                .parse()
                                .context("autosave interval must be a number")?,
                            confirm_destructive: parse_yes_no(&form.values[6])?,
                            date_format: form.values[7].trim().to_lowercase(),
                            clock_24h: parse_yes_no(&form.values[8])?,
                            decimal_separator: form.values[9].clone(),
                            thousands_separator: decode_space(&form.values[10]),
                        };
                        preferences.save_to(&self.preferences_path)?;
                        self.preferences = preferences;
                        self.last_autosave = Instant::now();
                        self.mode = Mode::Normal;
                        self.status =
                            format!("Preferences saved to {}", self.preferences_path.display());
                    }
                    FormKind::DocumentSettings => {
                        let settings = DocumentSettings {
                            description: form.values[0].trim().into(),
                            backup_on_open: parse_yes_no(&form.values[1])?,
                            trash_policy: form.values[2]
                                .trim()
                                .to_lowercase()
                                .parse::<TrashPolicy>()
                                .map_err(anyhow::Error::msg)?,
                            done_policy: form.values[3]
                                .trim()
                                .to_lowercase()
                                .parse::<DonePolicy>()
                                .map_err(anyhow::Error::msg)?,
                            automatic_filing: parse_yes_no(&form.values[4])?,
                            date_order: form.values[5]
                                .trim()
                                .to_lowercase()
                                .parse::<DateOrder>()
                                .map_err(anyhow::Error::msg)?,
                            week_start: form.values[6]
                                .trim()
                                .to_lowercase()
                                .parse::<WeekStart>()
                                .map_err(anyhow::Error::msg)?,
                            default_time: form.values[7].trim().into(),
                            morning_time: form.values[8].trim().into(),
                            afternoon_time: form.values[9].trim().into(),
                            evening_time: form.values[10].trim().into(),
                            note_tab_width: form.values[11]
                                .parse()
                                .context("note tab width must be a number")?,
                            report_headers: parse_yes_no(&form.values[12])?,
                        };
                        self.db.save_document_settings(&settings)?;
                        self.document_settings = settings;
                        self.mode = Mode::Normal;
                        self.refresh()?;
                        self.status = "Document settings saved inside the .agnd file".into();
                    }
                }
                return Ok(());
            }
            KeyCode::Char(c)
                if choices.is_none() && !key.modifiers.contains(KeyModifiers::CONTROL) =>
            {
                form.values[form.field].push(c)
            }
            _ => {}
        }
        self.mode = Mode::Form(form);
        Ok(())
    }

    fn handle_choice(&mut self, key: KeyEvent) -> Result<()> {
        let Mode::Choice(mut choice) = self.mode.clone() else {
            return Ok(());
        };
        match key.code {
            KeyCode::Esc => self.mode = Mode::Form(choice.form),
            KeyCode::Up | KeyCode::Char('k') => {
                choice.selected = choice.selected.saturating_sub(1);
                self.mode = Mode::Choice(choice);
            }
            KeyCode::Down | KeyCode::Char('j') => {
                choice.selected = (choice.selected + 1).min(choice.options.len().saturating_sub(1));
                self.mode = Mode::Choice(choice);
            }
            KeyCode::Home => {
                choice.selected = 0;
                self.mode = Mode::Choice(choice);
            }
            KeyCode::End => {
                choice.selected = choice.options.len().saturating_sub(1);
                self.mode = Mode::Choice(choice);
            }
            KeyCode::Enter | KeyCode::Char(' ') => {
                if let Some(option) = choice.options.get(choice.selected) {
                    let field = choice.form.field;
                    choice.form.values[field] = option.value.clone();
                }
                self.mode = Mode::Form(choice.form);
            }
            _ => self.mode = Mode::Choice(choice),
        }
        Ok(())
    }

    fn open_dependencies(&mut self) -> Result<()> {
        let Some(item_id) = self.selected_item().map(|i| i.id) else {
            return Ok(());
        };
        let Some(all) = self.views.iter().find(|v| v.kind == ViewKind::List) else {
            return Ok(());
        };
        let choices = self
            .db
            .items(all, "")?
            .into_iter()
            .filter(|i| i.id != item_id)
            .collect();
        self.mode = Mode::Dependencies {
            item_id,
            choices,
            selected: 0,
        };
        Ok(())
    }

    fn handle_dependencies(&mut self, key: KeyEvent) -> Result<()> {
        let Mode::Dependencies {
            item_id,
            choices,
            mut selected,
        } = self.mode.clone()
        else {
            return Ok(());
        };
        match key.code {
            KeyCode::Esc | KeyCode::Enter => {
                self.mode = Mode::Normal;
                return Ok(());
            }
            KeyCode::Up | KeyCode::Char('k') => selected = selected.saturating_sub(1),
            KeyCode::Down | KeyCode::Char('j') => {
                selected = (selected + 1).min(choices.len().saturating_sub(1))
            }
            KeyCode::Char(' ') => {
                if let Some(choice) = choices.get(selected) {
                    self.db.toggle_dependency(item_id, choice.id)?;
                    self.status = "Prerequisite changed".into();
                }
            }
            _ => {}
        }
        self.mode = Mode::Dependencies {
            item_id,
            choices,
            selected,
        };
        Ok(())
    }

    pub fn handle_mouse(&mut self, event: MouseEvent) -> Result<()> {
        if !matches!(event.kind, MouseEventKind::Down(MouseButton::Left)) {
            return Ok(());
        }
        let (x, y) = (event.column, event.row);
        if matches!(self.mode, Mode::MacroManager) {
            if let Some(index) = self
                .choice_regions
                .iter()
                .position(|region| inside(*region, x, y))
            {
                self.macro_selected = index;
            }
            return Ok(());
        }
        if matches!(self.mode, Mode::MacroMenu { .. }) {
            if let Some(index) = self
                .choice_regions
                .iter()
                .position(|region| inside(*region, x, y))
            {
                if let Mode::MacroMenu { selected, .. } = &mut self.mode {
                    *selected = index;
                }
                return self.handle_macro_menu(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
            }
            return Ok(());
        }
        if let Mode::Choice(mut choice) = self.mode.clone() {
            if let Some(index) = self
                .choice_regions
                .iter()
                .position(|region| inside(*region, x, y))
                && let Some(option) = choice.options.get(index)
            {
                let field = choice.form.field;
                choice.form.values[field] = option.value.clone();
                self.mode = Mode::Form(choice.form);
            }
            return Ok(());
        }
        if let Some((_, id)) = self.item_rows.iter().find(|(r, _)| inside(*r, x, y)) {
            if let Some(pos) = self.items.iter().position(|i| i.id == *id) {
                self.selected = pos;
            }
            if let Some((_, column)) = self
                .item_columns
                .iter()
                .find(|(region, _)| inside(*region, x, y))
            {
                self.selected_column = *column;
            }
            return Ok(());
        }
        if let Some(i) = self.command_regions.iter().position(|r| inside(*r, x, y)) {
            return self.handle_key(KeyEvent::new(KeyCode::F((i + 1) as u8), KeyModifiers::NONE));
        }
        if let Some(i) = self.view_regions.iter().position(|r| inside(*r, x, y)) {
            self.view_index = i;
            self.selected = 0;
            self.scroll = 0;
            self.search.clear();
            self.refresh()?;
        }
        Ok(())
    }
}

fn normalize_date(db: &Database, value: &str) -> Result<Option<String>> {
    let s = value.trim();
    if s.is_empty() {
        Ok(None)
    } else if chrono::DateTime::parse_from_rfc3339(s).is_ok() {
        Ok(Some(s.into()))
    } else {
        Ok(Some(db.interpret_date(&format!("on {s}"))?.with_context(
            || format!("unrecognized date or time {s:?}"),
        )?))
    }
}
fn inside(r: Rect, x: u16, y: u16) -> bool {
    x >= r.x && x < r.x + r.width && y >= r.y && y < r.y + r.height
}

fn is_macro_shortcut(key: KeyEvent) -> bool {
    key.code == KeyCode::Char('g') && key.modifiers.contains(KeyModifiers::CONTROL)
}

fn is_legacy_macro_shortcut(key: KeyEvent) -> bool {
    key.code == KeyCode::F(3) && key.modifiers.contains(KeyModifiers::ALT)
}

fn is_key_capture_shortcut(key: KeyEvent) -> bool {
    key.code == KeyCode::Char('v') && key.modifiers.contains(KeyModifiers::CONTROL)
}

fn is_legacy_key_capture_shortcut(key: KeyEvent) -> bool {
    key.code == KeyCode::Char('=') && key.modifiers.contains(KeyModifiers::ALT)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::preferences::AppPreferences;
    use tempfile::tempdir;

    fn key(code: KeyCode) -> KeyEvent {
        KeyEvent::new(code, KeyModifiers::NONE)
    }

    fn control(character: char) -> KeyEvent {
        KeyEvent::new(KeyCode::Char(character), KeyModifiers::CONTROL)
    }

    #[test]
    fn learn_records_real_commands_and_the_recording_plays_back() {
        let directory = tempdir().unwrap();
        let path = directory.path().join("learn.agnd");
        let mut db = Database::open(&path).unwrap();
        db.save_macro(None, "learnme", "{learnme}\n", "").unwrap();
        let mut app = App::new_with_preferences(
            db,
            path,
            AppPreferences::default(),
            directory.path().join("preferences.toml"),
        )
        .unwrap();

        app.handle_key(control('g')).unwrap();
        app.handle_key(key(KeyCode::F(7))).unwrap();
        app.handle_key(key(KeyCode::Char('n'))).unwrap();
        for character in "recorded item".chars() {
            app.handle_key(key(KeyCode::Char(character))).unwrap();
        }
        app.handle_key(key(KeyCode::Enter)).unwrap();
        app.handle_key(control('g')).unwrap();

        assert_eq!(app.macros[0].source, "{learnme}\nnrecorded item{ENTER}");
        assert_eq!(app.items.len(), 1);
        app.handle_key(key(KeyCode::Enter)).unwrap();
        app.tick().unwrap();
        assert_eq!(app.items.len(), 2);
        assert!(app.items.iter().all(|item| item.text == "recorded item"));
    }

    #[test]
    fn plain_a_appends_a_learn_recording() {
        let directory = tempdir().unwrap();
        let path = directory.path().join("append-learn.agnd");
        let mut db = Database::open(&path).unwrap();
        db.save_macro(None, "appendme", "{appendme}x", "").unwrap();
        let mut app = App::new_with_preferences(
            db,
            path,
            AppPreferences::default(),
            directory.path().join("preferences.toml"),
        )
        .unwrap();

        app.handle_key(control('g')).unwrap();
        app.handle_key(key(KeyCode::Char('a'))).unwrap();
        app.handle_key(key(KeyCode::Char('y'))).unwrap();
        app.handle_key(control('g')).unwrap();

        assert_eq!(app.macros[0].source, "{appendme}xy");
    }

    #[test]
    fn prompted_macro_returns_to_the_editor_it_opened() {
        let directory = tempdir().unwrap();
        let path = directory.path().join("prompt.agnd");
        let mut db = Database::open(&path).unwrap();
        db.save_macro(
            None,
            "capture",
            "{capture}n{inputtext;Item text;%text}{type;%text}{enter}",
            "",
        )
        .unwrap();
        let mut app = App::new_with_preferences(
            db,
            path,
            AppPreferences::default(),
            directory.path().join("preferences.toml"),
        )
        .unwrap();

        app.start_macro("capture").unwrap();
        app.tick().unwrap();
        assert!(matches!(
            app.mode,
            Mode::Input(InputState {
                kind: InputKind::MacroPrompt(_),
                ..
            })
        ));
        for character in "prompted item".chars() {
            app.handle_key(key(KeyCode::Char(character))).unwrap();
        }
        app.handle_key(key(KeyCode::Enter)).unwrap();
        assert!(matches!(
            app.mode,
            Mode::Input(InputState {
                kind: InputKind::NewItem,
                ..
            })
        ));
        app.tick().unwrap();
        assert_eq!(app.items.len(), 1);
        assert_eq!(app.items[0].text, "prompted item");
    }

    #[test]
    fn macro_editor_captures_keys_and_f5_saves() {
        let directory = tempdir().unwrap();
        let path = directory.path().join("editor.agnd");
        let db = Database::open(&path).unwrap();
        let mut app = App::new_with_preferences(
            db,
            path,
            AppPreferences::default(),
            directory.path().join("preferences.toml"),
        )
        .unwrap();

        app.handle_key(control('g')).unwrap();
        app.handle_key(key(KeyCode::Insert)).unwrap();
        app.handle_key(control('v')).unwrap();
        app.handle_key(key(KeyCode::F(2))).unwrap();
        app.handle_key(key(KeyCode::F(5))).unwrap();

        assert!(matches!(app.mode, Mode::MacroManager));
        assert_eq!(app.macros.len(), 1);
        assert_eq!(app.macros[0].source, "{macro1}\n{F2}");

        app.handle_key(key(KeyCode::Char('b'))).unwrap();
        app.handle_key(control('v')).unwrap();
        app.handle_key(control('p')).unwrap();
        app.handle_key(key(KeyCode::Enter)).unwrap();
        assert_eq!(app.macros[0].key_binding, "{CtlP}");
    }

    #[test]
    fn manager_runs_a_macro_in_the_screen_it_was_opened_from() {
        let directory = tempdir().unwrap();
        let path = directory.path().join("context.agnd");
        let mut db = Database::open(&path).unwrap();
        db.add_item("Item with a note").unwrap();
        db.save_macro(None, "typenote", "{typenote}x", "").unwrap();
        let mut app = App::new_with_preferences(
            db,
            path,
            AppPreferences::default(),
            directory.path().join("preferences.toml"),
        )
        .unwrap();

        app.handle_key(key(KeyCode::F(5))).unwrap();
        app.handle_key(control('g')).unwrap();
        app.handle_key(key(KeyCode::Enter)).unwrap();
        app.tick().unwrap();

        let Mode::Input(input) = &app.mode else {
            panic!("macro did not return to the note editor");
        };
        assert!(matches!(input.kind, InputKind::Note(_)));
        assert_eq!(input.value, "x");
    }

    #[test]
    fn invalid_properties_remain_open_without_overwriting_the_item() {
        let directory = tempdir().unwrap();
        let path = directory.path().join("invalid-properties.agnd");
        let mut db = Database::open(&path).unwrap();
        let item_id = db.add_item("Keep my properties").unwrap();
        let mut app = App::new_with_preferences(
            db,
            path,
            AppPreferences::default(),
            directory.path().join("preferences.toml"),
        )
        .unwrap();
        app.mode = Mode::Properties(PropsState {
            item_id,
            values: [
                "not-a-priority".into(),
                "not-a-date".into(),
                String::new(),
                String::new(),
            ],
            field: 0,
        });

        app.handle_key(key(KeyCode::Enter)).unwrap();

        assert!(matches!(app.mode, Mode::Properties(_)));
        assert!(app.status.starts_with("Cannot save properties:"));
        assert_eq!(app.items[0].priority, 3);
        assert!(app.items[0].when_at.is_none());
        assert!(app.items[0].numeric_value.is_none());
    }

    #[test]
    fn highlighted_numeric_cells_feed_macros_and_edit_the_bound_category() {
        let directory = tempdir().unwrap();
        let path = directory.path().join("cell-context.agnd");
        let mut db = Database::open(&path).unwrap();
        let hours = db
            .save_category(None, "Hours", None, "numeric", "", false)
            .unwrap();
        let item = db.add_item("Consulting").unwrap();
        db.set_numeric_value(item, hours, Some(12.5)).unwrap();
        let view_id = db
            .save_view_design(
                None,
                "Hours",
                "list",
                "",
                "manual",
                true,
                "",
                "item:60:Item,numeric[Hours]:20:Hours",
                "",
            )
            .unwrap();
        let mut app = App::new_with_preferences(
            db,
            path,
            AppPreferences::default(),
            directory.path().join("preferences.toml"),
        )
        .unwrap();
        app.view_index = app
            .views
            .iter()
            .position(|view| view.id == view_id)
            .unwrap();
        app.refresh().unwrap();
        app.handle_key(key(KeyCode::Right)).unwrap();
        let context = app.macro_context();
        assert_eq!(context.highlight_type, "NUMERIC");
        assert_eq!(context.highlight_value, "12.5");

        app.handle_key(key(KeyCode::F(2))).unwrap();
        let Mode::Input(mut input) = app.mode.clone() else {
            panic!("numeric cell did not open a value editor");
        };
        input.value = "20".into();
        input.cursor = 2;
        app.accept_input(input).unwrap();
        assert_eq!(app.items[0].numeric_value_for("Hours"), Some(20.0));
    }
}
