use std::time::Instant;

use crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
use ratatui::layout::{Constraint, Layout, Rect};

use crate::commands::AppCommand;
use crate::page::Page;
use crate::state::{
    DeviceKind, DictFocus, EditorTarget, EngineSection, HelpFocus, MinimapMode, Modal,
    OptionsFocus, PatternsColumn, SettingKind,
};
use crate::views::{dict_view, engine_view, help_view, main_view, patterns_view, script_view};

use super::InputContext;

#[derive(Clone, Copy, PartialEq, Eq)]
enum ClickKind {
    Single,
    Double,
}

const DOUBLE_CLICK_MS: u128 = 300;

pub fn handle_mouse(ctx: &mut InputContext, mouse: MouseEvent, term: Rect) {
    let kind = mouse.kind;
    let col = mouse.column;
    let row = mouse.row;

    // Dismiss title screen on any click
    if ctx.app.ui.show_title {
        if matches!(kind, MouseEventKind::Down(MouseButton::Left)) {
            ctx.dispatch(AppCommand::HideTitle);
        }
        return;
    }

    match kind {
        MouseEventKind::Down(MouseButton::Left) => {
            let now = Instant::now();
            let click_kind = match ctx.app.ui.last_click.take() {
                Some((t, c, r)) if now.duration_since(t).as_millis() < DOUBLE_CLICK_MS
                    && c == col && r == row => ClickKind::Double,
                _ => {
                    ctx.app.ui.last_click = Some((now, col, row));
                    ClickKind::Single
                }
            };
            handle_click(ctx, col, row, term, click_kind);
        }
        MouseEventKind::Drag(MouseButton::Left) | MouseEventKind::Moved => {
            handle_editor_drag(ctx, col, row, term);
            handle_script_editor_drag(ctx, col, row, term);
        }
        MouseEventKind::Up(MouseButton::Left) => {
            ctx.app.editor_ctx.mouse_selecting = false;
            ctx.app.script_editor.mouse_selecting = false;
        }
        MouseEventKind::ScrollUp => handle_scroll(ctx, col, row, term, true),
        MouseEventKind::ScrollDown => handle_scroll(ctx, col, row, term, false),
        _ => {}
    }
}

fn padded(term: Rect) -> Rect {
    let h_pad = crate::views::horizontal_padding(term.width);
    Rect {
        x: term.x + h_pad,
        y: term.y + 1,
        width: term.width.saturating_sub(h_pad * 2),
        height: term.height.saturating_sub(2),
    }
}

fn top_level_layout(padded: Rect) -> (Rect, Rect, Rect) {
    let header_height = 3u16;
    let [header, _pad, body, _bpad, footer] = Layout::vertical([
        Constraint::Length(header_height),
        Constraint::Length(1),
        Constraint::Fill(1),
        Constraint::Length(1),
        Constraint::Length(3),
    ])
    .areas(padded);
    (header, body, footer)
}

fn contains(area: Rect, col: u16, row: u16) -> bool {
    col >= area.x && col < area.x + area.width && row >= area.y && row < area.y + area.height
}

fn handle_editor_drag(ctx: &mut InputContext, col: u16, row: u16, term: Rect) {
    if ctx.app.editor_ctx.mouse_selecting {
        handle_editor_mouse(ctx, col, row, term, true);
    }
}

fn editor_modal_rect(term: Rect) -> Rect {
    let width = (term.width * 80 / 100).max(40);
    let height = (term.height * 60 / 100).max(10);
    let modal_w = width.min(term.width.saturating_sub(4));
    let modal_h = height.min(term.height.saturating_sub(4));
    let mx = term.x + (term.width.saturating_sub(modal_w)) / 2;
    let my = term.y + (term.height.saturating_sub(modal_h)) / 2;
    Rect::new(mx, my, modal_w, modal_h)
}

fn handle_editor_mouse(ctx: &mut InputContext, col: u16, row: u16, term: Rect, dragging: bool) {
    let modal = editor_modal_rect(term);
    let mx = modal.x;
    let my = modal.y;
    let modal_w = modal.width;
    let modal_h = modal.height;
    // inner = area inside 1-cell border
    let inner_x = mx + 1;
    let inner_y = my + 1;
    let inner_w = modal_w.saturating_sub(2);
    let inner_h = modal_h.saturating_sub(2);

    let show_search = ctx.app.editor_ctx.editor.search_active()
        || !ctx.app.editor_ctx.editor.search_query().is_empty();
    let reserved = 1 + if show_search { 1 } else { 0 };
    let editor_y = inner_y + if show_search { 1 } else { 0 };
    let editor_h = inner_h.saturating_sub(reserved);

    if col < inner_x || col >= inner_x + inner_w || row < editor_y || row >= editor_y + editor_h {
        return;
    }

    let scroll = ctx.app.editor_ctx.editor.scroll_offset();
    let text_row = (row - editor_y) + scroll;
    let text_col = col - inner_x;

    if dragging {
        if !ctx.app.editor_ctx.editor.is_selecting() {
            ctx.app.editor_ctx.editor.start_selection();
        }
    } else {
        ctx.app.editor_ctx.mouse_selecting = true;
        ctx.app.editor_ctx.editor.cancel_selection();
    }

    ctx.app
        .editor_ctx
        .editor
        .move_cursor_to(text_row, text_col);
}

