use super::{
    PopupSize, PreviewHeaderInfo, build_list_widget, build_tab_spans, draw_confirm_popup,
    draw_corner_watermark, draw_dim_vline, draw_popup_frame, draw_status_bar, draw_template_popup,
    draw_view_title_bar, draw_view_title_bar_with_tabs, ext_badge, format_keybind_hints,
    format_relative_time, list_state_selected, popup_block, popup_hint_line,
};
use crate::app::{App, VIRTUAL_PINNED_LABEL, VIRTUAL_PINNED_PATH, VIRTUAL_SMART_PATH, ViewMode};
use crate::app_theme::AppThemeColors;
use crate::keybinds::ListAction;
use ratatui::{prelude::*, widgets::*};

const GRID_TILE_W: u16 = 10; // outer width incl. border
const GRID_TILE_H: u16 = 5; // outer height incl. border
const GRID_GAP: u16 = 1; // space between tiles (h and v)
const GRID_LEFT_MARGIN: u16 = 2; // left inset inside list_area
const GRID_TOP_MARGIN: u16 = 3; // top inset inside list_area

/// Compute the per-section rectangles within the calendar strip area.
pub(crate) fn section_rects(cal_rect: Rect, active: &[crate::config::NotesSection]) -> Vec<Rect> {
    match active.len() {
        0 => Vec::new(),
        1 => {
            let w = cal_rect.width / 2;
            let x = cal_rect.x + (cal_rect.width - w) / 2;
            vec![Rect::new(x, cal_rect.y, w, cal_rect.height)]
        }
        _ => {
            let cs = Layout::default()
                .direction(Direction::Horizontal)
                .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
                .split(cal_rect);
            vec![cs[0], cs[1]]
        }
    }
}

fn draw_strip_draw(
    frame: &mut Frame,
    rect: Rect,
    app: &App,
    bottom_border: bool,
    strip_rect: Rect,
) {
    use ratatui::widgets::{Block, Borders};

    // Strip-wide border so that a single centered section still gets a
    // full-width border.
    let borders = if bottom_border {
        Borders::BOTTOM
    } else {
        Borders::TOP
    };
    let strip_block = Block::default()
        .borders(borders)
        .border_style(ratatui::style::Style::default().fg(app.app_theme.border));
    frame.render_widget(&strip_block, strip_rect);
    let inner = strip_block.inner(strip_rect);

    // Content area = section rect clipped to the border's inner area.
    let content_x = rect.x.max(inner.x);
    let content_y = rect.y.max(inner.y);
    let content_w = (rect.right().min(inner.right())).saturating_sub(content_x);
    let content_h = (rect.bottom().min(inner.bottom())).saturating_sub(content_y);
    let content_area = Rect::new(content_x, content_y, content_w, content_h);

    let (_, data) = match app.draw_preview.as_ref() {
        Some(pair) => pair,
        None => {
            let line = popup_hint_line(&app.app_theme, "No .draw file");
            let p = ratatui::widgets::Paragraph::new(line)
                .alignment(ratatui::layout::Alignment::Center);
            frame.render_widget(p, content_area);
            return;
        }
    };
    if data.elements.is_empty() || content_area.width == 0 || content_area.height == 0 {
        let line = popup_hint_line(&app.app_theme, "No .draw file");
        let p =
            ratatui::widgets::Paragraph::new(line).alignment(ratatui::layout::Alignment::Center);
        frame.render_widget(p, content_area);
        return;
    }
    let grid = crate::snapshot::render_draw_snapshot_with_size(
        data,
        &app.app_theme,
        content_area.width,
        content_area.height,
    );
    frame.render_widget(crate::snapshot::RenderedSnapshot::new(&grid), content_area);
}

fn draw_strip_graf(
    frame: &mut Frame,
    rect: Rect,
    app: &mut App,
    bottom_border: bool,
    strip_rect: Rect,
) {
    match app.graph_preview.as_mut() {
        Some(gs) => {
            // Progressive settle: 10 steps per frame to avoid blocking
            if !gs.is_settled && app.graph_preview_steps < 100 {
                for _ in 0..10 {
                    crate::graf::physics::simulation_step(gs, 0.01, 0.016);
                    app.graph_preview_steps += 1;
                    if gs.is_settled {
                        break;
                    }
                }
            }
            let (wx_min, wx_max, wy_min, wy_max) = gs.graph_bounds;
            if wx_max - wx_min <= 0.0 || wy_max - wy_min <= 0.0 {
                return;
            }

            // Border spans the full strip rect (so it extends across the
            // entire strip even when a single section is centered)
            let borders = if bottom_border {
                Borders::BOTTOM
            } else {
                Borders::TOP
            };
            let block = Block::default()
                .borders(borders)
                .border_style(Style::default().fg(app.app_theme.border));
            let outer_inner = block.inner(strip_rect);
            frame.render_widget(block, strip_rect);

            // Content area = intersection of section rect with border inner
            let content_x = rect.x.max(outer_inner.x);
            let content_y = rect.y.max(outer_inner.y);
            let content_w = (rect.right().min(outer_inner.right())).saturating_sub(content_x);
            let content_h = (rect.bottom().min(outer_inner.bottom())).saturating_sub(content_y);
            if content_w == 0 || content_h == 0 {
                return;
            }
            let iw = content_w as usize;
            let ih = content_h as usize;
            let sub_h = ih * 2; // 2× vertical sub-pixels for halfblocks
            let world_w = wx_max - wx_min;
            let world_h = wy_max - wy_min;

            // Preserve world aspect ratio — constrain the tighter axis and center
            let av_cols = content_w as f64;
            let av_rows_sub = sub_h as f64;
            let world_aspect = world_w / world_h;
            let grid_aspect = av_cols / av_rows_sub;
            let (draw_cols, draw_rows_sub) = if grid_aspect > world_aspect {
                // grid wider → constrain horizontal
                (av_rows_sub * world_aspect, av_rows_sub)
            } else {
                // grid taller → constrain vertical
                (av_cols, av_cols / world_aspect)
            };
            let col_off = ((av_cols - draw_cols) / 2.0) as isize;
            let row_off = ((av_rows_sub - draw_rows_sub) / 2.0) as isize;

            let world_to_col = |x: f64| -> usize {
                let t = (x - wx_min) / world_w;
                let v = (t * draw_cols).floor() as isize + col_off;
                v.clamp(0, (iw as isize) - 1) as usize
            };
            let world_to_subrow = |y: f64| -> usize {
                let t = (wy_max - y) / world_h;
                let v = (t * draw_rows_sub).floor() as isize + row_off;
                v.clamp(0, (sub_h as isize) - 1) as usize
            };

            // Build per-node colors via the render cache (respects graf node_color_mode config)
            let graph = gs.simulation.get_graph();
            let colors = app.config.theme_colors();
            let mut cache = gs.render_cache.lock();
            if cache.topology_dirty {
                cache.rebuild_topology(graph, &app.config, &colors, false);
            }

            // Map nodes to sub-pixel grid with per-node colors
            let grid_size = sub_h * iw;
            let mut grid: Vec<Option<Color>> = Vec::with_capacity(grid_size);
            grid.resize(grid_size, None);

            for idx in graph.node_indices() {
                let node = &graph[idx];
                let color = cache
                    .node_own_color
                    .get(&idx)
                    .copied()
                    .unwrap_or(Color::Gray);
                let col = world_to_col(node.location.x as f64);
                let sub_row = world_to_subrow(node.location.y as f64);
                grid[sub_row * iw + col] = Some(color);
            }
            drop(cache);

            // Render using halfblocks (▀ top-half, ▄ bottom-half)
            let buf = frame.buffer_mut();
            for cell_row in 0..ih {
                let top_sub = cell_row * 2;
                let bot_sub = cell_row * 2 + 1;
                for col in 0..iw {
                    let top_color = grid[top_sub * iw + col];
                    let bot_color = grid[bot_sub * iw + col];
                    let x = content_x + col as u16;
                    let y = content_y + cell_row as u16;
                    let cell = match buf.cell_mut((x, y)) {
                        Some(c) => c,
                        None => continue,
                    };
                    match (top_color, bot_color) {
                        (None, None) => {}
                        (Some(tc), None) => {
                            cell.set_symbol("▀");
                            cell.set_style(Style::default().fg(tc));
                        }
                        (None, Some(bc)) => {
                            cell.set_symbol("▄");
                            cell.set_style(Style::default().fg(bc));
                        }
                        (Some(tc), Some(bc)) => {
                            cell.set_symbol("▄");
                            cell.set_style(Style::default().fg(bc).bg(tc));
                        }
                    }
                }
            }
        }
        None => {
            let line = popup_hint_line(&app.app_theme, "Graph unavailable");
            let p = ratatui::widgets::Paragraph::new(line)
                .alignment(ratatui::layout::Alignment::Center);
            frame.render_widget(p, rect);
        }
    }
}