fn handle_click(ctx: &mut InputContext, col: u16, row: u16, term: Rect, kind: ClickKind) {
    // Sticky minimap intercepts all clicks
    if matches!(ctx.app.ui.minimap, MinimapMode::Sticky) {
        if let Some((gc, gr)) = cagire_ratatui::hit_test_tile(col, row, term) {
            if let Some(page) = Page::at_pos(gc, gr) {
                ctx.dispatch(AppCommand::GoToPage(page));
            }
        }
        ctx.app.ui.dismiss_minimap();
        return;
    }

    ctx.dispatch(AppCommand::ClearStatus);

    // If a modal is active, clicks outside dismiss it (except Editor)
    if !matches!(ctx.app.ui.modal, Modal::None) {
        handle_modal_click(ctx, col, row, term);
        return;
    }

    let padded = padded(term);
    let (header, body, footer) = top_level_layout(padded);

    if contains(header, col, row) {
        handle_header_click(ctx, col, row, header);
    } else if contains(footer, col, row) {
        handle_footer_click(ctx, col, row, footer);
    } else if contains(body, col, row) {
        handle_body_click(ctx, col, row, body, kind);
    }
}

fn handle_scroll(ctx: &mut InputContext, col: u16, row: u16, term: Rect, up: bool) {
    // Modal scroll
    if matches!(ctx.app.ui.modal, Modal::KeybindingsHelp { .. }) {
        if up {
            ctx.dispatch(AppCommand::HelpScrollUp(3));
        } else {
            ctx.dispatch(AppCommand::HelpScrollDown(3));
        }
        return;
    }

    if matches!(ctx.app.ui.modal, Modal::Editor) {
        use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
        let code = if up { KeyCode::Up } else { KeyCode::Down };
        for _ in 0..3 {
            ctx.app
                .editor_ctx
                .editor
                .input(KeyEvent::new(code, KeyModifiers::empty()));
        }
        return;
    }

    if !matches!(ctx.app.ui.modal, Modal::None) {
        return;
    }

    let padded = padded(term);
    let (_header, body, _footer) = top_level_layout(padded);

    if !contains(body, col, row) {
        return;
    }

    match ctx.app.page {
        Page::Main => {
            if up {
                ctx.dispatch(AppCommand::StepUp);
            } else {
                ctx.dispatch(AppCommand::StepDown);
            }
        }
        Page::Script => {
            let [editor_area, _] = script_view::layout(body);
            if contains(editor_area, col, row) {
                use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
                let code = if up { KeyCode::Up } else { KeyCode::Down };
                ctx.app.script_editor.editor.input(KeyEvent::new(code, KeyModifiers::empty()));
            }
        }
        Page::Help => {
            let [topics_area, content_area] = help_view::layout(body);
            if contains(topics_area, col, row) {
                if up {
                    ctx.dispatch(AppCommand::HelpPrevTopic(1));
                } else {
                    ctx.dispatch(AppCommand::HelpNextTopic(1));
                }
            } else if contains(content_area, col, row) {
                if up {
                    ctx.dispatch(AppCommand::HelpScrollUp(3));
                } else {
                    ctx.dispatch(AppCommand::HelpScrollDown(3));
                }
            }
        }
        Page::Dict => {
            let [cat_area, words_area] = dict_view::layout(body);
            if contains(cat_area, col, row) {
                if up {
                    ctx.dispatch(AppCommand::DictPrevCategory);
                } else {
                    ctx.dispatch(AppCommand::DictNextCategory);
                }
            } else if contains(words_area, col, row) {
                if up {
                    ctx.dispatch(AppCommand::DictScrollUp(3));
                } else {
                    ctx.dispatch(AppCommand::DictScrollDown(3));
                }
            }
        }
        Page::Patterns => {
            let [banks_area, patterns_area, _] = patterns_view::layout(body);

            if contains(banks_area, col, row) {
                if up {
                    ctx.app.patterns_nav.column = PatternsColumn::Banks;
                    ctx.app.patterns_nav.move_up_clamped();
                } else {
                    ctx.app.patterns_nav.column = PatternsColumn::Banks;
                    ctx.app.patterns_nav.move_down_clamped();
                }
            } else if contains(patterns_area, col, row) {
                if up {
                    ctx.app.patterns_nav.column = PatternsColumn::Patterns;
                    ctx.app.patterns_nav.move_up_clamped();
                } else {
                    ctx.app.patterns_nav.column = PatternsColumn::Patterns;
                    ctx.app.patterns_nav.move_down_clamped();
                }
            }
        }
        Page::Options => {
            if up {
                ctx.dispatch(AppCommand::OptionsPrevFocus);
            } else {
                ctx.dispatch(AppCommand::OptionsNextFocus);
            }
        }
        Page::Engine => {
            let [left_col, _, _] = engine_view::layout(body);
            if contains(left_col, col, row) {
                match ctx.app.audio.section {
                    EngineSection::Devices => {
                        if ctx.app.audio.device_kind == DeviceKind::Input {
                            if up {
                                ctx.dispatch(AppCommand::AudioInputListUp);
                            } else {
                                ctx.dispatch(AppCommand::AudioInputListDown(1));
                            }
                        } else if up {
                            ctx.dispatch(AppCommand::AudioOutputListUp);
                        } else {
                            ctx.dispatch(AppCommand::AudioOutputListDown(1));
                        }
                    }
                    EngineSection::Settings => {
                        if up {
                            ctx.dispatch(AppCommand::AudioSettingPrev);
                        } else {
                            ctx.dispatch(AppCommand::AudioSettingNext);
                        }
                    }
                    EngineSection::Samples => {}
                }
            }
        }
    }
}