pub fn draw_list_view(frame: &mut Frame, app: &mut App) {
    let area = frame.area();
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Length(1),
            Constraint::Min(5),
            Constraint::Length(1),
        ])
        .split(area);
    let title = if app.layout_edit {
        "Notes - Editing Layout"
    } else {
        "Notes"
    };
    let in_select_mode = app.list.list_mode == crate::list_view::ListMode::Select
        || app.list.tag_to_assign.is_some();
    if in_select_mode {
        let mode_label = if app.list.tag_to_assign.is_some() {
            "TAG MODE"
        } else {
            "SELECT MODE"
        };
        let badge_text = format!(
            " {} \u{2014} {} selected ",
            mode_label,
            app.list.selected_indices.len()
        );
        let bar = Paragraph::new(Span::styled(
            badge_text,
            Style::default()
                .fg(app.app_theme.highlight_fg)
                .bg(app.app_theme.accent)
                .add_modifier(Modifier::BOLD),
        ))
        .alignment(Alignment::Center);
        frame.render_widget(bar, chunks[0]);
    } else if app.preview_fullscreen {
        let preview_info = get_preview_info(app);
        draw_view_title_bar(
            frame,
            chunks[0],
            title,
            &app.app_theme,
            preview_info,
            Some(app.status.as_ref()),
            None,
        );
    } else if app.list.notes_layout == crate::config::NotesLayout::Grid {
        let mut tabs = vec![
            (
                "Vault",
                Some(crate::ui::get_icon(
                    "\u{f07b}",
                    "\u{1f4c1}",
                    app.config.ui.icon_mode,
                )),
            ),
            (
                "Pinned",
                Some(crate::ui::get_icon(
                    "\u{f4cc}",
                    "\u{1f4cc}",
                    app.config.ui.icon_mode,
                )),
            ),
        ];
        if app.config.list.smart_folders_enabled {
            tabs.push((
                "Smart",
                Some(crate::ui::get_icon(
                    "\u{f0e7}",
                    "\u{26a1}",
                    app.config.ui.icon_mode,
                )),
            ));
        }
        let selected_idx = if app.list.grid_folder == VIRTUAL_PINNED_PATH {
            1
        } else if app.list.grid_folder == VIRTUAL_SMART_PATH
            || app.list.grid_folder.starts_with('@')
        {
            2
        } else {
            0
        };
        let tab_spans = build_tab_spans(
            &tabs,
            selected_idx,
            &app.app_theme,
            app.config.ui.tab_icons_only,
            app.config.ui.icon_mode,
        );
        let detail_text = if let Some(crate::app::VisualItem::Note { summary_idx, .. }) =
            app.list.visual_list.get(app.list.visual_index)
        {
            let s = &app.notes[*summary_idx];
            let mut spans = Vec::new();

            let when = format_relative_time(s.updated_at);
            spans.push(Span::styled(
                format!(
                    " {} ",
                    crate::ui::get_icon("\u{f017}", "\u{23f0}", app.config.ui.icon_mode)
                ),
                Style::default().fg(app.app_theme.muted),
            ));
            spans.push(Span::styled(
                when.into_owned(),
                Style::default().fg(app.app_theme.muted),
            ));

            if !s.tags.is_empty() {
                spans.push(Span::raw(" | "));
                spans.push(Span::styled(
                    format!(
                        "{} ",
                        crate::ui::get_icon("\u{f02b}", "\u{1f3f7}", app.config.ui.icon_mode)
                    ),
                    Style::default()
                        .fg(app.app_theme.tag)
                        .add_modifier(Modifier::BOLD),
                ));
                spans.push(Span::styled(
                    s.tags.join(", "),
                    Style::default().fg(app.app_theme.fg),
                ));
            }
            spans.push(Span::raw(" ")); // padding right
            Some(Line::from(spans))
        } else if let Some(crate::app::VisualItem::Folder {
            name, note_count, ..
        }) = app.list.visual_list.get(app.list.visual_index)
            && name != ".."
        {
            let mut spans = Vec::new();
            let suffix = if *note_count == 1 { "note" } else { "notes" };
            spans.push(Span::styled(
                format!(
                    " {} ",
                    crate::ui::get_icon("\u{f0ca}", "\u{1f4cb}", app.config.ui.icon_mode)
                ),
                Style::default().fg(app.app_theme.folder),
            ));
            spans.push(Span::styled(
                format!("{note_count} {suffix}"),
                Style::default().fg(app.app_theme.fg),
            ));
            spans.push(Span::raw(" ")); // padding right
            Some(Line::from(spans))
        } else if let Some(crate::app::VisualItem::SmartFolder { note_count, .. }) =
            app.list.visual_list.get(app.list.visual_index)
        {
            let mut spans = Vec::new();
            let suffix = if *note_count == 1 { "note" } else { "notes" };
            spans.push(Span::styled(
                format!(
                    " {} ",
                    crate::ui::get_icon("\u{f0ca}", "\u{1f4cb}", app.config.ui.icon_mode)
                ),
                Style::default().fg(app.app_theme.tag),
            ));
            spans.push(Span::styled(
                format!("{note_count} {suffix}"),
                Style::default().fg(app.app_theme.fg),
            ));
            spans.push(Span::raw(" ")); // padding right
            Some(Line::from(spans))
        } else {
            None
        };

        draw_view_title_bar_with_tabs(
            frame,
            chunks[0],
            title,
            tab_spans,
            &app.app_theme,
            Some(app.status.as_ref()),
            detail_text,
        );
    } else {
        draw_view_title_bar(
            frame,
            chunks[0],
            title,
            &app.app_theme,
            None,
            Some(app.status.as_ref()),
            None,
        );
    }

    let (list_area, preview_area, calendar_area) = list_view_layout(
        area,
        app.list.preview_enabled,
        app.preview_position,
        app.list.calendar_enabled,
        app.preview_fullscreen,
        app.list.preview_width_ratio,
        app.list.calendar_height,
        app.calendar_position,
    );
    if let Some(p) = preview_area {
        app.list.last_preview_pane_width = p.width;
        app.list.last_preview_pane_height = p.height;
    }

    if !app.preview_fullscreen {
        let is_grid = app.list.notes_layout == crate::config::NotesLayout::Grid;
        let mut items: Vec<ListItem> = Vec::new();

        if is_grid {
            app.list.grid_tiles.clear();

            // --- render directory breadcrumbs at the top of the list area ---
            let is_pinned = app.list.grid_folder == VIRTUAL_PINNED_PATH;
            let is_smart =
                app.list.grid_folder == VIRTUAL_SMART_PATH || app.list.grid_folder.starts_with('@');
            let mut spans = Vec::new();
            if is_pinned {
                spans.push(Span::styled(
                    format!(
                        " {} Pinned",
                        crate::ui::get_icon("\u{f4cc}", "\u{1f4cc}", app.config.ui.icon_mode)
                    ),
                    Style::default()
                        .fg(app.app_theme.heading)
                        .add_modifier(Modifier::BOLD),
                ));
            } else if is_smart {
                spans.push(Span::styled(
                    format!(
                        " {} Smart",
                        crate::ui::get_icon("\u{f0e7}", "\u{26a1}", app.config.ui.icon_mode)
                    ),
                    Style::default()
                        .fg(app.app_theme.tag)
                        .add_modifier(Modifier::BOLD),
                ));
                if app.list.grid_folder.starts_with('@') {
                    let label = if app.list.grid_folder == "@today" {
                        "Today"
                    } else if app.list.grid_folder == "@week" {
                        "This Week"
                    } else if app.list.grid_folder == "@untagged" {
                        "Untagged"
                    } else if let Some(tag) = app.list.grid_folder.strip_prefix("@tag:") {
                        tag
                    } else if let Some(custom) = app.list.grid_folder.strip_prefix("@custom:") {
                        custom
                    } else {
                        &app.list.grid_folder
                    };
                    spans.push(Span::styled(
                        " / ",
                        Style::default().fg(app.app_theme.muted),
                    ));
                    spans.push(Span::styled(
                        label.to_string(),
                        Style::default().fg(app.app_theme.fg),
                    ));
                }
            } else {
                spans.push(Span::styled(
                    format!(
                        " {} Vault",
                        crate::ui::get_icon("\u{f07b}", "\u{1f4c1}", app.config.ui.icon_mode)
                    ),
                    Style::default()
                        .fg(app.app_theme.folder)
                        .add_modifier(Modifier::BOLD),
                ));
                if !app.list.grid_folder.is_empty() {
                    for part in app.list.grid_folder.split('/') {
                        spans.push(Span::styled(
                            " / ",
                            Style::default().fg(app.app_theme.muted),
                        ));
                        spans.push(Span::styled(
                            part.to_string(),
                            Style::default().fg(app.app_theme.fg),
                        ));
                    }
                }
            }
            let dir_rect = Rect::new(list_area.x, list_area.y + 1, list_area.width, 1);
            frame.render_widget(Paragraph::new(Line::from(spans)), dir_rect);

            // --- columns / visible rows ---
            let cols = ((list_area.width.saturating_sub(GRID_LEFT_MARGIN + GRID_GAP))
                / (GRID_TILE_W + GRID_GAP))
                .max(1) as usize;
            let rows = ((list_area.height.saturating_sub(GRID_TOP_MARGIN + GRID_GAP))
                / (GRID_TILE_H + GRID_GAP)) as usize;
            app.list.grid_columns = cols; // events.rs grid nav reads this (Up/Down move by cols)

            let len = app.list.visual_list.len();

            // --- clamp grid_scroll so the selected tile stays visible, without over-scrolling ---
            if cols > 0 && rows > 0 && len > 0 {
                let sel_row = app.list.visual_index / cols;
                if sel_row < app.list.grid_scroll {
                    app.list.grid_scroll = sel_row;
                }
                let last_visible = app.list.grid_scroll + rows.saturating_sub(1);
                if sel_row > last_visible {
                    app.list.grid_scroll = sel_row.saturating_sub(rows.saturating_sub(1));
                }
                let max_scroll = (len - 1) / cols;
                if app.list.grid_scroll > max_scroll {
                    app.list.grid_scroll = max_scroll;
                }
            } else {
                app.list.grid_scroll = 0;
            }

            let start = app.list.grid_scroll * cols;
            let count = (rows * cols).min(len.saturating_sub(start));
            let buf = frame.buffer_mut();

            for i in 0..count {
                let vi = start + i;
                if vi >= len {
                    break;
                }
                let row = i / cols;
                let col = i % cols;
                let tile_rect = ratatui::layout::Rect::new(
                    list_area.x + GRID_LEFT_MARGIN + (col as u16) * (GRID_TILE_W + GRID_GAP),
                    list_area.y + GRID_TOP_MARGIN + (row as u16) * (GRID_TILE_H + GRID_GAP),
                    GRID_TILE_W,
                    GRID_TILE_H,
                );
                let is_selected = vi == app.list.visual_index;
                let in_selection = app.list.selected_indices.contains(&vi);

                // --- resolve (icon char, glyph color, display name): SAME mapping the old code used ---
                let item = &app.list.visual_list[vi];
                let (icon_char, text_label, glyph_color, raw_name) = match item {
                    crate::app::VisualItem::Folder { name, .. } => {
                        let is_pinned = name == VIRTUAL_PINNED_LABEL;
                        let is_parent = name == "..";
                        let (ic, label) = if is_pinned {
                            (
                                crate::ui::get_char(
                                    '\u{f4cc}',
                                    '\u{1f4cc}',
                                    app.config.ui.icon_mode,
                                ),
                                "F",
                            )
                        } else if is_parent {
                            (
                                crate::ui::get_char(
                                    '\u{f062}',
                                    '\u{2b06}',
                                    app.config.ui.icon_mode,
                                ),
                                "^",
                            )
                        } else {
                            (
                                crate::ui::get_char(
                                    '\u{f07b}',
                                    '\u{1f4c1}',
                                    app.config.ui.icon_mode,
                                ),
                                "F",
                            )
                        };
                        let col = if is_pinned {
                            app.app_theme.heading
                        } else {
                            app.app_theme.folder
                        };
                        (ic, label, col, name.clone())
                    }
                    crate::app::VisualItem::SmartFolder { kind, label, .. } => {
                        let (ic, text_label) = match kind {
                            crate::list_view::SmartFolderKind::Today => (
                                crate::ui::get_char(
                                    '\u{f133}',
                                    '\u{1f4c5}',
                                    app.config.ui.icon_mode,
                                ),
                                "Today",
                            ),
                            crate::list_view::SmartFolderKind::ThisWeek => (
                                crate::ui::get_char(
                                    '\u{f073}',
                                    '\u{1f5d3}',
                                    app.config.ui.icon_mode,
                                ),
                                "Week",
                            ),
                            crate::list_view::SmartFolderKind::Untagged => (
                                crate::ui::get_char(
                                    '\u{f187}',
                                    '\u{1f4e5}',
                                    app.config.ui.icon_mode,
                                ),
                                "Untag",
                            ),
                            crate::list_view::SmartFolderKind::Tag(_) => (
                                crate::ui::get_char(
                                    '\u{f02c}',
                                    '\u{1f3f7}',
                                    app.config.ui.icon_mode,
                                ),
                                "Tag",
                            ),
                            crate::list_view::SmartFolderKind::Custom(_) => (
                                crate::ui::get_char(
                                    '\u{f0e7}',
                                    '\u{26a1}',
                                    app.config.ui.icon_mode,
                                ),
                                "Custom",
                            ),
                        };
                        (ic, text_label, app.app_theme.tag, label.clone())
                    }
                    crate::app::VisualItem::Note {
                        summary_idx,
                        is_clin,
                        is_draw,
                        is_canvas,
                        ..
                    } => {
                        let s = &app.notes[*summary_idx];
                        let col = if s.pinned {
                            app.app_theme.heading
                        } else if *is_clin {
                            app.app_theme.destructive
                        } else if *is_draw {
                            app.app_theme.success
                        } else if *is_canvas {
                            app.app_theme.accent
                        } else {
                            app.app_theme.text
                        };
                        let ic = if s.pinned {
                            crate::ui::get_char('\u{f4cc}', '\u{1f4cc}', app.config.ui.icon_mode)
                        } else if *is_clin {
                            crate::ui::get_char('\u{f023}', '\u{1f512}', app.config.ui.icon_mode)
                        } else if *is_draw {
                            crate::ui::get_char('\u{f1fc}', '\u{270f}', app.config.ui.icon_mode)
                        } else if *is_canvas {
                            crate::ui::get_char('\u{f005}', '\u{2b50}', app.config.ui.icon_mode)
                        } else {
                            crate::ui::get_char('\u{f15c}', '\u{1f4c4}', app.config.ui.icon_mode)
                        };
                        let label = if *is_clin {
                            "CX"
                        } else if *is_draw {
                            "D"
                        } else if *is_canvas {
                            "C"
                        } else {
                            "MD"
                        };
                        (ic, label, col, s.title.clone())
                    }
                    crate::app::VisualItem::CreateNew { .. } => (
                        crate::ui::get_char('\u{f067}', '\u{2795}', app.config.ui.icon_mode),
                        "+",
                        app.app_theme.success,
                        "Create...".to_string(),
                    ),
                };

                // --- tile border (plain border = "button") ---
                let mut block = Block::default().borders(Borders::ALL);
                // Selected (in multi-select set) tiles get an accent-filled interior; the
                // cursor tile keeps its highlight_bg border on top of any fill so a tile
                // that is both selected and cursor stays distinguishable.
                if in_selection {
                    block = block.style(Style::default().bg(app.app_theme.accent));
                }
                let border_fg = if is_selected {
                    app.app_theme.highlight_bg
                } else if in_selection {
                    app.app_theme.accent
                } else {
                    app.app_theme.border
                };
                block = block.border_style(Style::default().fg(border_fg));
                let inner = block.inner(tile_rect);
                block.render(tile_rect, buf); // paints border
                let icon_fg = if in_selection {
                    app.app_theme.highlight_fg
                } else {
                    glyph_color
                };
                let base_style = if in_selection {
                    Style::default().bg(app.app_theme.accent)
                } else {
                    Style::default()
                };
                let icon_style =
                    base_style
                        .fg(icon_fg)
                        .add_modifier(if is_selected || in_selection {
                            Modifier::BOLD
                        } else {
                            Modifier::empty()
                        });
                buf.set_style(Rect::new(inner.x, inner.y, inner.width, 1), icon_style);
                let inner_w = inner.width as usize;
                if app.config.ui.icon_mode == crate::config::IconMode::None {
                    let label_chars: Vec<char> = text_label.chars().collect();
                    let label_start =
                        inner.x + ((inner_w.saturating_sub(label_chars.len())) / 2) as u16;
                    for (k, ch) in label_chars.iter().enumerate() {
                        if let Some(cell) = buf.cell_mut((label_start + k as u16, inner.y)) {
                            cell.set_char(*ch).set_style(icon_style);
                        }
                    }
                } else {
                    use unicode_width::UnicodeWidthChar;
                    let w = UnicodeWidthChar::width(icon_char).unwrap_or(1) as u16;
                    let icon_x = inner.x + (inner_w.saturating_sub(w as usize) / 2) as u16;
                    buf.set_string(icon_x, inner.y, icon_char.to_string(), icon_style);
                }

                // --- tag icon: top right corner for items that have tags ---
                let has_tags = match item {
                    crate::app::VisualItem::Note { summary_idx, .. } => {
                        !app.notes[*summary_idx].tags.is_empty()
                    }
                    _ => false,
                };
                if has_tags {
                    let tag_x = inner.x + inner.width.saturating_sub(1);
                    let tag_fg = if in_selection {
                        app.app_theme.highlight_fg
                    } else {
                        app.app_theme.tag
                    };
                    let tag_style =
                        base_style
                            .fg(tag_fg)
                            .add_modifier(if is_selected || in_selection {
                                Modifier::BOLD
                            } else {
                                Modifier::empty()
                            });
                    use unicode_width::UnicodeWidthChar;
                    let tg = crate::ui::get_char('\u{f02b}', '\u{1f3f7}', app.config.ui.icon_mode);
                    let tw = UnicodeWidthChar::width(tg).unwrap_or(1) as u16;
                    let tg_x = tag_x.saturating_sub(tw.saturating_sub(1));
                    buf.set_string(tg_x, inner.y, tg.to_string(), tag_style);
                }

                // --- name: sanitize, truncate to inner width, center, write on the bottom row (row 2) ---
                let sanitized = crate::sanitize::sanitize_for_terminal(&raw_name);
                let mut chars: Vec<char> = sanitized.chars().collect();
                if chars.len() > inner_w {
                    chars.truncate(inner_w - 1);
                    chars.push('…');
                }
                let pad = inner_w.saturating_sub(chars.len());
                let left = pad / 2;
                let name_style = if is_selected || in_selection {
                    if in_selection {
                        Style::default()
                            .fg(app.app_theme.highlight_fg)
                            .add_modifier(Modifier::BOLD)
                    } else {
                        Style::default().add_modifier(Modifier::BOLD)
                    }
                } else {
                    Style::default()
                };
                let mut name_string: String = " ".repeat(left);
                name_string.extend(chars.iter());
                let name_row = inner.y + 2; // bottom row of the 3 inner rows
                for (k, ch) in name_string.chars().enumerate() {
                    if let Some(cell) = buf.cell_mut((inner.x + k as u16, name_row)) {
                        cell.set_char(ch).set_style(name_style);
                    }
                }

                // --- record tile for mouse hit-testing ---
                app.list.grid_tiles.push(crate::list_view::GridTile {
                    visual_index: vi,
                    rect: tile_rect,
                });
            }
            // do NOT render a List widget here; do NOT touch list_state (tree view still uses it).
        } else {
            items.reserve(app.list.display_items.len());
            for item in &app.list.display_items {
                items.push(item.clone());
            }
            let list = List::new(items)
                .block(
                    Block::default()
                        .style(app.app_theme.bg_style())
                        .borders(Borders::NONE)
                        .padding(Padding::new(2, 2, 1, 1)),
                )
                .highlight_style(
                    Style::default()
                        .fg(app.app_theme.highlight_fg)
                        .bg(app.app_theme.highlight_bg)
                        .add_modifier(Modifier::BOLD),
                );

            app.list.list_state.select(Some(app.list.visual_index));
            frame.render_stateful_widget(list, list_area, &mut app.list.list_state);
        }
    }
    if let Some(preview_rect) = preview_area {
        let hide_encrypted = app.preview_encryption
            && app
                .list
                .visual_list
                .get(app.list.visual_index)
                .is_some_and(|item| {
                    matches!(item, crate::app::VisualItem::Note { is_clin: true, .. })
                });

        let content_is_current = app.list.preview_content_index == Some(app.list.visual_index);
        let content = if content_is_current || app.list.pending_preview_update {
            app.list.preview_content.as_ref()
        } else {
            None
        };

        crate::preview::draw_preview_pane(
            frame,
            preview_rect,
            &app.app_theme,
            content,
            hide_encrypted,
            app.list.snapshot_scroll_offset,
            app.config.ui.icon_mode,
        );
    }
    if let Some(cal_rect) = calendar_area {
        let bottom_border = app.calendar_position == crate::config::CalendarPosition::Top;
        let active = app.active_strip_sections_for(cal_rect.width);
        let rects = section_rects(cal_rect, &active);
        for (sec, r) in active.iter().zip(rects.iter().copied()) {
            match sec {
                crate::config::NotesSection::Calendar => crate::calendar::draw_calendar(
                    frame,
                    r,
                    &app.app_theme,
                    &app.notes,
                    bottom_border,
                    app.config.list.week_start,
                    cal_rect,
                ),
                crate::config::NotesSection::Goals => {
                    let _ = app.get_current_goals_progress();
                    crate::goals::draw_goals_progress(
                        frame,
                        r,
                        &app.app_theme,
                        &app.goals_progress,
                        &app.config.goals,
                        bottom_border,
                        cal_rect,
                    );
                }
                crate::config::NotesSection::Draw => {
                    app.ensure_draw_preview();
                    draw_strip_draw(frame, r, app, bottom_border, cal_rect);
                }
                crate::config::NotesSection::Graf => {
                    app.ensure_graph_preview();
                    draw_strip_graf(frame, r, app, bottom_border, cal_rect);
                }
            }
        }
    }
    let kb = &app.keybinds;
    let is_grid = app.list.notes_layout == crate::config::NotesLayout::Grid;
    let hints_items = if is_grid {
        vec![
            (
                format!(
                    "{}/{}/{}/{}",
                    kb.display_list(ListAction::MoveLeft),
                    kb.display_list(ListAction::MoveDown),
                    kb.display_list(ListAction::MoveUp),
                    kb.display_list(ListAction::MoveRight)
                ),
                "move",
            ),
            (kb.display_list(ListAction::Open), "open"),
            (kb.display_list(ListAction::Help), "help"),
            (kb.display_list(ListAction::Quit), "quit"),
        ]
    } else {
        vec![
            (
                format!(
                    "{}/{}",
                    kb.display_list(ListAction::MoveDown),
                    kb.display_list(ListAction::MoveUp)
                ),
                "move",
            ),
            (kb.display_list(ListAction::Open), "open"),
            (kb.display_list(ListAction::CollapseAll), "collapse"),
            (kb.display_list(ListAction::ExpandAll), "expand"),
            (kb.display_list(ListAction::Help), "help"),
            (kb.display_list(ListAction::Quit), "quit"),
        ]
    };
    let default_hints = format_keybind_hints(&app.app_theme, &hints_items);

    let hint = if in_select_mode {
        if app.list.tag_to_assign.is_some() {
            let tag_items = vec![
                (kb.display_list(ListAction::ToggleSelectItem), "toggle"),
                ("Enter".to_string(), "apply tag"),
                (kb.display_list(ListAction::Cancel), "cancel"),
            ];
            format_keybind_hints(&app.app_theme, &tag_items)
        } else {
            let select_items = vec![
                (kb.display_list(ListAction::ToggleSelectItem), "toggle"),
                (kb.display_list(ListAction::MoveNote), "move"),
                (kb.display_list(ListAction::ManageTags), "tag"),
                (kb.display_list(ListAction::Delete), "delete"),
                (kb.display_list(ListAction::ToggleSelectMode), "exit"),
            ];
            format_keybind_hints(&app.app_theme, &select_items)
        }
    } else if app.layout_edit {
        let layout_items = vec![
            ("drag".to_string(), "borders/panes"),
            ("Tab".to_string(), "swap sections"),
            ("Space/click".to_string(), "cycle section"),
            ("a".to_string(), "add/remove section"),
            ("s".to_string(), "preview"),
            ("c".to_string(), "calendar"),
            ("←→ ↑↓".to_string(), "resize"),
            ("Esc".to_string(), "done"),
        ];
        format_keybind_hints(&app.app_theme, &layout_items)
    } else {
        default_hints
    };
    let badge = Some(ext_badge(
        app.editor.external_editor_enabled,
        &app.app_theme,
    ));
    draw_status_bar(
        frame,
        chunks[2],
        &app.app_theme,
        badge,
        hint,
        None,
        app.seq_matcher.pending_display().as_deref(),
    );
    draw_corner_watermark(frame, chunks[2], app.app_theme.muted);
    if app.list.preview_enabled && !app.preview_fullscreen {
        let ratio_num = (app.list.preview_width_ratio.clamp(0.2, 0.8) * 100.0).round() as u32;
        let constraints = match app.preview_position {
            crate::config::PreviewPosition::Left => [
                Constraint::Ratio(ratio_num, 100),
                Constraint::Length(1),
                Constraint::Min(0),
            ],
            crate::config::PreviewPosition::Right => [
                Constraint::Min(0),
                Constraint::Length(1),
                Constraint::Ratio(ratio_num, 100),
            ],
        };
        let full_cols = Layout::default()
            .direction(Direction::Horizontal)
            .constraints(constraints)
            .split(chunks[1]);
        if app.layout_edit {
            let divider_area = full_cols[1];
            let accent = app.app_theme.heading;
            let buf = frame.buffer_mut();
            for row in divider_area.top()..divider_area.bottom() {
                if let Some(cell) = buf.cell_mut((divider_area.x, row)) {
                    cell.set_char('║');
                    cell.set_fg(accent);
                }
            }
            let mid_row = divider_area.top() + divider_area.height / 2;
            if let Some(cell) = buf.cell_mut((divider_area.x, mid_row)) {
                cell.set_char('⇄');
                cell.set_fg(accent);
            }
        } else {
            draw_dim_vline(frame, full_cols[1], app.app_theme.muted);
        }
    }
    if app.layout_edit && app.list.calendar_enabled {
        let hdiv_y = match app.calendar_position {
            crate::config::CalendarPosition::Bottom => list_area.y + list_area.height,
            crate::config::CalendarPosition::Top => list_area.y.saturating_sub(1),
        };
        let accent = app.app_theme.heading;
        let buf = frame.buffer_mut();
        for col in list_area.left()..list_area.right() {
            if let Some(cell) = buf.cell_mut((col, hdiv_y)) {
                cell.set_char('═');
                cell.set_fg(accent);
            }
        }
        let mid_col = list_area.left() + list_area.width / 2;
        if let Some(cell) = buf.cell_mut((mid_col, hdiv_y)) {
            cell.set_char('⇅');
            cell.set_fg(accent);
        }
    }

    if let Some(crate::popups::ActivePopup::Template(popup)) = &app.popups.active {
        draw_template_popup(frame, popup, area, &app.app_theme);
    }

    if let Some(crate::popups::ActivePopup::Folder(popup)) = &mut app.popups.active {
        let title = match popup.mode {
            crate::popups::FolderPopupMode::Create { .. } => "NEW FOLDER",
            crate::popups::FolderPopupMode::Rename { .. } => "RENAME FOLDER",
        };
        let hint_line = popup_hint_line(&app.app_theme, "Enter confirm · Esc cancel");
        let content = draw_popup_frame(
            frame,
            area,
            title,
            PopupSize::Prompt,
            &hint_line,
            &app.app_theme,
        );

        popup.input.set_block(
            Block::default()
                .style(app.app_theme.bg_style())
                .borders(Borders::ALL)
                .border_style(Style::default().fg(app.app_theme.heading)),
        );
        frame.render_widget(&popup.input, content);
    }

    if let Some(crate::popups::ActivePopup::Tag(popup)) = &mut app.popups.active {
        let suggestion_height = if popup.suggestions.is_empty() {
            0u16
        } else {
            (popup.suggestions.len() as u16).clamp(1, 5)
        };
        let hint_line = popup_hint_line(
            &app.app_theme,
            "Ctrl+S batch assign · Tab accept · Enter save · d delete from all · Esc cancel",
        );
        let content = draw_popup_frame(
            frame,
            area,
            "TAGS",
            PopupSize::Large,
            &hint_line,
            &app.app_theme,
        );

        let chunks = Layout::default()
            .direction(Direction::Vertical)
            .constraints([
                Constraint::Length(3u16 + suggestion_height),
                Constraint::Min(3),
            ])
            .split(content);

        let input_border = if popup.focus == crate::popups::TagPopupFocus::Input {
            Style::default().fg(app.app_theme.heading)
        } else {
            Style::default().fg(app.app_theme.muted)
        };
        let input_chunks = Layout::default()
            .direction(Direction::Vertical)
            .constraints([Constraint::Length(3), Constraint::Min(0)])
            .split(chunks[0]);

        let input_block = Block::default()
            .style(app.app_theme.bg_style())
            .borders(Borders::ALL)
            .border_style(input_border)
            .title("");
        let input_inner = input_block.inner(input_chunks[0]);
        frame.render_widget(input_block, chunks[0]);
        frame.render_widget(&popup.input, input_inner);

        if !popup.suggestions.is_empty() {
            let suggestion_items: Vec<ListItem> = popup
                .suggestions
                .iter()
                .enumerate()
                .map(|(i, tag)| {
                    let style = if i == popup.suggestion_index {
                        Style::default()
                            .fg(app.app_theme.highlight_fg)
                            .bg(app.app_theme.heading)
                    } else {
                        Style::default()
                    };
                    ListItem::new(format!("  {tag}")).style(style)
                })
                .collect();

            let suggestions_list = List::new(suggestion_items)
                .block(
                    Block::default()
                        .borders(Borders::NONE)
                        .style(app.app_theme.bg_style()),
                )
                .highlight_style(Style::default());

            frame.render_widget(suggestions_list, input_chunks[1]);
        }

        let all_tags_border = if popup.focus == crate::popups::TagPopupFocus::AllTagsList {
            Style::default().fg(app.app_theme.heading)
        } else {
            Style::default().fg(app.app_theme.muted)
        };
        let tag_empty = popup.all_tags.is_empty();
        let tag_items: Vec<ListItem> = if tag_empty {
            vec![ListItem::new(Span::styled(
                "No tags found",
                Style::default().fg(app.app_theme.muted),
            ))]
        } else {
            popup
                .all_tags
                .iter()
                .map(|tag| ListItem::new(tag.to_string()))
                .collect()
        };

        let tags_list = build_list_widget(tag_items, &app.app_theme)
            .block(
                Block::default()
                    .style(app.app_theme.bg_style())
                    .borders(Borders::ALL)
                    .border_style(all_tags_border),
            )
            .highlight_style(
                Style::default()
                    .fg(app.app_theme.highlight_fg)
                    .bg(app.app_theme.highlight_bg)
                    .add_modifier(Modifier::BOLD),
            )
            .highlight_symbol("  ");

        let mut tags_state = list_state_selected(
            (popup.focus == crate::popups::TagPopupFocus::AllTagsList
                && !popup.all_tags.is_empty())
            .then_some(popup.all_tags_selected),
        );
        frame.render_stateful_widget(tags_list, chunks[1], &mut tags_state);
    }

    if let Some(crate::popups::ActivePopup::FolderPicker(picker)) = &mut app.popups.active {
        let title = match picker.mode {
            crate::popups::FolderPickerMode::CopyNote { .. }
            | crate::popups::FolderPickerMode::BulkCopyNotes { .. }
            | crate::popups::FolderPickerMode::BulkCopyFolders { .. }
            | crate::popups::FolderPickerMode::BulkCopyMixed { .. } => "COPY",
            _ => "MOVE",
        };
        let hint_line = popup_hint_line(&app.app_theme, "Tab switch  Enter confirm  Esc cancel");
        let content = draw_popup_frame(
            frame,
            area,
            title,
            PopupSize::Large,
            &hint_line,
            &app.app_theme,
        );

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

        let search_border = if picker.focus == crate::app::FolderPickerFocus::Search {
            Style::default().fg(app.app_theme.heading)
        } else {
            Style::default().fg(app.app_theme.muted)
        };
        picker.input.set_block(
            Block::default()
                .style(app.app_theme.bg_style())
                .borders(Borders::ALL)
                .border_style(search_border)
                .title(""),
        );
        frame.render_widget(&picker.input, chunks[0]);

        let items: Vec<ListItem> = if picker.filtered_folders.is_empty() {
            vec![ListItem::new(Span::styled(
                "(no matching folders)",
                Style::default().fg(app.app_theme.muted),
            ))]
        } else {
            picker
                .filtered_folders
                .iter()
                .map(|f| {
                    let label = if f.is_empty() { "Vault (Root)" } else { f };
                    ListItem::new(label)
                })
                .collect()
        };

        let results_border = if picker.focus == crate::app::FolderPickerFocus::Results {
            Style::default().fg(app.app_theme.heading)
        } else {
            Style::default().fg(app.app_theme.muted)
        };
        let list = List::new(items)
            .block(
                Block::default()
                    .style(app.app_theme.bg_style())
                    .borders(Borders::ALL)
                    .border_style(results_border)
                    .title(""),
            )
            .highlight_style(
                Style::default()
                    .fg(app.app_theme.highlight_fg)
                    .bg(app.app_theme.highlight_bg)
                    .add_modifier(Modifier::BOLD),
            )
            .highlight_symbol("  ");

        let mut state = list_state_selected(
            (picker.focus == crate::app::FolderPickerFocus::Results
                && !picker.filtered_folders.is_empty())
            .then_some(picker.selected),
        );

        frame.render_stateful_widget(list, chunks[1], &mut state);
    }

    if let Some(palette) = &mut app.command_palette {
        let hint_line = popup_hint_line(
            &app.app_theme,
            "Tab category · Enter run · ↑/↓ select · Esc close",
        );
        let content = draw_popup_frame(
            frame,
            area,
            "COMMANDS",
            PopupSize::Large,
            &hint_line,
            &app.app_theme,
        );

        let chunks = Layout::default()
            .direction(Direction::Vertical)
            .constraints([
                Constraint::Length(3), // search input
                Constraint::Length(1), // tab bar
                Constraint::Min(0),    // results list
            ])
            .split(content);

        palette.input.set_block(
            Block::default()
                .style(app.app_theme.bg_style())
                .borders(Borders::ALL)
                .border_style(Style::default().fg(app.app_theme.muted))
                .title(""),
        );
        frame.render_widget(&palette.input, chunks[0]);

        let tabs: Vec<(&str, Option<&str>)> = crate::palette::palette_tabs(app.config.ui.icon_mode)
            .iter()
            .map(|(l, g, _)| (*l, Some(*g)))
            .collect();
        let tab_spans = build_tab_spans(
            &tabs,
            palette.active_tab,
            &app.app_theme,
            app.config.ui.tab_icons_only,
            app.config.ui.icon_mode,
        );
        let tabs_w = Paragraph::new(Line::from(tab_spans))
            .alignment(Alignment::Center)
            .style(app.app_theme.hint_line_bg_style());
        frame.render_widget(tabs_w, chunks[1]);

        let items: Vec<ListItem> = palette
            .items
            .iter()
            .map(|item| {
                let mut spans = vec![Span::styled(
                    format!("{} ", item.glyph),
                    Style::default()
                        .fg(app.app_theme.accent)
                        .add_modifier(Modifier::BOLD),
                )];
                spans.extend(crate::ui::style_palette_name(&item.name, &app.app_theme));
                ListItem::new(vec![
                    Line::from(spans),
                    Line::from(Span::styled(
                        &item.description,
                        Style::default().fg(app.app_theme.muted),
                    )),
                ])
            })
            .collect();

        let list = ratatui::widgets::List::new(items)
            .block(
                Block::default()
                    .style(app.app_theme.bg_style())
                    .borders(Borders::ALL)
                    .border_style(Style::default().fg(app.app_theme.muted))
                    .title(""),
            )
            .highlight_style(
                Style::default()
                    .fg(app.app_theme.highlight_fg)
                    .bg(app.app_theme.highlight_bg)
                    .add_modifier(Modifier::BOLD),
            )
            .highlight_symbol("  ");

        frame.render_stateful_widget(list, chunks[2], &mut palette.state);
    }

    if let Some(crate::popups::ActivePopup::NoteRename(popup)) = &mut app.popups.active {
        let hint_line = popup_hint_line(&app.app_theme, "Enter rename · Esc cancel");
        let content = draw_popup_frame(
            frame,
            area,
            "RENAME",
            PopupSize::Prompt,
            &hint_line,
            &app.app_theme,
        );

        popup.input.set_block(
            Block::default()
                .style(app.app_theme.bg_style())
                .borders(Borders::ALL)
                .border_style(Style::default().fg(app.app_theme.heading)),
        );
        frame.render_widget(&popup.input, content);
    }

    if let Some(crate::popups::ActivePopup::Goals(popup)) = &mut app.popups.active {
        let (title, sub) = match popup.mode {
            crate::popups::GoalsPopupMode::WordGoal => {
                ("DAILY WORD GOAL", "Enter word count · Esc cancel")
            }
            crate::popups::GoalsPopupMode::NoteGoal => {
                ("DAILY NOTE GOAL", "Enter note count · Esc cancel")
            }
        };
        let hint_line = popup_hint_line(&app.app_theme, sub);
        let content = draw_popup_frame(
            frame,
            area,
            title,
            PopupSize::Prompt,
            &hint_line,
            &app.app_theme,
        );

        popup.input.set_block(
            Block::default()
                .style(app.app_theme.bg_style())
                .borders(Borders::ALL)
                .border_style(Style::default().fg(app.app_theme.heading)),
        );
        frame.render_widget(&popup.input, content);
    }

    if let Some(crate::popups::ActivePopup::CreateNote(popup, format)) = &mut app.popups.active {
        let title = match format {
            crate::popups::NoteFormat::Markdown => "NEW NOTE",
            crate::popups::NoteFormat::Draw => "NEW DRAWING",
            crate::popups::NoteFormat::Canvas => "NEW CANVAS",
            crate::popups::NoteFormat::PlainText => "NEW TEXT FILE",
        };
        let hint_line = popup_hint_line(&app.app_theme, "Enter create · Esc cancel");
        let content = draw_popup_frame(
            frame,
            area,
            title,
            PopupSize::Prompt,
            &hint_line,
            &app.app_theme,
        );
        popup.input.set_block(popup_block("", &app.app_theme));
        frame.render_widget(&popup.input, content);
    }

    if let Some(crate::popups::ActivePopup::Import(popup)) = &mut app.popups.active {
        let title = match popup.source {
            crate::popups::ImportSource::File => "IMPORT FILE",
            crate::popups::ImportSource::Csv => "IMPORT CSV/TSV",
            crate::popups::ImportSource::Json => "IMPORT JSON",
            crate::popups::ImportSource::Url => "IMPORT URL",
            crate::popups::ImportSource::Clipboard => "IMPORT CLIPBOARD",
        };
        let hint_line = popup_hint_line(&app.app_theme, "Enter import · Esc cancel");
        let content = draw_popup_frame(
            frame,
            area,
            title,
            PopupSize::Large,
            &hint_line,
            &app.app_theme,
        );
        popup.input.set_block(
            Block::default()
                .style(app.app_theme.bg_style())
                .borders(Borders::ALL)
                .border_style(Style::default().fg(app.app_theme.muted)),
        );
        frame.render_widget(&popup.input, content);
    }

    if let Some(crate::popups::ActivePopup::Search(popup)) = &mut app.popups.active {
        let hint_line = popup_hint_line(
            &app.app_theme,
            "Tab switch · Enter open · Esc cancel · f:folder p:pinned t:tag g:text · \\e\\ escapes filters",
        );
        let content = draw_popup_frame(
            frame,
            area,
            "SEARCH",
            PopupSize::Large,
            &hint_line,
            &app.app_theme,
        );

        let query_text = popup.input.lines().join("");
        let parsed = crate::app::parse_search_query(&query_text);
        let has_filter = parsed.folder_filter.is_some()
            || parsed.pinned_only
            || parsed.tag_filter.is_some()
            || parsed.grep_mode;

        let constraints = if has_filter {
            vec![
                Constraint::Length(3),
                Constraint::Length(1),
                Constraint::Min(3),
            ]
        } else {
            vec![Constraint::Length(3), Constraint::Min(3)]
        };
        let chunks = Layout::default()
            .direction(Direction::Vertical)
            .constraints(constraints)
            .split(content);

        if has_filter {
            let mut spans: Vec<Span<'static>> = vec![Span::raw("  ")];
            let mut first = true;

            let add_sep =
                |spans: &mut Vec<Span<'static>>, first: &mut bool, theme: &AppThemeColors| {
                    if !*first {
                        spans.push(Span::styled(
                            " · ",
                            Style::default()
                                .fg(theme.accent)
                                .add_modifier(Modifier::BOLD),
                        ));
                    }
                    *first = false;
                };

            if let Some(f) = &parsed.folder_filter {
                let text = if f.is_empty() { "Vault" } else { f.as_str() };
                add_sep(&mut spans, &mut first, &app.app_theme);
                spans.push(Span::styled(
                    format!(
                        "{} ",
                        crate::ui::get_icon("\u{f07c}", "\u{1f4c2}", app.config.ui.icon_mode)
                    ),
                    Style::default()
                        .fg(app.app_theme.accent)
                        .add_modifier(Modifier::BOLD),
                ));
                spans.push(Span::styled(
                    text.to_string(),
                    Style::default()
                        .fg(app.app_theme.accent)
                        .add_modifier(Modifier::BOLD),
                ));
            }
            if parsed.pinned_only {
                add_sep(&mut spans, &mut first, &app.app_theme);
                spans.push(Span::styled(
                    format!(
                        "{} Pinned",
                        crate::ui::get_icon("\u{f08d}", "\u{1f4cc}", app.config.ui.icon_mode)
                    ),
                    Style::default()
                        .fg(app.app_theme.accent)
                        .add_modifier(Modifier::BOLD),
                ));
            }
            if parsed.grep_mode {
                add_sep(&mut spans, &mut first, &app.app_theme);
                let grep_display = if parsed.grep_text.is_empty() {
                    "Grep".to_string()
                } else {
                    parsed.grep_text.clone()
                };
                spans.push(Span::styled(
                    format!(
                        "{} {grep_display}",
                        crate::ui::get_icon("\u{f002}", "\u{1f50d}", app.config.ui.icon_mode)
                    ),
                    Style::default()
                        .fg(app.app_theme.accent)
                        .add_modifier(Modifier::BOLD),
                ));
            }
            if let Some(tags) = &parsed.tag_filter {
                add_sep(&mut spans, &mut first, &app.app_theme);
                let tag_text = if tags.is_empty() {
                    String::new()
                } else {
                    tags.join(", ")
                };
                spans.push(Span::styled(
                    format!(
                        "{} ",
                        crate::ui::get_icon("\u{f02b}", "\u{1f3f7}", app.config.ui.icon_mode)
                    ),
                    Style::default()
                        .fg(app.app_theme.accent)
                        .add_modifier(Modifier::BOLD),
                ));
                spans.push(Span::styled(
                    tag_text,
                    Style::default()
                        .fg(app.app_theme.accent)
                        .add_modifier(Modifier::BOLD),
                ));
            }

            let filter_line = Line::from(spans);
            let filter_para = Paragraph::new(filter_line).style(app.app_theme.bg_style());
            frame.render_widget(filter_para, chunks[1]);
        }

        let input_chunk = chunks[0];
        let results_chunk = if has_filter { chunks[2] } else { chunks[1] };

        popup.input.set_block(
            Block::default()
                .style(app.app_theme.bg_style())
                .borders(Borders::ALL)
                .border_style(if popup.focus == crate::popups::SearchFocus::Input {
                    Style::default().fg(app.app_theme.heading)
                } else {
                    Style::default().fg(app.app_theme.muted)
                })
                .title(""),
        );
        frame.render_widget(&popup.input, input_chunk);

        let has_title = !popup.title_results.is_empty();
        let has_grep = !popup.grep_results.is_empty();

        let results_focused = popup.focus == crate::popups::SearchFocus::Results;
        let results_border = if results_focused {
            Style::default().fg(app.app_theme.heading)
        } else {
            Style::default().fg(app.app_theme.muted)
        };

        let (all_items, results_title) = if has_grep {
            let mut visible: Vec<(usize, String)> = Vec::new();
            let mut i = 0;
            while i < popup.grep_results.len() {
                let is_collapsed = popup.grep_is_header[i] && !popup.grep_expanded.contains(&i);
                let icon = if popup.grep_is_header[i] {
                    if is_collapsed { "\u{25b6}" } else { "\u{25bc}" }
                } else {
                    ""
                };
                visible.push((i, format!("{}{}", icon, popup.grep_results[i])));
                i += 1;
                if is_collapsed {
                    while i < popup.grep_results.len() && !popup.grep_is_header[i] {
                        i += 1;
                    }
                }
            }
            let items: Vec<ListItem> = visible
                .iter()
                .map(|(_, t)| {
                    ListItem::new(crate::ui::styled_result_line(
                        t,
                        &app.app_theme,
                        app.config.ui.icon_mode,
                    ))
                })
                .collect();
            (items, "")
        } else if has_title {
            let items: Vec<ListItem> = popup
                .title_results
                .iter()
                .map(|entry| {
                    ListItem::new(crate::ui::styled_result_line(
                        entry,
                        &app.app_theme,
                        app.config.ui.icon_mode,
                    ))
                })
                .collect();
            (items, "")
        } else {
            let msg = if query_text.trim().is_empty() && !has_filter {
                "Type to search notes"
            } else {
                "No results"
            };
            (
                vec![ListItem::new(Span::styled(
                    msg.to_string(),
                    Style::default().fg(app.app_theme.muted),
                ))],
                "",
            )
        };

        let results_list = List::new(all_items)
            .block(
                Block::default()
                    .style(app.app_theme.bg_style())
                    .borders(Borders::ALL)
                    .border_style(results_border)
                    .title(results_title),
            )
            .highlight_style(
                Style::default()
                    .fg(app.app_theme.highlight_fg)
                    .bg(app.app_theme.highlight_bg)
                    .add_modifier(Modifier::BOLD),
            )
            .highlight_symbol("  ");
        let mut list_state = ListState::default();
        if results_focused && has_grep {
            let mut vis_pos = 0;
            let mut i = 0;
            while i < popup.grep_results.len() && i <= popup.grep_selected {
                let is_collapsed = popup.grep_is_header[i] && !popup.grep_expanded.contains(&i);
                if i == popup.grep_selected {
                    list_state.select(Some(vis_pos));
                    break;
                }
                vis_pos += 1;
                i += 1;
                if is_collapsed {
                    while i < popup.grep_results.len() && !popup.grep_is_header[i] {
                        i += 1;
                    }
                }
            }
        } else if results_focused && has_title {
            list_state.select(Some(popup.title_selected));
        }
        frame.render_stateful_widget(results_list, results_chunk, &mut list_state);
    }

    if let Some(crate::popups::ActivePopup::TrashView(trash)) = &app.popups.active {
        let hint_line = popup_hint_line(&app.app_theme, "r restore · d delete · E empty · q close");
        let content = draw_popup_frame(
            frame,
            area,
            "TRASH",
            PopupSize::Large,
            &hint_line,
            &app.app_theme,
        );

        let border_color = if trash.items.is_empty() {
            app.app_theme.muted
        } else {
            app.app_theme.heading
        };

        let items: Vec<ListItem> = trash
            .items
            .iter()
            .map(|item| {
                let name = item.name.to_string_lossy();
                let when = format_relative_time(item.time_deleted as u64);
                ListItem::new(Line::from(vec![
                    Span::raw(name.to_string()),
                    Span::styled(
                        format!("  ({when})"),
                        Style::default().fg(app.app_theme.muted),
                    ),
                ]))
            })
            .collect();

        let list = List::new(items)
            .block(
                Block::default()
                    .style(app.app_theme.bg_style())
                    .borders(Borders::ALL)
                    .border_style(Style::default().fg(border_color))
                    .title(""),
            )
            .highlight_style(
                Style::default()
                    .fg(app.app_theme.highlight_fg)
                    .bg(app.app_theme.highlight_bg)
                    .add_modifier(Modifier::BOLD),
            )
            .highlight_symbol("  ");

        let mut state = list_state_selected(Some(trash.selected));

        frame.render_stateful_widget(list, content, &mut state);
    }

    if let Some(popup) = &app.popups.confirm {
        draw_confirm_popup(frame, popup, area, &app.app_theme);
    }
}

pub(crate) fn list_view_layout(
    area: Rect,
    preview_enabled: bool,
    preview_position: crate::config::PreviewPosition,
    calendar_enabled: bool,
    preview_fullscreen: bool,
    preview_width_ratio: f32,
    calendar_height: u16,
    calendar_position: crate::config::CalendarPosition,
) -> (Rect, Option<Rect>, Option<Rect>) {
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Length(1),
            Constraint::Min(5),
            Constraint::Length(1),
        ])
        .split(area);

    if preview_fullscreen {
        return (chunks[1], Some(chunks[1]), None);
    }

    let (list_column, preview_area) = if preview_enabled {
        let ratio_num = (preview_width_ratio.clamp(0.2, 0.8) * 100.0).round() as u32;
        let (constraints, list_idx, p_idx) = match preview_position {
            crate::config::PreviewPosition::Left => (
                [
                    Constraint::Ratio(ratio_num, 100),
                    Constraint::Length(1),
                    Constraint::Min(0),
                ],
                2,
                0,
            ),
            crate::config::PreviewPosition::Right => (
                [
                    Constraint::Min(0),
                    Constraint::Length(1),
                    Constraint::Ratio(ratio_num, 100),
                ],
                0,
                2,
            ),
        };
        let full_cols = Layout::default()
            .direction(Direction::Horizontal)
            .constraints(constraints)
            .split(chunks[1]);
        (full_cols[list_idx], Some(full_cols[p_idx]))
    } else {
        (
            Rect::new(area.x, area.y + 1, area.width, chunks[1].height),
            None,
        )
    };

    let (list_area, calendar_area) = if calendar_enabled {
        let cal_h = calendar_height.max(9);
        let sp = match calendar_position {
            crate::config::CalendarPosition::Bottom => Layout::default()
                .direction(Direction::Vertical)
                .constraints([Constraint::Min(5), Constraint::Length(cal_h)])
                .split(list_column),
            crate::config::CalendarPosition::Top => Layout::default()
                .direction(Direction::Vertical)
                .constraints([Constraint::Length(cal_h), Constraint::Min(5)])
                .split(list_column),
        };
        match calendar_position {
            crate::config::CalendarPosition::Bottom => (sp[0], Some(sp[1])),
            crate::config::CalendarPosition::Top => (sp[1], Some(sp[0])),
        }
    } else {
        (list_column, None)
    };

    (list_area, preview_area, calendar_area)
}