// --- Header ---

fn handle_header_click(ctx: &mut InputContext, col: u16, row: u16, header: Rect) {
    let [logo_area, transport_area, _live, tempo_area, _bank, pattern_area, stats_area] =
        Layout::horizontal([
            Constraint::Length(5),
            Constraint::Min(12),
            Constraint::Length(9),
            Constraint::Min(14),
            Constraint::Fill(1),
            Constraint::Fill(2),
            Constraint::Min(20),
        ])
        .areas(header);

    if contains(logo_area, col, row) {
        ctx.app.ui.minimap = MinimapMode::Sticky;
    } else if contains(transport_area, col, row) {
        ctx.dispatch(AppCommand::TogglePlaying);
    } else if contains(tempo_area, col, row) {
        let tempo = format!("{:.1}", ctx.link.tempo());
        ctx.dispatch(AppCommand::OpenModal(Modal::SetTempo(tempo)));
    } else if contains(pattern_area, col, row) {
        ctx.dispatch(AppCommand::GoToPage(Page::Patterns));
    } else if contains(stats_area, col, row) {
        ctx.dispatch(AppCommand::GoToPage(Page::Engine));
    }
}

// --- Footer ---

fn handle_footer_click(ctx: &mut InputContext, col: u16, row: u16, footer: Rect) {
    let block_inner = Rect {
        x: footer.x + 1,
        y: footer.y + 1,
        width: footer.width.saturating_sub(2),
        height: footer.height.saturating_sub(2),
    };
    if !contains(block_inner, col, row) {
        return;
    }

    let badge_text = match ctx.app.page {
        Page::Main => " MAIN ",
        Page::Patterns => " PATTERNS ",
        Page::Engine => " ENGINE ",
        Page::Options => " OPTIONS ",
        Page::Help => " HELP ",
        Page::Dict => " DICT ",
        Page::Script => " SCRIPT ",
    };
    let badge_end = block_inner.x + badge_text.len() as u16;
    if col < badge_end {
        ctx.app.ui.minimap = MinimapMode::Sticky;
    }
}

// --- Body ---

fn handle_body_click(ctx: &mut InputContext, col: u16, row: u16, body: Rect, kind: ClickKind) {
    // Account for side panel splitting
    let page_area = if ctx.app.panel.visible && ctx.app.panel.side.is_some() {
        if body.width >= 120 {
            let panel_width = body.width * 35 / 100;
            let [main, _side] =
                Layout::horizontal([Constraint::Fill(1), Constraint::Length(panel_width)])
                    .areas(body);
            main
        } else {
            let panel_height = body.height * 40 / 100;
            let [main, _side] =
                Layout::vertical([Constraint::Fill(1), Constraint::Length(panel_height)])
                    .areas(body);
            main
        }
    } else {
        body
    };

    if !contains(page_area, col, row) {
        return;
    }

    match ctx.app.page {
        Page::Main => handle_main_click(ctx, col, row, page_area, kind),
        Page::Patterns => handle_patterns_click(ctx, col, row, page_area, kind),
        Page::Help => handle_help_click(ctx, col, row, page_area),
        Page::Dict => handle_dict_click(ctx, col, row, page_area),
        Page::Options => handle_options_click(ctx, col, row, page_area),
        Page::Engine => handle_engine_click(ctx, col, row, page_area, kind),
        Page::Script => handle_script_click(ctx, col, row, page_area),
    }
}

// --- Main page (grid) ---

fn handle_main_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect, kind: ClickKind) {
    let [main_area, _, _vu_area] = main_view::layout(area);

    if !contains(main_area, col, row) {
        return;
    }

    // Check viz area clicks before sequencer
    if let Some(cmd) = hit_test_main_viz(ctx, col, row, main_area, kind) {
        ctx.dispatch(cmd);
        return;
    }

    let sequencer_area = main_view::sequencer_rect(ctx.app, main_area);

    if !contains(sequencer_area, col, row) {
        return;
    }

    if let Some(step) = hit_test_grid(ctx, col, row, sequencer_area) {
        ctx.dispatch(AppCommand::GoToStep(step));
        if kind == ClickKind::Double {
            ctx.dispatch(AppCommand::OpenModal(Modal::Editor));
        }
    }
}

fn hit_test_main_viz(
    ctx: &InputContext,
    col: u16,
    row: u16,
    main_area: Rect,
    kind: ClickKind,
) -> Option<AppCommand> {
    use crate::state::MainLayout;

    let layout = ctx.app.audio.config.layout;
    let show_scope = ctx.app.audio.config.show_scope;
    let show_spectrum = ctx.app.audio.config.show_spectrum;
    let show_lissajous = ctx.app.audio.config.show_lissajous;
    let show_preview = ctx.app.audio.config.show_preview;

    let has_viz = show_scope || show_spectrum || show_lissajous || show_preview;
    if !has_viz {
        return None;
    }

    // Determine viz area based on layout
    let viz_area = if matches!(layout, MainLayout::Top) {
        // Top layout: render_audio_viz uses only audio panels (no preview)
        let has_audio_viz = show_scope || show_spectrum || show_lissajous;
        if !has_audio_viz {
            return None;
        }
        let mut constraints = Vec::new();
        if has_audio_viz {
            constraints.push(Constraint::Fill(1));
        }
        if show_preview {
            let ph = if has_audio_viz { 10u16 } else { 14 };
            constraints.push(Constraint::Length(ph));
        }
        constraints.push(Constraint::Fill(1));
        let areas = Layout::vertical(&constraints).split(main_area);
        areas[0]
    } else {
        let (viz, _) = main_view::viz_seq_split(main_area, layout, has_viz);
        viz
    };

    if !contains(viz_area, col, row) {
        return None;
    }

    // Build panel list matching render order
    let is_vertical_layout = matches!(layout, MainLayout::Left | MainLayout::Right);
    let mut panels: Vec<&str> = Vec::new();
    if show_scope { panels.push("scope"); }
    if show_spectrum { panels.push("spectrum"); }
    if show_lissajous { panels.push("lissajous"); }

    // Top layout uses render_audio_viz (horizontal only, no preview)
    // Other layouts use render_viz_area (includes preview, vertical if Left/Right)
    if !matches!(layout, MainLayout::Top) && show_preview {
        panels.push("preview");
    }

    if panels.is_empty() {
        return None;
    }

    let constraints: Vec<Constraint> = panels.iter().map(|_| Constraint::Fill(1)).collect();
    let areas: Vec<Rect> = if is_vertical_layout && !matches!(layout, MainLayout::Top) {
        Layout::vertical(&constraints).split(viz_area).to_vec()
    } else {
        Layout::horizontal(&constraints).split(viz_area).to_vec()
    };

    for (panel, panel_area) in panels.iter().zip(areas.iter()) {
        if contains(*panel_area, col, row) {
            return match *panel {
                "scope" => Some(if kind == ClickKind::Double {
                    AppCommand::FlipScopeOrientation
                } else {
                    AppCommand::CycleScopeMode
                }),
                "lissajous" => Some(AppCommand::ToggleLissajousTrails),
                "spectrum" => Some(if kind == ClickKind::Double {
                    AppCommand::ToggleSpectrumPeaks
                } else {
                    AppCommand::CycleSpectrumMode
                }),
                _ => None,
            };
        }
    }

    None
}

fn hit_test_grid(ctx: &InputContext, col: u16, row: u16, area: Rect) -> Option<usize> {
    let pattern = ctx.app.current_edit_pattern();
    let length = pattern.length;
    let spp = ctx.app.editor_ctx.steps_per_page.get();
    let page = ctx.app.editor_ctx.step / spp;
    let page_start = page * spp;
    let steps_on_page = (page_start + spp).min(length) - page_start;

    for (tile_rect, step_offset) in main_view::grid_layout(area, steps_on_page) {
        if contains(tile_rect, col, row) {
            let step_idx = page_start + step_offset;
            if step_idx < length {
                return Some(step_idx);
            }
        }
    }

    None
}

// --- Patterns page ---

fn handle_patterns_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect, kind: ClickKind) {
    let [banks_area, patterns_area, _] = patterns_view::layout(area);

    if contains(banks_area, col, row) {
        if let Some(bank) = hit_test_patterns_list(ctx, col, row, banks_area, true) {
            ctx.app.patterns_nav.column = PatternsColumn::Banks;
            ctx.dispatch(AppCommand::PatternsSelectBank(bank));
        }
    } else if contains(patterns_area, col, row) {
        if let Some(pattern) = hit_test_patterns_list(ctx, col, row, patterns_area, false) {
            ctx.app.patterns_nav.column = PatternsColumn::Patterns;
            ctx.dispatch(AppCommand::PatternsSelectPattern(pattern));
            if kind == ClickKind::Double {
                ctx.dispatch(AppCommand::PatternsEnter);
            }
        }
    }
}