fn get_item_name(app: &App, idx: usize) -> Option<String> {
    if let Some(item) = app.list.visual_list.get(idx) {
        match item {
            crate::list_view::VisualItem::Folder { name, .. } => Some(name.clone()),
            crate::list_view::VisualItem::SmartFolder { label, .. } => Some(label.clone()),
            crate::list_view::VisualItem::Note { summary_idx, .. } => {
                app.notes.get(*summary_idx).map(|n| n.title.clone())
            }
            crate::list_view::VisualItem::CreateNew { .. } => Some("Create...".to_string()),
        }
    } else {
        None
    }
}

pub fn get_preview_info(app: &App) -> Option<PreviewHeaderInfo> {
    if !app.preview_fullscreen {
        return None;
    }

    let current_index = if app.mode == ViewMode::Edit {
        if let Some(id) = &app.editor.editing_id {
            if let Some(note_pos) = app.notes.iter().position(|n| &n.id == id) {
                app.list.visual_list.iter().position(|item| {
                    if let crate::list_view::VisualItem::Note { summary_idx, .. } = item {
                        *summary_idx == note_pos
                    } else {
                        false
                    }
                })
            } else {
                None
            }
        } else {
            None
        }
    } else {
        Some(app.list.visual_index)
    };

    if app.mode == ViewMode::Edit {
        if let Some(id) = &app.editor.editing_id {
            if let Some(note) = app.notes.iter().find(|n| &n.id == id) {
                let folder = if note.folder.is_empty() {
                    "Vault".to_string()
                } else {
                    format!("Vault/{}", note.folder)
                };
                let title = crate::events::get_title_text(&app.editor.title_editor).into_owned();
                let title = if title.is_empty() {
                    "Untitled note".to_string()
                } else {
                    title
                };
                let (prev_name, next_name) = if let Some(idx) = current_index {
                    let prev = if idx > 0 {
                        get_item_name(app, idx - 1)
                    } else {
                        None
                    };
                    let next = if idx + 1 < app.list.visual_list.len() {
                        get_item_name(app, idx + 1)
                    } else {
                        None
                    };
                    (prev, next)
                } else {
                    (None, None)
                };
                return Some(PreviewHeaderInfo {
                    path: folder,
                    item_name: title,
                    prev_name,
                    next_name,
                });
            }
        } else if let Some(path) = &app.editor.template_edit_path {
            let parent = path
                .parent()
                .map(|p| p.to_string_lossy().into_owned())
                .unwrap_or_default();
            let filename = path
                .file_name()
                .map(|f| f.to_string_lossy().into_owned())
                .unwrap_or_default();
            let folder = if parent.is_empty() {
                "Templates".to_string()
            } else {
                format!("Templates/{}", parent)
            };
            return Some(PreviewHeaderInfo {
                path: folder,
                item_name: filename,
                prev_name: None,
                next_name: None,
            });
        }
    }

    if let Some(item) = app.list.visual_list.get(app.list.visual_index) {
        let (path, item_name) = match item {
            crate::list_view::VisualItem::Folder { path, name, .. } => {
                if path.is_empty() {
                    ("Vault".to_string(), "Vault".to_string())
                } else if path == VIRTUAL_PINNED_PATH {
                    ("Pinned".to_string(), "Pinned".to_string())
                } else if path == VIRTUAL_SMART_PATH {
                    ("Smart".to_string(), "Smart".to_string())
                } else if let Some(slash_idx) = path.rfind('/') {
                    let parent = &path[..slash_idx];
                    (format!("Vault/{}", parent), name.clone())
                } else {
                    ("Vault".to_string(), name.clone())
                }
            }
            crate::list_view::VisualItem::SmartFolder { label, .. } => {
                ("Smart".to_string(), label.clone())
            }

            crate::list_view::VisualItem::Note { summary_idx, .. } => {
                let note = app.notes.get(*summary_idx)?;
                let folder = if note.folder.is_empty() {
                    "Vault".to_string()
                } else {
                    format!("Vault/{}", note.folder)
                };
                (folder, note.title.clone())
            }
            crate::list_view::VisualItem::CreateNew { path, .. } => {
                let folder = if path.is_empty() {
                    "Vault".to_string()
                } else {
                    format!("Vault/{}", path)
                };
                (folder, "Create...".to_string())
            }
        };

        let idx = app.list.visual_index;
        let prev_name = if idx > 0 {
            get_item_name(app, idx - 1)
        } else {
            None
        };
        let next_name = if idx + 1 < app.list.visual_list.len() {
            get_item_name(app, idx + 1)
        } else {
            None
        };

        Some(PreviewHeaderInfo {
            path,
            item_name,
            prev_name,
            next_name,
        })
    } else {
        None
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::app::ViewMode;
    use crate::config::CalendarPosition;
    use crate::config::PreviewPosition;

    #[test]
    fn calendar_never_overlaps_preview_and_stays_in_list_column() {
        let area = Rect::new(0, 0, 80, 24);
        for &position in &[PreviewPosition::Left, PreviewPosition::Right] {
            let (list_area, preview_area, calendar_area) = list_view_layout(
                area,
                true, // preview enabled
                position,
                true,  // calendar enabled
                false, // preview_fullscreen
                0.43,  // preview_width_ratio (default)
                9,     // calendar_height (default)
                CalendarPosition::Bottom,
            );

            let cal = calendar_area.expect("calendar enabled");
            let preview = preview_area.expect("preview enabled");

            // Calendar is strictly underneath list_area.
            assert_eq!(cal.x, list_area.x);
            assert_eq!(cal.width, list_area.width);
            assert_eq!(cal.y, list_area.y + list_area.height);

            // Calendar and preview are disjoint (separated in x since y overlaps).
            let disjoint = cal.right() <= preview.x || preview.right() <= cal.x;
            assert!(disjoint, "calendar must not overlap preview @ {position:?}");
        }
    }

    /// With preview off the calendar spans the full content width at the bottom.
    #[test]
    fn calendar_full_width_when_no_preview() {
        let area = Rect::new(0, 0, 80, 24);
        let (list_area, preview_area, calendar_area) = list_view_layout(
            area,
            false,
            PreviewPosition::Right,
            true,
            false,
            0.43,
            9,
            CalendarPosition::Bottom,
        );

        assert!(preview_area.is_none());
        let cal = calendar_area.expect("calendar enabled");
        assert_eq!(cal.width, list_area.width);
        assert_eq!(cal.y, list_area.y + list_area.height);
        assert_eq!(cal.height, 9);
    }

    /// With the calendar disabled there is no calendar area and list is full.
    #[test]
    fn no_calendar_area_when_disabled() {
        let area = Rect::new(0, 0, 80, 24);
        let (_, preview_area, calendar_area) = list_view_layout(
            area,
            true,
            PreviewPosition::Right,
            false,
            false,
            0.43,
            9,
            CalendarPosition::Bottom,
        );
        assert!(calendar_area.is_none());
        assert!(preview_area.is_some());
    }

    #[test]
    fn test_get_preview_info() {
        let temp_dir = tempfile::tempdir().unwrap();
        let data_dir = temp_dir.path().join("data");
        let config_dir = temp_dir.path().join("config");
        let notes_dir = temp_dir.path().join("notes");
        let templates_dir = temp_dir.path().join("templates");
        std::fs::create_dir_all(&data_dir).unwrap();
        std::fs::create_dir_all(&config_dir).unwrap();
        std::fs::create_dir_all(&notes_dir).unwrap();
        std::fs::create_dir_all(&templates_dir).unwrap();

        let storage = crate::storage::Storage {
            data_dir,
            config_dir,
            notes_dir,
            templates_dir,
            key: [0u8; 32],
        };
        let mut app = App::new(storage).unwrap();

        // 1. preview_fullscreen is false
        app.preview_fullscreen = false;
        assert!(get_preview_info(&app).is_none());

        app.preview_fullscreen = true;

        // 2. ViewMode::List, empty list
        app.mode = ViewMode::List;
        app.list.visual_list.clear();
        assert!(get_preview_info(&app).is_none());

        // 3. ViewMode::List, selected folder
        app.list.visual_list = vec![crate::list_view::VisualItem::Folder {
            path: "work/projects".to_string(),
            name: "projects".to_string(),
            depth: 1,
            is_expanded: false,
            note_count: 0,
            recursive_count: 0,
            stale: false,
            is_pinned: false,
        }];
        app.list.visual_index = 0;
        assert_eq!(
            get_preview_info(&app),
            Some(PreviewHeaderInfo {
                path: "Vault/work".to_string(),
                item_name: "projects".to_string(),
                prev_name: None,
                next_name: None,
            })
        );

        // 4. ViewMode::List, selected note with prev and next
        let note = crate::storage::NoteSummary {
            id: "note1".to_string(),
            title: "My Note".to_string(),
            updated_at: 0,
            folder: "work/projects".to_string(),
            tags: vec![],
            pinned: false,
            links: vec![],
            size_bytes: 0,
        };
        app.notes = vec![note];
        app.list.visual_list = vec![
            crate::list_view::VisualItem::Folder {
                path: "work/projects".to_string(),
                name: "projects".to_string(),
                depth: 1,
                is_expanded: true,
                note_count: 1,
                recursive_count: 1,
                stale: false,
                is_pinned: false,
            },
            crate::list_view::VisualItem::Note {
                summary_idx: 0,
                depth: 2,
                is_clin: false,
                is_draw: false,
                is_canvas: false,
                in_virtual_pinned_folder: false,
            },
            crate::list_view::VisualItem::Folder {
                path: "other".to_string(),
                name: "other".to_string(),
                depth: 1,
                is_expanded: false,
                note_count: 0,
                recursive_count: 0,
                stale: false,
                is_pinned: false,
            },
        ];
        app.list.visual_index = 1;
        assert_eq!(
            get_preview_info(&app),
            Some(PreviewHeaderInfo {
                path: "Vault/work/projects".to_string(),
                item_name: "My Note".to_string(),
                prev_name: Some("projects".to_string()),
                next_name: Some("other".to_string()),
            })
        );

        // 5. ViewMode::Edit, editing a note
        app.mode = ViewMode::Edit;
        app.editor.editing_id = Some("note1".to_string());
        app.editor.title_editor = crate::events::make_title_editor(
            "Unsaved Title Change",
            app.app_theme.highlight_fg,
            app.app_theme.highlight_bg,
        );
        assert_eq!(
            get_preview_info(&app),
            Some(PreviewHeaderInfo {
                path: "Vault/work/projects".to_string(),
                item_name: "Unsaved Title Change".to_string(),
                prev_name: Some("projects".to_string()),
                next_name: Some("other".to_string()),
            })
        );
    }

    #[test]
    fn preview_width_ratio_controls_preview_width() {
        let area = Rect::new(0, 0, 80, 24);
        let (_, preview_43, _) = list_view_layout(
            area,
            true,
            PreviewPosition::Right,
            true,
            false,
            0.43,
            9,
            CalendarPosition::Bottom,
        );
        let (_, preview_70, _) = list_view_layout(
            area,
            true,
            PreviewPosition::Right,
            true,
            false,
            0.70,
            9,
            CalendarPosition::Bottom,
        );
        let p43 = preview_43.expect("preview enabled");
        let p70 = preview_70.expect("preview enabled");
        assert!(
            p70.width > p43.width,
            "0.70 ratio should give wider preview than 0.43"
        );
    }

    #[test]
    fn calendar_height_controls_calendar_height() {
        let area = Rect::new(0, 0, 80, 24);
        let (_, _, cal_9) = list_view_layout(
            area,
            true,
            PreviewPosition::Right,
            true,
            false,
            0.43,
            9,
            CalendarPosition::Bottom,
        );
        let (_, _, cal_14) = list_view_layout(
            area,
            true,
            PreviewPosition::Right,
            true,
            false,
            0.43,
            14,
            CalendarPosition::Bottom,
        );
        let c9 = cal_9.expect("calendar enabled");
        let c14 = cal_14.expect("calendar enabled");
        assert!(
            c14.height > c9.height,
            "height 14 should give taller calendar than 9"
        );
        assert_eq!(c14.height, 14);
    }

    #[test]
    fn calendar_height_clamped_to_at_least_9() {
        let area = Rect::new(0, 0, 80, 24);
        let (_, _, cal_3) = list_view_layout(
            area,
            true,
            PreviewPosition::Right,
            true,
            false,
            0.43,
            3,
            CalendarPosition::Bottom,
        );
        let c3 = cal_3.expect("calendar enabled");
        assert_eq!(
            c3.height, 9,
            "calendar height should be clamped to at least 9 to be visible"
        );
    }

    #[test]
    fn section_rects_zero_active() {
        let r = Rect::new(0, 0, 100, 10);
        let rects = section_rects(r, &[]);
        assert!(rects.is_empty());
    }

    #[test]
    fn section_rects_one_active() {
        let r = Rect::new(0, 0, 100, 10);
        let active = [crate::config::NotesSection::Calendar];
        let rects = section_rects(r, &active);
        assert_eq!(rects.len(), 1);
        // Single section centered at 50% width: 100/2=50 wide, x=(100-50)/2=25
        assert_eq!(rects[0], Rect::new(25, 0, 50, 10));
    }

    #[test]
    fn section_rects_two_active_equal_halves() {
        let r = Rect::new(0, 0, 100, 10);
        let active = [
            crate::config::NotesSection::Calendar,
            crate::config::NotesSection::Goals,
        ];
        let rects = section_rects(r, &active);
        assert_eq!(rects.len(), 2);
        // widths differ by at most 1
        let diff = (rects[0].width as i16 - rects[1].width as i16).unsigned_abs();
        assert!(diff <= 1, "halves must be equal width within 1, got {diff}");
        assert_eq!(rects[0].x, r.x);
        assert_eq!(rects[0].y, r.y);
        assert_eq!(rects[0].height, r.height);
        assert_eq!(rects[1].y, r.y);
        assert_eq!(rects[1].height, r.height);
        assert_eq!(rects[1].right(), r.right());
    }
}