fn hit_test_patterns_list(
    ctx: &InputContext,
    _col: u16,
    row: u16,
    area: Rect,
    is_banks: bool,
) -> Option<usize> {
    use crate::model::{MAX_BANKS, MAX_PATTERNS};
    use ratatui::widgets::{Block, Borders};

    let inner = Block::default().borders(Borders::ALL).inner(area);

    let max_items = if is_banks { MAX_BANKS } else { MAX_PATTERNS };
    let cursor = if is_banks {
        ctx.app.patterns_nav.bank_cursor
    } else {
        ctx.app.patterns_nav.pattern_cursor
    };

    let available = inner.height as usize;
    // Patterns column: cursor row takes 2 lines
    let max_visible = if is_banks {
        available.max(1)
    } else {
        available.saturating_sub(1).max(1)
    };

    let scroll_offset = if max_items <= max_visible {
        0
    } else {
        cursor
            .saturating_sub(max_visible / 2)
            .min(max_items - max_visible)
    };

    let visible_count = max_items.min(max_visible);

    if row < inner.y {
        return None;
    }

    if is_banks {
        let row_height = (inner.height / visible_count as u16).max(1);
        let relative_y = row - inner.y;
        let visible_idx = (relative_y / row_height) as usize;
        if visible_idx < visible_count {
            let idx = scroll_offset + visible_idx;
            if idx < max_items {
                return Some(idx);
            }
        }
    } else {
        let mut y = inner.y;
        for visible_idx in 0..visible_count {
            let idx = scroll_offset + visible_idx;
            let row_h: u16 = if idx == cursor { 2 } else { 1 };
            let actual_h = row_h.min(inner.y + inner.height - y);
            if row >= y && row < y + actual_h {
                return Some(idx);
            }
            y += actual_h;
            if y >= inner.y + inner.height {
                break;
            }
        }
    }

    None
}

// --- Help page ---

fn handle_help_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect) {
    let [topics_area, content_area] = help_view::layout(area);

    if contains(topics_area, col, row) {
        use crate::model::docs::{self, DocEntry, DOCS};
        let items = build_visible_list(
            &DOCS
                .iter()
                .map(|e| matches!(e, DocEntry::Section(_)))
                .collect::<Vec<_>>(),
            &ctx.app.ui.help_collapsed,
        );
        let sel = match ctx.app.ui.help_on_section {
            Some(s) => VisibleEntry::Section(s),
            None => VisibleEntry::Item(ctx.app.ui.help_topic),
        };
        if let Some(entry) = hit_test_visible_list(&items, &sel, topics_area, row) {
            match entry {
                VisibleEntry::Item(i) => ctx.dispatch(AppCommand::HelpSelectTopic(i)),
                VisibleEntry::Section(s) => {
                    let collapsed = ctx.app.ui.help_collapsed.get(s).copied().unwrap_or(false);
                    if collapsed {
                        if let Some(v) = ctx.app.ui.help_collapsed.get_mut(s) {
                            *v = false;
                        }
                        ctx.app.ui.help_on_section = None;
                        if let Some(first) = docs::first_topic_in_section(s) {
                            ctx.app.ui.help_topic = first;
                        }
                    } else {
                        if let Some(v) = ctx.app.ui.help_collapsed.get_mut(s) {
                            *v = true;
                        }
                        ctx.app.ui.help_on_section = Some(s);
                    }
                }
            }
        }
        ctx.app.ui.help_focus = HelpFocus::Topics;
    } else if contains(content_area, col, row) {
        ctx.app.ui.help_focus = HelpFocus::Content;
    }
}

// --- Dict page ---

fn handle_dict_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect) {
    let [cat_area, words_area] = dict_view::layout(area);

    if contains(cat_area, col, row) {
        use crate::model::categories::{self, CatEntry, CATEGORIES};
        let items = build_visible_list(
            &CATEGORIES
                .iter()
                .map(|e| matches!(e, CatEntry::Section(_)))
                .collect::<Vec<_>>(),
            &ctx.app.ui.dict_collapsed,
        );
        let sel = match ctx.app.ui.dict_on_section {
            Some(s) => VisibleEntry::Section(s),
            None => VisibleEntry::Item(ctx.app.ui.dict_category),
        };
        if let Some(entry) = hit_test_visible_list(&items, &sel, cat_area, row) {
            match entry {
                VisibleEntry::Item(i) => ctx.dispatch(AppCommand::DictSelectCategory(i)),
                VisibleEntry::Section(s) => {
                    let collapsed = ctx.app.ui.dict_collapsed.get(s).copied().unwrap_or(false);
                    if collapsed {
                        if let Some(v) = ctx.app.ui.dict_collapsed.get_mut(s) {
                            *v = false;
                        }
                        ctx.app.ui.dict_on_section = None;
                        if let Some(first) = categories::first_category_in_section(s) {
                            ctx.app.ui.dict_category = first;
                        }
                    } else {
                        if let Some(v) = ctx.app.ui.dict_collapsed.get_mut(s) {
                            *v = true;
                        }
                        ctx.app.ui.dict_on_section = Some(s);
                    }
                }
            }
        }
        ctx.app.ui.dict_focus = DictFocus::Categories;
    } else if contains(words_area, col, row) {
        ctx.app.ui.dict_focus = DictFocus::Words;
    }
}

// --- CategoryList hit test ---

#[derive(Clone, Copy)]
enum VisibleEntry {
    Item(usize),
    Section(usize),
}

fn build_visible_list(is_section: &[bool], collapsed: &[bool]) -> Vec<VisibleEntry> {
    let mut result = Vec::new();
    let mut section_idx = 0usize;
    let mut item_idx = 0usize;
    let mut skipping = false;
    for &is_sec in is_section {
        if is_sec {
            let is_collapsed = collapsed.get(section_idx).copied().unwrap_or(false);
            result.push(VisibleEntry::Section(section_idx));
            skipping = is_collapsed;
            section_idx += 1;
        } else if !skipping {
            result.push(VisibleEntry::Item(item_idx));
            item_idx += 1;
        } else {
            item_idx += 1;
        }
    }
    result
}

fn hit_test_visible_list(
    items: &[VisibleEntry],
    selected: &VisibleEntry,
    area: Rect,
    click_row: u16,
) -> Option<VisibleEntry> {
    let visible_height = area.height.saturating_sub(2) as usize;
    if visible_height == 0 {
        return None;
    }
    let total_items = items.len();

    let selected_visual_idx = match selected {
        VisibleEntry::Item(sel) => items
            .iter()
            .position(|v| matches!(v, VisibleEntry::Item(i) if *i == *sel))
            .unwrap_or(0),
        VisibleEntry::Section(sel) => items
            .iter()
            .position(|v| matches!(v, VisibleEntry::Section(s) if *s == *sel))
            .unwrap_or(0),
    };

    let scroll = if selected_visual_idx < visible_height / 2 {
        0
    } else if selected_visual_idx > total_items.saturating_sub(visible_height / 2) {
        total_items.saturating_sub(visible_height)
    } else {
        selected_visual_idx.saturating_sub(visible_height / 2)
    };

    let inner_y = area.y + 1;
    if click_row < inner_y {
        return None;
    }
    let relative = (click_row - inner_y) as usize;
    if relative >= visible_height {
        return None;
    }

    let visual_idx = scroll + relative;
    if visual_idx >= total_items {
        return None;
    }

    Some(items[visual_idx])
}

// --- Options page ---

fn handle_options_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect) {
    // Replicate options_view layout: Block with borders → inner → padded (+2, +1)
    let inner = Rect {
        x: area.x + 1,
        y: area.y + 1,
        width: area.width.saturating_sub(2),
        height: area.height.saturating_sub(2),
    };
    let padded = Rect {
        x: inner.x + 2,
        y: inner.y + 1,
        width: inner.width.saturating_sub(4),
        height: inner.height.saturating_sub(2),
    };

    if row < padded.y || row >= padded.y + padded.height {
        return;
    }

    let focus = ctx.app.options.focus;
    let plugin_mode = ctx.app.plugin_mode;
    let focus_line = focus.line_index(plugin_mode);
    let total_lines = crate::state::options::total_lines(plugin_mode);
    let max_visible = padded.height as usize;

    let scroll_offset = if total_lines <= max_visible {
        0
    } else {
        focus_line
            .saturating_sub(max_visible / 2)
            .min(total_lines.saturating_sub(max_visible))
    };

    let relative_y = (row - padded.y) as usize;
    let abs_line = scroll_offset + relative_y;

    if let Some(new_focus) = OptionsFocus::at_line(abs_line, plugin_mode) {
        ctx.dispatch(AppCommand::OptionsSetFocus(new_focus));

        // Value area starts at prefix(2) + label(20) = offset 22 from padded.x
        let value_x = padded.x + 22;
        if col >= value_x {
            let right = col >= value_x + 4; // past the "< " prefix → right half
            super::options_page::cycle_option_value(ctx, right);
        }
    }
}

// --- Engine page ---

fn handle_script_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect) {
    let [editor_area, _] = script_view::layout(area);
    if contains(editor_area, col, row) {
        ctx.app.script_editor.focused = true;
        handle_script_editor_mouse(ctx, col, row, area, false);
    } else {
        ctx.app.script_editor.focused = false;
    }
}

fn script_editor_text_area(area: Rect) -> Rect {
    let [editor_area, _] = script_view::layout(area);
    // Block with borders → inner
    let inner = Rect {
        x: editor_area.x + 1,
        y: editor_area.y + 1,
        width: editor_area.width.saturating_sub(2),
        height: editor_area.height.saturating_sub(2),
    };
    // Editor takes all but last row (hint line)
    let editor_height = inner.height.saturating_sub(1);
    Rect::new(inner.x, inner.y, inner.width, editor_height)
}

fn handle_script_editor_drag(ctx: &mut InputContext, col: u16, row: u16, term: Rect) {
    if ctx.app.script_editor.mouse_selecting {
        let padded = padded(term);
        let (_header, body, _footer) = top_level_layout(padded);
        let page_area = if ctx.app.panel.visible && ctx.app.panel.side.is_some() {
            if body.width >= 120 {
                let panel_width = body.width * 35 / 100;
                Layout::horizontal([Constraint::Fill(1), Constraint::Length(panel_width)])
                    .split(body)[0]
            } else {
                let panel_height = body.height * 40 / 100;
                Layout::vertical([Constraint::Fill(1), Constraint::Length(panel_height)])
                    .split(body)[0]
            }
        } else {
            body
        };
        handle_script_editor_mouse(ctx, col, row, page_area, true);
    }
}

fn handle_script_editor_mouse(
    ctx: &mut InputContext,
    col: u16,
    row: u16,
    area: Rect,
    dragging: bool,
) {
    let text_area = script_editor_text_area(area);

    if col < text_area.x
        || col >= text_area.x + text_area.width
        || row < text_area.y
        || row >= text_area.y + text_area.height
    {
        return;
    }

    let scroll = ctx.app.script_editor.editor.scroll_offset();
    let text_row = (row - text_area.y) + scroll;
    let text_col = col - text_area.x;

    if dragging {
        if !ctx.app.script_editor.editor.is_selecting() {
            ctx.app.script_editor.editor.start_selection();
        }
    } else {
        ctx.app.script_editor.mouse_selecting = true;
        ctx.app.script_editor.editor.cancel_selection();
    }

    ctx.app.script_editor.editor.move_cursor_to(text_row, text_col);
}

fn handle_engine_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect, kind: ClickKind) {
    let [left_col, _, right_col] = engine_view::layout(area);

    // Viz panel clicks (right column)
    if contains(right_col, col, row) {
        let [scope_area, _, lissajous_area, _, spectrum_area] = Layout::vertical([
            Constraint::Fill(1),
            Constraint::Length(1),
            Constraint::Fill(1),
            Constraint::Length(1),
            Constraint::Fill(1),
        ])
        .areas(right_col);

        if contains(scope_area, col, row) {
            if kind == ClickKind::Double {
                ctx.dispatch(AppCommand::FlipScopeOrientation);
            } else {
                ctx.dispatch(AppCommand::CycleScopeMode);
            }
        } else if contains(lissajous_area, col, row) {
            ctx.dispatch(AppCommand::ToggleLissajousTrails);
        } else if contains(spectrum_area, col, row) {
            if kind == ClickKind::Double {
                ctx.dispatch(AppCommand::ToggleSpectrumPeaks);
            } else {
                ctx.dispatch(AppCommand::CycleSpectrumMode);
            }
        }
        return;
    }

    if !contains(left_col, col, row) {
        return;
    }

    // Replicate engine_view render_settings_section layout
    let inner = Rect {
        x: left_col.x + 1,
        y: left_col.y + 1,
        width: left_col.width.saturating_sub(2),
        height: left_col.height.saturating_sub(2),
    };
    let padded = Rect {
        x: inner.x + 1,
        y: inner.y + 1,
        width: inner.width.saturating_sub(2),
        height: inner.height.saturating_sub(1),
    };

    if row < padded.y || row >= padded.y + padded.height {
        return;
    }

    let devices_lines = engine_view::devices_section_height(ctx.app) as usize;
    let settings_lines: usize = 8;
    let samples_lines: usize = 6;
    let total_lines = devices_lines + 1 + settings_lines + 1 + samples_lines;
    let max_visible = padded.height as usize;

    let (focus_start, focus_height) = match ctx.app.audio.section {
        EngineSection::Devices => (0, devices_lines),
        EngineSection::Settings => (devices_lines + 1, settings_lines),
        EngineSection::Samples => (devices_lines + 1 + settings_lines + 1, samples_lines),
    };

    let scroll_offset = if total_lines <= max_visible {
        0
    } else {
        let focus_end = focus_start + focus_height;
        if focus_end <= max_visible {
            0
        } else {
            focus_start.min(total_lines.saturating_sub(max_visible))
        }
    };

    let relative_y = (row - padded.y) as usize;
    let abs_line = scroll_offset + relative_y;

    let devices_end = devices_lines;
    let settings_start = devices_lines + 1;
    let settings_end = settings_start + settings_lines;
    let samples_start = settings_end + 1;

    if abs_line < devices_end {
        ctx.dispatch(AppCommand::AudioSetSection(EngineSection::Devices));
        // Determine output vs input sub-column
        let [output_col, _sep, input_col] = Layout::horizontal([
            Constraint::Percentage(48),
            Constraint::Length(3),
            Constraint::Percentage(48),
        ])
        .areas(padded);
        if contains(input_col, col, row) {
            ctx.dispatch(AppCommand::SetDeviceKind(DeviceKind::Input));
        } else if contains(output_col, col, row) {
            ctx.dispatch(AppCommand::SetDeviceKind(DeviceKind::Output));
        }
    } else if abs_line >= settings_start && abs_line < settings_end {
        ctx.dispatch(AppCommand::AudioSetSection(EngineSection::Settings));
        // Settings section: 2 header lines + 6 table rows
        // Rows 0-3 are adjustable (Channels, Buffer, Voices, Nudge)
        let row_in_section = abs_line - settings_start;
        if row_in_section >= 2 {
            let table_row = row_in_section - 2;
            let setting = match table_row {
                0 => Some(SettingKind::Channels),
                1 => Some(SettingKind::BufferSize),
                2 => Some(SettingKind::Polyphony),
                3 => Some(SettingKind::Nudge),
                _ => None,
            };
            if let Some(kind) = setting {
                ctx.app.audio.setting_kind = kind;
                // Table columns: [Length(14), Fill(1)] — value starts at padded.x + 14
                let value_x = padded.x + 14;
                if col >= value_x {
                    let right = col >= value_x + 4;
                    super::engine_page::cycle_engine_setting(ctx, right);
                }
            }
        }
    } else if abs_line >= samples_start {
        ctx.dispatch(AppCommand::AudioSetSection(EngineSection::Samples));
    }
}

// --- Modal ---

fn handle_modal_click(ctx: &mut InputContext, col: u16, row: u16, term: Rect) {
    match &ctx.app.ui.modal {
        Modal::Editor => {
            let modal_area = editor_modal_rect(term);
            if contains(modal_area, col, row) {
                handle_editor_mouse(ctx, col, row, term, false);
            } else {
                match ctx.app.editor_ctx.target {
                    EditorTarget::Step => {
                        ctx.dispatch(AppCommand::SaveEditorToStep);
                        ctx.dispatch(AppCommand::CompileCurrentStep);
                    }
                    EditorTarget::Prelude => {
                        ctx.dispatch(AppCommand::SavePrelude);
                        ctx.dispatch(AppCommand::EvaluatePrelude);
                        ctx.dispatch(AppCommand::ClosePreludeEditor);
                    }
                }
                ctx.dispatch(AppCommand::CloseModal);
            }
        }
        Modal::Confirm { .. } => {
            handle_confirm_click(ctx, col, row, term);
        }
        Modal::KeybindingsHelp { .. } => {
            let padded = padded(term);
            let width = (padded.width * 80 / 100).clamp(60, 100);
            let height = (padded.height * 80 / 100).max(15);
            let modal_area = centered_rect(term, width, height);
            if !contains(modal_area, col, row) {
                ctx.dispatch(AppCommand::CloseModal);
            }
        }
        _ => {
            let (w, h) = match &ctx.app.ui.modal {
                Modal::PatternProps { .. } => (50, 18),
                Modal::EuclideanDistribution { .. } => (50, 11),
                Modal::Onboarding { .. } => (57, 20),
                Modal::FileBrowser(_) | Modal::AddSamplePath(_) => (60, 18),
                Modal::Rename { .. } => (40, 5),
                Modal::SetPattern { .. } | Modal::SetScript { .. } => (45, 5),
                Modal::SetTempo(_) | Modal::JumpToStep(_) => (30, 5),
                _ => return,
            };
            let modal_area = centered_rect(term, w, h);
            if !contains(modal_area, col, row) {
                ctx.dispatch(AppCommand::CloseModal);
            }
        }
    }
}

fn handle_confirm_click(ctx: &mut InputContext, col: u16, row: u16, term: Rect) {
    // The confirm modal is rendered centered. Approximate its area.
    let modal_area = centered_rect(term, 40, 7);
    if !contains(modal_area, col, row) {
        ctx.dispatch(AppCommand::CloseModal);
        return;
    }

    // The confirm modal has two buttons at the bottom row of the inner area
    // Button row is approximately at modal_area.y + modal_area.height - 2
    let button_row = modal_area.y + modal_area.height.saturating_sub(3);
    if row == button_row || row == button_row + 1 {
        if let Modal::Confirm { selected, .. } = &mut ctx.app.ui.modal {
            let mid = modal_area.x + modal_area.width / 2;
            *selected = col < mid;
        }
    }
}

fn centered_rect(term: Rect, width: u16, height: u16) -> Rect {
    let x = term.x + term.width.saturating_sub(width) / 2;
    let y = term.y + term.height.saturating_sub(height) / 2;
    Rect {
        x,
        y,
        width: width.min(term.width),
        height: height.min(term.height),
    }
}
