mod layout;
mod popups;
mod table;
pub mod util;

use layout::{DynamicLayout, Section};

use crate::tui::app::{App, AppMode, Focus};
use crate::tui::theme::Theme;
use popups::{
    render_cell_edit_overlay, render_command_palette, render_file_create_confirm,
    render_file_picker, render_help_popup, render_link_picker, render_save_before_nav_confirm,
    render_save_before_quit_confirm, render_save_width_confirm, render_theme_picker,
};
use ratatui::Frame;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span, Text};
use ratatui::widgets::{
    Block, Borders, Clear, List, ListItem, Paragraph, Scrollbar, ScrollbarOrientation,
    ScrollbarState, Wrap,
};
use table::render_table;
use util::{detect_checkbox_in_text, filter_content};

pub fn render(frame: &mut Frame, app: &mut App) {
    // Re-index interactive elements if mermaid image dimensions arrived last frame.
    #[cfg(all(feature = "mermaid", unix))]
    app.reindex_mermaid_if_needed();

    // Update content metrics before rendering to ensure content height and scroll are correct
    app.update_content_metrics();

    // Clear expired status messages (auto-dismiss after timeout)
    app.clear_expired_status_message();

    let area = frame.area();

    // Create dynamic main layout
    // Show search bar if: outline search is active OR in document search mode (typing or viewing results)
    let show_search_bar = app.show_search || app.mode == AppMode::DocSearch;
    let main_layout = DynamicLayout::vertical(area)
        .section(Section::Title, Constraint::Length(2))
        .section_if(show_search_bar, Section::Search, Constraint::Length(3))
        .section(Section::Content, Constraint::Min(0))
        .section(Section::Status, Constraint::Length(1))
        .section(Section::Footer, Constraint::Length(1))
        .build();

    // Render title bar
    render_title_bar(frame, app, main_layout.require(Section::Title));

    // Render search bar if visible
    if let Some(search_area) = main_layout.get(Section::Search) {
        render_search_bar(frame, app, search_area);
    }

    // Create horizontal layout for outline and content (conditional based on outline visibility)
    let content_area = main_layout.require(Section::Content);

    // Update viewport height for scroll calculations (subtract 2 for block borders)
    app.set_viewport_height(content_area.height.saturating_sub(2));

    // Minimum widths: outline needs at least 20 cols to be usable, content needs at least 40
    const MIN_OUTLINE_WIDTH: u16 = 20;
    const MIN_CONTENT_WIDTH: u16 = 40;
    const MIN_TOTAL_WIDTH: u16 = MIN_OUTLINE_WIDTH + MIN_CONTENT_WIDTH;

    // Decide whether to show outline based on terminal width
    let effective_show_outline = app.show_outline && content_area.width >= MIN_TOTAL_WIDTH;

    let content_chunks = if effective_show_outline {
        let content_width = 100 - app.outline_width;
        Layout::horizontal([
            Constraint::Percentage(app.outline_width),
            Constraint::Percentage(content_width),
        ])
        .split(content_area)
    } else {
        // Full-width content when outline is hidden
        Layout::horizontal([Constraint::Percentage(100)]).split(content_area)
    };

    // Render outline (left pane) only if effectively visible (user toggle AND enough width)
    if effective_show_outline {
        render_outline(frame, app, content_chunks[0]);
        // Render content (right pane)
        render_content(frame, app, content_chunks[1]);
    } else {
        // Full-width content
        render_content(frame, app, content_chunks[0]);
    }

    // Render status bar at bottom
    render_status_bar(frame, app, main_layout.require(Section::Status));

    // Render keybinding hints footer
    render_footer(frame, app, main_layout.require(Section::Footer));

    // Render help popup if shown
    if app.show_help {
        render_help_popup(frame, app, area);
    }

    // Render theme picker if shown
    if app.show_theme_picker {
        render_theme_picker(frame, app, area);
    }

    // Render cell edit overlay if in cell edit mode
    if matches!(app.mode, crate::tui::app::AppMode::CellEdit) {
        render_cell_edit_overlay(frame, app, area);
    }

    // Render image modal if viewing an image
    render_image_modal(frame, app, area);

    // Render link picker if in link follow mode with links
    if matches!(app.mode, crate::tui::app::AppMode::LinkFollow) && !app.links_in_view.is_empty() {
        render_link_picker(frame, app, area);
    }

    // Render file picker modal (FileSearch is only used as a fallback for old code)
    if matches!(app.mode, AppMode::FilePicker | AppMode::FileSearch) {
        render_file_picker(frame, app, area);
    }

    // Render file creation confirmation dialog
    if matches!(app.mode, AppMode::ConfirmFileCreate)
        && let Some(message) = &app.pending_file_create_message
    {
        render_file_create_confirm(frame, message, &app.theme);
    }

    // Render save width confirmation dialog
    if matches!(app.mode, AppMode::ConfirmSaveWidth) {
        render_save_width_confirm(frame, app.outline_width, &app.theme);
    }

    // Render save before quit confirmation dialog
    if matches!(app.mode, AppMode::ConfirmSaveBeforeQuit) {
        render_save_before_quit_confirm(frame, app.pending_edits.len(), &app.theme);
    }

    // Render save before navigate confirmation dialog
    if matches!(app.mode, AppMode::ConfirmSaveBeforeNav) {
        render_save_before_nav_confirm(frame, app.pending_edits.len(), &app.theme);
    }

    // Render command palette
    if matches!(app.mode, AppMode::CommandPalette) {
        render_command_palette(frame, app, &app.theme);
    }
}

fn render_title_bar(frame: &mut Frame, app: &App, area: Rect) {
    let heading_count = app.document.headings.len();
    let title_text = format!("treemd - {} - {} headings", app.filename, heading_count);

    let title = Paragraph::new(title_text)
        .style(
            Style::default()
                .fg(app.theme.title_bar_fg)
                .add_modifier(Modifier::BOLD),
        )
        .block(Block::default().borders(Borders::BOTTOM));
    frame.render_widget(title, area);
}

fn render_search_bar(frame: &mut Frame, app: &App, area: Rect) {
    // Unified search bar rendering for both outline and document search
    let is_doc_search = app.mode == AppMode::DocSearch;

    // Get the current query and display state
    let (query, is_active, match_info) = if is_doc_search {
        let info = if !app.doc_search.matches.is_empty() {
            let current = app.doc_search.current_idx.unwrap_or(0) + 1;
            let total = app.doc_search.matches.len();
            format!(" [{}/{}]", current, total)
        } else if !app.doc_search.query.is_empty() {
            " [no matches]".to_string()
        } else {
            String::new()
        };
        (&app.doc_search.query, app.doc_search.active, info)
    } else {
        // Outline search — show how many headings the filter currently matches
        let total = app.document.headings.len();
        let shown = app
            .outline_items
            .iter()
            .filter(|item| item.heading_index.is_some())
            .count();
        let info = if app.search_query.is_empty() {
            String::new()
        } else if shown == 0 {
            " [no matches]".to_string()
        } else {
            format!(" [{}/{} headings]", shown, total)
        };
        (&app.search_query, app.outline_search_active, info)
    };

    let theme = &app.theme;

    // Styling based on search type (accents derive from the theme so the
    // bar respects custom themes and the 256-color compat layer)
    let (label, title, accent_color) = if is_doc_search {
        (
            "Find",
            " Content Search (Tab: switch to Outline) ",
            theme.link_fg,
        )
    } else {
        (
            "Filter",
            " Outline Search (Tab: switch to Content) ",
            theme.search_match_bg,
        )
    };

    // Build search bar with query
    let query_display = if is_active {
        format!("{}_", query)
    } else {
        query.clone()
    };

    let mut line_spans = vec![
        Span::raw(format!("{}: ", label)),
        Span::styled(
            query_display,
            Style::default()
                .fg(accent_color)
                .add_modifier(Modifier::BOLD),
        ),
        Span::raw(match_info),
    ];

    // Add hint text
    let hint = if is_active {
        "  (Esc: cancel • Ctrl+U: clear • Ctrl+W: del word)"
    } else if is_doc_search {
        "  (Esc: clear • Tab: switch • /: edit)"
    } else {
        "  (Esc: clear • Tab: switch • s: edit)"
    };
    line_spans.push(Span::styled(
        hint.to_string(),
        Style::default().fg(theme.help_desc_fg),
    ));

    let paragraph = Paragraph::new(Line::from(line_spans))
        .block(
            Block::default()
                .borders(Borders::ALL)
                .border_style(Style::default().fg(accent_color))
                .title(title)
                .style(Style::default().bg(theme.modal_bg())),
        )
        .style(Style::default().fg(theme.foreground));

    frame.render_widget(paragraph, area);
}

fn render_outline(frame: &mut Frame, app: &mut App, area: Rect) {
    use crate::tui::app::DOCUMENT_OVERVIEW;
    use util::build_highlighted_line;

    let theme = &app.theme;
    let search_query = if app.show_search && !app.search_query.is_empty() {
        Some(app.search_query.as_str())
    } else {
        None
    };

    let items: Vec<ListItem> = app
        .outline_items
        .iter()
        .map(|item| {
            let indent = "  ".repeat(item.level.saturating_sub(1));

            // Show expand/collapse indicator if heading has children
            let expand_indicator = if item.has_children {
                if item.expanded { "▼ " } else { "▶ " }
            } else {
                "  "
            };

            // Show bookmark indicator if this item's text matches the bookmark
            let bookmark_indicator = if app.bookmark_position.as_deref() == Some(&item.text) {
                "⚑ "
            } else {
                ""
            };

            // Color headings by level using theme
            let color = theme.heading_color(item.level);
            let base_style = Style::default().fg(color);

            // Build prefix (indent + indicators + optional #'s)
            let prefix_text = if item.text == DOCUMENT_OVERVIEW {
                format!("{}{}{}📄 ", indent, expand_indicator, bookmark_indicator)
            } else if app.show_heading_markers {
                let hashes = "#".repeat(item.level);
                format!(
                    "{}{}{}{} ",
                    indent, expand_indicator, bookmark_indicator, hashes
                )
            } else {
                format!("{}{}{}", indent, expand_indicator, bookmark_indicator)
            };

            // Build line with search highlighting using shared utility
            let line = build_highlighted_line(
                vec![Span::styled(prefix_text, base_style)],
                &item.text,
                search_query,
                base_style,
                theme.search_match_style(),
            );

            ListItem::new(line)
        })
        .collect();

    let block_style = theme.border_style(app.focus == Focus::Outline);

    let list = List::new(items)
        .block(
            Block::default()
                .borders(Borders::ALL)
                .border_style(block_style)
                .title(" Outline "),
        )
        .style(theme.content_style())
        .highlight_style(theme.selection_style())
        .highlight_symbol("► ");

    frame.render_stateful_widget(list, area, &mut app.outline_state);

    // Render scrollbar
    let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
        .begin_symbol(Some("↑"))
        .end_symbol(Some("↓"))
        .thumb_symbol("┃")
        .track_symbol(Some("│"))
        .style(Style::default().fg(theme.scrollbar_fg));

    frame.render_stateful_widget(
        scrollbar,
        area.inner(ratatui::layout::Margin {
            vertical: 1,
            horizontal: 0,
        }),
        &mut app.outline_scroll_state,
    );
}

fn render_content(frame: &mut Frame, app: &mut App, area: Rect) {
    use crate::tui::app::AppMode;

    // Clone theme early to avoid borrow conflicts
    let theme = app.theme.clone();
    let block_style = theme.border_style(app.focus == Focus::Content);

    // Get content for selected section and determine title
    let (content_text, title) = if let Some(heading_text) = app.selected_heading_text() {
        let content = app
            .selected_heading_index()
            .and_then(|idx| app.document.extract_section_at_index(idx))
            .unwrap_or_else(|| app.document.content.clone());

        // Build title with various indicators
        let raw_indicator = if app.show_raw_source { "[RAW] " } else { "" };
        let title = if app.mode == AppMode::LinkFollow && !app.links_in_view.is_empty() {
            format!(
                " {}{} [Links: {}] ",
                raw_indicator,
                heading_text,
                app.links_in_view.len()
            )
        } else {
            format!(" {}{} ", raw_indicator, heading_text)
        };

        (content, title)
    } else {
        let raw_indicator = if app.show_raw_source { "[RAW] " } else { "" };
        let title = if app.mode == AppMode::LinkFollow && !app.links_in_view.is_empty() {
            format!(
                " {}Content [Links: {}] ",
                raw_indicator,
                app.links_in_view.len()
            )
        } else {
            format!(" {}Content ", raw_indicator)
        };
        (app.document.content.clone(), title)
    };

    // Apply content filtering (frontmatter, LaTeX) based on config
    // Only filter when not showing raw source - raw view shows everything
    let content_text = if !app.show_raw_source {
        filter_content(
            &content_text,
            app.should_hide_frontmatter(),
            app.should_hide_latex(),
            app.should_latex_aggressive(),
        )
    } else {
        content_text
    };

    // Check if we should render raw source or enhanced markdown
    let mut rendered_text = if app.show_raw_source {
        // Raw source view - show unprocessed markdown
        render_raw_markdown(&content_text, &theme)
    } else {
        // Enhanced markdown rendering with syntax highlighting
        // Pre-extract what we need before passing app as mutable to avoid borrow conflicts
        let selected_element_id = if app.mode == AppMode::Interactive {
            app.interactive_state.current_element().map(|elem| elem.id)
        } else {
            None
        };
        // Clone interactive state to avoid keeping a borrow when passing app as mutable
        let interactive_state = app.interactive_state.clone();

        // Calculate available width for tables (content area minus borders and padding)
        let content_width = area.width.saturating_sub(2); // 2 for left/right borders

        #[cfg(all(feature = "mermaid", unix))]
        let mermaid_rows_ref = &app.mermaid_placeholder_rows;
        #[cfg(not(all(feature = "mermaid", unix)))]
        let mermaid_rows_ref = &std::collections::HashMap::new();

        render_markdown_enhanced(
            &content_text,
            &app.highlighter,
            &theme,
            selected_element_id,
            Some(&interactive_state), // Pass cloned copy to release borrow
            Some(content_width),
            mermaid_rows_ref,
        )
    };

    // Apply search highlighting only for document/content search mode
    // Outline search (s) only filters headings, it doesn't highlight content
    if app.mode == AppMode::DocSearch && !app.doc_search.query.is_empty() {
        rendered_text = apply_search_highlighting(
            rendered_text,
            &app.doc_search.query,
            app.doc_search.current_idx,
            app.doc_search.matches.len(),
            &theme,
        );
    }

    // Build paragraph with wrapping to get accurate visual line count
    let block = Block::default()
        .borders(Borders::ALL)
        .border_style(block_style)
        .title(title);
    let paragraph = Paragraph::new(rendered_text)
        .block(block)
        .style(theme.content_style())
        .wrap(Wrap { trim: false });

    // Use line_count() for accurate visual line count after wrapping
    // (requires ratatui "unstable-rendered-line-info" feature)
    let inner_width = area.width.saturating_sub(2); // subtract block borders
    let visual_line_count = paragraph.line_count(inner_width);
    if app.content_height != visual_line_count {
        app.content_height = visual_line_count;
    }

    // Clamp scroll so we stop when last line reaches viewport bottom
    let max_scroll = app.max_content_scroll();
    if app.content_scroll > max_scroll {
        app.content_scroll = max_scroll;
    }
    app.content_scroll_state =
        ScrollbarState::new(max_scroll as usize).position(app.content_scroll as usize);

    // Apply scroll and render
    let paragraph = paragraph.scroll((app.content_scroll, 0));
    frame.render_widget(paragraph, area);

    // Render inline images (first image in content)
    render_inline_images(frame, app, area);

    // Render mermaid diagrams as image overlays
    #[cfg(all(feature = "mermaid", unix))]
    render_mermaid_images(frame, app, area);

    // Render scrollbar
    let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
        .begin_symbol(Some("↑"))
        .end_symbol(Some("↓"))
        .thumb_symbol("┃")
        .track_symbol(Some("│"))
        .style(Style::default().fg(theme.scrollbar_fg));

    frame.render_stateful_widget(
        scrollbar,
        area.inner(ratatui::layout::Margin {
            vertical: 1,
            horizontal: 0,
        }),
        &mut app.content_scroll_state.clone(),
    );
}

fn render_inline_images(frame: &mut Frame, app: &mut App, area: Rect) {
    use crate::tui::interactive::ElementType;
    use ratatui_image::{FilterType, Resize, StatefulImage};

    // Don't render inline when viewing modal
    if app.image_modal.path.is_some() {
        return;
    }

    let theme = &app.theme;

    // Account for borders and padding
    let inner = area.inner(ratatui::layout::Margin {
        vertical: 1,
        horizontal: 1,
    });

    // Maximum image width: 80% of content area
    let max_image_width = ((inner.width as usize * 80) / 100).max(20) as u16;
    // Use the full placeholder height reserved for images
    let max_image_height = crate::tui::interactive::IMAGE_PLACEHOLDER_LINES as u16;

    // Get currently selected image if in interactive mode
    let selected_image_id = if app.mode == crate::tui::app::AppMode::Interactive {
        app.interactive_state.current_element().and_then(|elem| {
            if matches!(elem.element_type, ElementType::Image { .. }) {
                Some(elem.id)
            } else {
                None
            }
        })
    } else {
        None
    };

    // Render all images that are visible in the current scroll viewport.
    // Only render images that have placeholder space reserved (line_range > 1 line).
    // Nested images in details/lists have 1-line ranges with no placeholder space.
    for elem in &app.interactive_state.elements {
        if let ElementType::Image { src, .. } = &elem.element_type {
            let (line_start, line_end) = elem.line_range;

            // Skip images without placeholder space (nested in details/lists)
            if line_end.saturating_sub(line_start) < 3 {
                continue;
            }

            // Check if this image is visible in current scroll window
            let scroll = app.content_scroll as usize;
            let viewport_height = app.content_viewport_height as usize;
            let viewport_end = scroll + viewport_height;

            // Skip if image is outside visible area
            if line_end < scroll || line_start >= viewport_end {
                continue;
            }

            // Calculate Y position: convert line number to screen coordinate
            // Line positions are relative to the full document, need to account for scroll
            let y_offset = if line_start >= scroll {
                (line_start - scroll) as u16
            } else {
                0
            };

            let image_y = inner.y + y_offset;

            // Only render if there's space on screen
            if image_y >= inner.bottom() {
                continue;
            }

            let available_height = inner.bottom().saturating_sub(image_y);
            if available_height < 3 {
                continue; // Not enough space for image
            }

            let image_height = available_height.min(max_image_height);

            // Resolve image path and use cached protocol
            if let Ok(image_path) = app.resolve_image_path(src)
                && let Some(protocol_state) = app.image_protocol_cache.get_mut(&image_path)
            {
                let resize = Resize::Scale(Some(FilterType::Triangle));

                // Check if this image is selected
                let is_selected = selected_image_id == Some(elem.id);

                // Calculate image area - add border space when selected
                let image_area = Rect {
                    x: inner.x,
                    y: image_y,
                    width: max_image_width.min(inner.width),
                    height: image_height,
                };

                // If selected, render a selection border around the image
                let render_area = if is_selected {
                    let border_style = Style::default()
                        .fg(theme.selection_indicator_fg)
                        .bg(theme.selection_indicator_bg)
                        .add_modifier(Modifier::BOLD);

                    let border = Block::default()
                        .borders(Borders::ALL)
                        .border_style(border_style)
                        .title(" ▶ Selected ")
                        .title_alignment(ratatui::layout::Alignment::Left);

                    // Render border first
                    frame.render_widget(border.clone(), image_area);

                    // Return inner area for image (inside border)
                    border.inner(image_area)
                } else {
                    image_area
                };

                let img_widget = StatefulImage::new().resize(resize);
                frame.render_stateful_widget(img_widget, render_area, protocol_state);
            }
        }
    }
}

#[cfg(all(feature = "mermaid", unix))]
fn render_mermaid_images(frame: &mut Frame, app: &mut App, area: Rect) {
    use crate::tui::app::App as MermaidApp;
    use crate::tui::interactive::{ElementType, mermaid_placeholder_lines};
    use ratatui_image::{Resize, StatefulImage};

    if app.image_modal.path.is_some() || !app.images_enabled {
        return;
    }

    let theme = app.theme.clone();
    let inner = area.inner(ratatui::layout::Margin {
        vertical: 1,
        horizontal: 1,
    });

    // Use full content width for mermaid diagrams (block-level elements)
    let image_width = inner.width.max(20);

    // Font cell size in pixels, used for centering calculations
    let font_size = app.picker.as_ref().map_or((8u32, 16u32), |p| {
        let font_size = p.font_size();
        (font_size.width as u32, font_size.height as u32)
    });

    let selected_code_id = if app.mode == crate::tui::app::AppMode::Interactive {
        app.interactive_state.current_element().and_then(|elem| {
            if let ElementType::CodeBlock { language, .. } = &elem.element_type
                && language.as_deref() == Some("mermaid")
            {
                return Some(elem.id);
            }
            None
        })
    } else {
        None
    };

    // Collect mermaid elements info before mutable borrow
    let mermaid_elements: Vec<_> = app
        .interactive_state
        .elements
        .iter()
        .filter_map(|elem| {
            if let ElementType::CodeBlock {
                language, content, ..
            } = &elem.element_type
                && language.as_deref() == Some("mermaid")
            {
                return Some((elem.id, elem.line_range, content.clone()));
            }
            None
        })
        .collect();

    for (elem_id, line_range, source) in &mermaid_elements {
        let (line_start, line_end) = *line_range;

        if line_end.saturating_sub(line_start) < 3 {
            continue;
        }

        let scroll = app.content_scroll as usize;
        let viewport_height = app.content_viewport_height as usize;
        let viewport_end = scroll + viewport_height;

        if line_end < scroll || line_start >= viewport_end {
            continue;
        }

        // Trigger rendering (caches on first call)
        let rendered = app.render_mermaid_if_needed(source, image_width);

        // Skip when diagram top has scrolled above the viewport — let it scroll off
        // naturally instead of pinning at y=0 and overlapping other content.
        if line_start < scroll {
            continue;
        }

        let y_offset = (line_start - scroll) as u16;
        let image_y = inner.y + y_offset;
        if image_y >= inner.bottom() {
            continue;
        }

        let available_height = inner.bottom().saturating_sub(image_y);
        if available_height < 3 {
            continue;
        }

        // Dynamic max height: use pixel-accurate rows once the image has been rendered,
        // fall back to the source-line heuristic on first load.
        // `stable_height` is constant during scrolling (used for centering so the
        // x-position doesn't jitter on every frame).  `image_height` clips to what
        // actually fits in the viewport right now.
        let max_image_height = {
            let hash = MermaidApp::mermaid_source_hash(source);
            app.mermaid_placeholder_rows
                .get(&hash)
                .copied()
                .unwrap_or_else(|| mermaid_placeholder_lines(source))
        } as u16;
        let stable_height = max_image_height.min(inner.height);
        let image_height = available_height.min(max_image_height);
        let hash = MermaidApp::mermaid_source_hash(source);

        if rendered {
            if let Some(protocol_state) = app.mermaid_protocol_cache.get_mut(&hash) {
                let is_selected = selected_code_id == Some(*elem_id);

                // Compute the natural displayed size in terminal cells to enable centering.
                // Resize::Scale uses fit_area_proportionally: ratio = min(avail_w/img_w, avail_h/img_h).
                let (centered_x, centered_width) =
                    if let Some(&(img_w, img_h)) = app.mermaid_image_dims.get(&hash) {
                        if img_w > 0 && img_h > 0 {
                            let avail_w_px = inner.width as u32 * font_size.0;
                            // Use stable_height (constant during scroll) so centering x
                            // doesn't jitter on every frame as the image enters the viewport.
                            let avail_h_px = stable_height as u32 * font_size.1;
                            let w_ratio = avail_w_px as f64 / img_w as f64;
                            let h_ratio = avail_h_px as f64 / img_h as f64;
                            let ratio = w_ratio.min(h_ratio);
                            let disp_w_px = (img_w as f64 * ratio).round() as u32;
                            // Round up to cell boundary
                            let disp_w_cols = disp_w_px.div_ceil(font_size.0) as u16;
                            let disp_w_cols = disp_w_cols.min(inner.width);
                            let x_offset = (inner.width.saturating_sub(disp_w_cols)) / 2;
                            (inner.x + x_offset, disp_w_cols)
                        } else {
                            (inner.x, image_width.min(inner.width))
                        }
                    } else {
                        (inner.x, image_width.min(inner.width))
                    };

                let image_area = Rect {
                    x: centered_x,
                    y: image_y,
                    width: centered_width,
                    height: image_height,
                };

                let render_area = if is_selected {
                    let border_style = Style::default()
                        .fg(theme.selection_indicator_fg)
                        .bg(theme.selection_indicator_bg)
                        .add_modifier(Modifier::BOLD);

                    let border = Block::default()
                        .borders(Borders::ALL)
                        .border_style(border_style)
                        .title(" ▶ Mermaid ")
                        .title_alignment(ratatui::layout::Alignment::Left);

                    frame.render_widget(border.clone(), image_area);
                    border.inner(image_area)
                } else {
                    image_area
                };

                let img_widget = StatefulImage::new().resize(Resize::Scale(None));
                frame.render_stateful_widget(img_widget, render_area, protocol_state);
            }
        } else if let Some(error) = app.mermaid_render_errors.get(&hash) {
            let prefix = "⚠ mermaid render failed: ";
            let max_msg_len = (inner.width as usize).saturating_sub(prefix.len());
            let truncated = util::truncate_with_ellipsis(error, max_msg_len);
            let error_line = Line::from(vec![
                Span::styled("⚠ ", Style::default().fg(Color::Yellow)),
                Span::styled(
                    format!("mermaid render failed: {}", truncated),
                    Style::default().fg(Color::DarkGray),
                ),
            ]);
            let error_area = Rect {
                x: inner.x,
                y: image_y,
                width: inner.width,
                height: 1,
            };
            frame.render_widget(Paragraph::new(error_line), error_area);
        }
    }
}

fn render_image_modal(frame: &mut Frame, app: &mut App, area: Rect) {
    use ratatui_image::{FilterType, Resize, StatefulImage};
    use std::time::Duration;

    // Must have frames available
    if !app.is_image_modal_open() || app.image_modal.gif_frames.is_empty() {
        return;
    }

    // Clone theme colors we need before any mutable borrows
    let theme_background = app.theme.background;
    let theme_foreground = app.theme.foreground;
    let theme_heading_1 = app.theme.heading_1;

    let is_multi_frame = app.image_modal.gif_frames.len() > 1;

    // Calculate modal area - centered on screen with padding
    // (u32 math: u16 multiplication can overflow on very wide terminals)
    let modal_width = (u32::from(area.width) * 80 / 100) as u16;
    let modal_height = (u32::from(area.height) * 80 / 100) as u16;
    let modal_x = area.x + (area.width.saturating_sub(modal_width)) / 2;
    let modal_y = area.y + (area.height.saturating_sub(modal_height)) / 2;

    let modal_area = Rect {
        x: modal_x,
        y: modal_y,
        width: modal_width,
        height: modal_height,
    };

    // Get inner area (inside modal border)
    let inner_area = Rect {
        x: modal_area.x + 1,
        y: modal_area.y + 1,
        width: modal_area.width.saturating_sub(2),
        height: modal_area.height.saturating_sub(2),
    };

    // Try to start Kitty native animation for multi-frame GIFs.
    // Kitty handles frame timing internally - no flicker!
    // Only start when animation is playing (not paused) - this allows:
    // 1. Manual frame stepping to work via software rendering
    // 2. Kitty animation to restart when user resumes playback
    if is_multi_frame
        && !app.has_kitty_animation()
        && app.use_kitty_animation
        && !app.image_modal.animation_paused
    {
        // Start animation at center of inner area
        let image_col = inner_area.x + inner_area.width / 4;
        let image_row = inner_area.y + inner_area.height / 4;
        app.start_kitty_animation(image_col, image_row);
    }

    // Check if Kitty is handling animation
    let kitty_animating = app.has_kitty_animation();

    // For software animation (non-Kitty terminals), handle frame timing
    let is_animating = is_multi_frame && !app.image_modal.animation_paused && !kitty_animating;

    // When animating (software or Kitty), avoid overwriting the image area
    let avoid_image_area = is_animating || kitty_animating;

    if is_animating && let Some(last_update) = app.image_modal.last_frame_update {
        let current_frame = &app.image_modal.gif_frames[app.image_modal.frame_index];
        let frame_delay = Duration::from_millis(current_frame.delay_ms as u64);

        if last_update.elapsed() >= frame_delay {
            app.image_modal.frame_index =
                (app.image_modal.frame_index + 1) % app.image_modal.gif_frames.len();
            app.image_modal.last_frame_update = Some(std::time::Instant::now());
        }
    }

    // Only create a new protocol when frame actually changes (software animation only).
    // Skip this entirely when Kitty handles animation.
    if !kitty_animating {
        let needs_new_protocol =
            app.image_modal.last_rendered_frame != Some(app.image_modal.frame_index);
        if needs_new_protocol && let Some(picker) = &mut app.picker {
            let current_img = app.image_modal.gif_frames[app.image_modal.frame_index]
                .image
                .clone();
            app.image_modal.state = Some(picker.new_resize_protocol(current_img));
            app.image_modal.last_rendered_frame = Some(app.image_modal.frame_index);
        }
    }

    // Get the active protocol for sizing (even Kitty needs this for layout)
    if let Some(protocol_state) = &mut app.image_modal.state {
        // Calculate image area
        let resize = Resize::Scale(Some(FilterType::Triangle));

        let image_size = protocol_state.size_for(resize.clone(), inner_area.as_size());
        let image_area = Rect {
            x: inner_area.x + (inner_area.width.saturating_sub(image_size.width)) / 2,
            y: inner_area.y + (inner_area.height.saturating_sub(image_size.height)) / 2,
            width: image_size.width,
            height: image_size.height,
        };

        // Clear and render background to hide underlying UI (sidebars, etc.)
        // During animation, we avoid overwriting the image area to prevent flicker.
        let bg_style = Style::default()
            .bg(theme_background)
            .fg(theme_foreground)
            .add_modifier(Modifier::DIM);

        // For non-animating state, just clear and fill the entire screen
        if !avoid_image_area {
            // Clear the entire screen first to hide sidebars
            frame.render_widget(Clear, area);
            frame.render_widget(Block::default().style(bg_style), area);
        } else {
            // During animation, clear all regions EXCEPT the image area to avoid flicker
            // This includes: outer background + modal interior padding around image

            // 1. Outer background - 4 strips around the modal
            // Top strip (above modal) - full width
            if modal_area.y > area.y {
                let top_bg = Rect {
                    x: area.x,
                    y: area.y,
                    width: area.width,
                    height: modal_area.y - area.y,
                };
                frame.render_widget(Clear, top_bg);
                frame.render_widget(Block::default().style(bg_style), top_bg);
            }
            // Bottom strip (below modal) - full width
            let modal_bottom = modal_area.y + modal_area.height;
            if modal_bottom < area.y + area.height {
                let bottom_bg = Rect {
                    x: area.x,
                    y: modal_bottom,
                    width: area.width,
                    height: (area.y + area.height) - modal_bottom,
                };
                frame.render_widget(Clear, bottom_bg);
                frame.render_widget(Block::default().style(bg_style), bottom_bg);
            }
            // Left strip (left of modal, modal height only)
            if modal_area.x > area.x {
                let left_bg = Rect {
                    x: area.x,
                    y: modal_area.y,
                    width: modal_area.x - area.x,
                    height: modal_area.height,
                };
                frame.render_widget(Clear, left_bg);
                frame.render_widget(Block::default().style(bg_style), left_bg);
            }
            // Right strip (right of modal, modal height only)
            let modal_right = modal_area.x + modal_area.width;
            if modal_right < area.x + area.width {
                let right_bg = Rect {
                    x: modal_right,
                    y: modal_area.y,
                    width: (area.x + area.width) - modal_right,
                    height: modal_area.height,
                };
                frame.render_widget(Clear, right_bg);
                frame.render_widget(Block::default().style(bg_style), right_bg);
            }

            // 2. Modal interior padding - 4 strips between modal border and image
            let modal_bg = Style::default().bg(theme_background).fg(theme_foreground);

            // Top padding (between modal top border and image top)
            if image_area.y > inner_area.y {
                let top_pad = Rect {
                    x: inner_area.x,
                    y: inner_area.y,
                    width: inner_area.width,
                    height: image_area.y - inner_area.y,
                };
                frame.render_widget(Clear, top_pad);
                frame.render_widget(Block::default().style(modal_bg), top_pad);
            }
            // Bottom padding (between image bottom and modal bottom border)
            let image_bottom = image_area.y + image_area.height;
            let inner_bottom = inner_area.y + inner_area.height;
            if image_bottom < inner_bottom {
                let bottom_pad = Rect {
                    x: inner_area.x,
                    y: image_bottom,
                    width: inner_area.width,
                    height: inner_bottom - image_bottom,
                };
                frame.render_widget(Clear, bottom_pad);
                frame.render_widget(Block::default().style(modal_bg), bottom_pad);
            }
            // Left padding (between modal left border and image left, image height only)
            if image_area.x > inner_area.x {
                let left_pad = Rect {
                    x: inner_area.x,
                    y: image_area.y,
                    width: image_area.x - inner_area.x,
                    height: image_area.height,
                };
                frame.render_widget(Clear, left_pad);
                frame.render_widget(Block::default().style(modal_bg), left_pad);
            }
            // Right padding (between image right and modal right border, image height only)
            let image_right = image_area.x + image_area.width;
            let inner_right = inner_area.x + inner_area.width;
            if image_right < inner_right {
                let right_pad = Rect {
                    x: image_right,
                    y: image_area.y,
                    width: inner_right - image_right,
                    height: image_area.height,
                };
                frame.render_widget(Clear, right_pad);
                frame.render_widget(Block::default().style(modal_bg), right_pad);
            }
        }

        // Build title with frame info and controls for GIFs
        let title = if is_multi_frame {
            let state = if app.image_modal.animation_paused {
                "⏸"
            } else {
                "▶"
            };
            // Show Kitty indicator when using native animation
            let mode = if kitty_animating { "Kitty" } else { "GIF" };
            format!(
                " {} {}/{} {} | ←/→:step Space:play/pause q:close ",
                mode,
                app.image_modal.frame_index + 1,
                app.image_modal.gif_frames.len(),
                state
            )
        } else {
            " Image | q/Esc: Close ".to_string()
        };

        // Render modal border (but NOT over image area during animation)
        let modal_border = ratatui::widgets::Block::default()
            .borders(Borders::ALL)
            .border_style(
                Style::default()
                    .fg(theme_heading_1)
                    .add_modifier(Modifier::BOLD),
            )
            .title(title)
            .title_alignment(ratatui::layout::Alignment::Center)
            .style(Style::default().bg(theme_background).fg(theme_foreground));

        // Only render the border frame (not the interior) during animation
        // to avoid overwriting the previous image before new one is drawn
        if avoid_image_area {
            // Render border edges only, preserving image area
            render_border_only(frame, &modal_border, modal_area, image_area);
        } else {
            // Static image or paused - safe to render full modal
            frame.render_widget(modal_border, modal_area);
        }

        // Render image via ratatui-image ONLY when Kitty is NOT handling animation.
        // For Kitty animation, the terminal renders the image directly via graphics protocol.
        if !kitty_animating {
            let img_widget = StatefulImage::new().resize(resize);
            frame.render_stateful_widget(img_widget, image_area, protocol_state);
        }
    }
}

/// Render only the border portions of a block, avoiding the image area.
/// This prevents flickering during GIF animation by not overwriting
/// the previous frame before the new one is drawn.
fn render_border_only(
    frame: &mut Frame,
    block: &ratatui::widgets::Block,
    modal_area: Rect,
    image_area: Rect,
) {
    use ratatui::widgets::Widget;

    // Top border row (full width of modal)
    let top_row = Rect {
        x: modal_area.x,
        y: modal_area.y,
        width: modal_area.width,
        height: 1,
    };
    block.clone().render(top_row, frame.buffer_mut());

    // Bottom border row (full width of modal)
    if modal_area.height > 1 {
        let bottom_row = Rect {
            x: modal_area.x,
            y: modal_area.y + modal_area.height - 1,
            width: modal_area.width,
            height: 1,
        };
        block.clone().render(bottom_row, frame.buffer_mut());
    }

    // Left border column (between top and bottom, avoiding image)
    if modal_area.height > 2 {
        let middle_height = modal_area.height - 2;
        // Left side - from border to image start
        let left_strip_width = image_area.x.saturating_sub(modal_area.x);
        if left_strip_width > 0 {
            let left_strip = Rect {
                x: modal_area.x,
                y: modal_area.y + 1,
                width: left_strip_width,
                height: middle_height,
            };
            block.clone().render(left_strip, frame.buffer_mut());
        }

        // Right side - from image end to border
        let image_right = image_area.x + image_area.width;
        let modal_right = modal_area.x + modal_area.width;
        if image_right < modal_right {
            let right_strip = Rect {
                x: image_right,
                y: modal_area.y + 1,
                width: modal_right - image_right,
                height: middle_height,
            };
            block.clone().render(right_strip, frame.buffer_mut());
        }

        // Top padding (above image, inside border)
        let padding_top_height = image_area.y.saturating_sub(modal_area.y + 1);
        if padding_top_height > 0 && image_area.width > 0 {
            let top_pad = Rect {
                x: image_area.x,
                y: modal_area.y + 1,
                width: image_area.width,
                height: padding_top_height,
            };
            block.clone().render(top_pad, frame.buffer_mut());
        }

        // Bottom padding (below image, inside border)
        let image_bottom = image_area.y + image_area.height;
        let modal_inner_bottom = modal_area.y + modal_area.height - 1;
        if image_bottom < modal_inner_bottom {
            let bottom_pad = Rect {
                x: image_area.x,
                y: image_bottom,
                width: image_area.width,
                height: modal_inner_bottom - image_bottom,
            };
            block.clone().render(bottom_pad, frame.buffer_mut());
        }
    }
}

/// The persistent mode chip shown at the left edge of the status bar.
/// Visible even while a status message occupies the rest of the bar, so the
/// user never loses track of which mode they're in.
fn mode_chip(app: &App) -> Option<(&'static str, Style)> {
    use crate::tui::app::AppMode;
    let theme = &app.theme;

    let chip_style = |accent: Color| {
        Style::default()
            .bg(accent)
            .fg(theme.background)
            .add_modifier(Modifier::BOLD)
    };

    match app.mode {
        AppMode::Interactive => {
            let label = if app.interactive_state.is_in_table_mode() {
                " TABLE "
            } else {
                " INTERACTIVE "
            };
            Some((label, chip_style(theme.heading_2)))
        }
        AppMode::LinkFollow => Some((" LINKS ", chip_style(theme.heading_3))),
        AppMode::DocSearch => Some((" SEARCH ", chip_style(theme.heading_4))),
        AppMode::Search => Some((" FILTER ", chip_style(theme.heading_4))),
        AppMode::CellEdit => Some((" EDIT ", chip_style(theme.heading_5))),
        AppMode::CommandPalette => Some((" PALETTE ", chip_style(theme.heading_1))),
        AppMode::FilePicker | AppMode::FileSearch => Some((" FILES ", chip_style(theme.heading_3))),
        _ => None,
    }
}

fn render_status_bar(frame: &mut Frame, app: &App, area: Rect) {
    use crate::tui::app::AppMode;

    let theme = &app.theme;
    let mut spans: Vec<Span> = Vec::new();
    if let Some((label, style)) = mode_chip(app) {
        spans.push(Span::styled(label, style));
        spans.push(Span::raw(" "));
    }

    // If there's a status message, it occupies the rest of the bar (the mode
    // chip stays visible on the left)
    if let Some(ref msg) = app.status_message {
        spans.push(Span::styled(
            format!(" {} ", msg),
            Style::default()
                .bg(theme.selection_bg)
                .fg(theme.selection_fg)
                .add_modifier(Modifier::BOLD),
        ));
        let status = Paragraph::new(Line::from(spans)).style(theme.status_bar_style());
        frame.render_widget(status, area);
        return;
    }

    // Pending vim-style count prefix (e.g. the "12" of "12j")
    if let Some(count) = app.count_prefix {
        spans.push(Span::styled(
            format!(" {} ", count),
            Style::default()
                .bg(theme.selection_bg)
                .fg(theme.selection_fg)
                .add_modifier(Modifier::BOLD),
        ));
        spans.push(Span::raw(" "));
    }

    let status_text = if app.mode == AppMode::Interactive {
        // Interactive mode status with position info
        let total = app.interactive_state.elements.len();
        let current = app
            .interactive_state
            .current_index
            .map(|i| i + 1)
            .unwrap_or(0);
        let percentage = if total > 0 && current > 0 {
            current * 100 / total
        } else {
            0
        };

        // Get element-specific hint (shows current element info)
        let element_hint = app.interactive_state.get_status_hint();

        format!(
            " {}/{} ({}%) • {}",
            current, total, percentage, element_hint
        )
    } else if app.mode == AppMode::LinkFollow {
        // Link follow mode status
        let link_count = app.links_in_view.len();
        let selected = app.link_picker.selected.map(|i| i + 1).unwrap_or(0);

        let link_info = if link_count > 0 {
            // Show current link details
            let current_link = app
                .link_picker
                .selected
                .and_then(|idx| app.links_in_view.get(idx));

            if let Some(link) = current_link {
                use crate::parser::LinkTarget;
                let target_str = match &link.target {
                    LinkTarget::Anchor(a) => format!("#{}", a),
                    LinkTarget::RelativeFile { path, anchor } => {
                        if let Some(a) = anchor {
                            format!("{}#{}", path.display(), a)
                        } else {
                            path.display().to_string()
                        }
                    }
                    LinkTarget::WikiLink { target, .. } => format!("[[{}]]", target),
                    LinkTarget::External(url) => {
                        // Truncate long URLs
                        util::truncate_with_ellipsis(url, 40)
                    }
                };

                format!(
                    "Link {}/{}: \"{}\" → {}",
                    selected, link_count, link.text, target_str
                )
            } else {
                format!("Link {}/{}", selected, link_count)
            }
        } else {
            "No links in current section".to_string()
        };

        format!(" {} ", link_info)
    } else {
        // Normal mode status - show position based on focus
        let (focus_indicator, position_info) = match app.focus {
            Focus::Outline => {
                let selected_idx = app.outline_state.selected().unwrap_or(0);
                let total = app.outline_items.len();
                let percentage = ((selected_idx + 1) * 100).checked_div(total).unwrap_or(0);
                (
                    "Outline",
                    format!("{}/{} ({}%)", selected_idx + 1, total, percentage),
                )
            }
            Focus::Content => {
                // Show content scroll position
                let scroll_pos = app.content_scroll as usize;
                let content_height = app.content_height;
                let viewport = app.content_viewport_height as usize;
                let bottom_line = (scroll_pos + viewport).min(content_height);
                let percentage = (bottom_line * 100)
                    .checked_div(content_height)
                    .unwrap_or(0)
                    .min(100);
                (
                    "Content",
                    format!("Line {} ({}%)", scroll_pos + 1, percentage),
                )
            }
        };

        let outline_status = if app.show_outline {
            format!("Outline:{}%", app.outline_width)
        } else {
            "Outline:Hidden".to_string()
        };

        let bookmark_indicator = if app.bookmark_position.is_some() {
            " ⚑"
        } else {
            ""
        };

        let history_indicator = if !app.file_history.is_empty() {
            format!(" ← {} ", app.file_history.len())
        } else {
            "".to_string()
        };

        format!(
            " [{}] {}{}{} • {}",
            focus_indicator, position_info, bookmark_indicator, history_indicator, outline_status
        )
    };

    let theme_name = format!(" • Theme:{}", app.theme.name);
    let raw_indicator = if app.show_raw_source { " [RAW]" } else { "" };
    let latex_indicator = if app.latex_detected && app.should_hide_latex() {
        " [LaTeX filtered]"
    } else {
        ""
    };
    let status_text = format!(
        "{}{}{}{}",
        status_text, theme_name, raw_indicator, latex_indicator
    );

    spans.push(Span::raw(status_text));
    let status = Paragraph::new(Line::from(spans)).style(theme.status_bar_style());

    frame.render_widget(status, area);
}

/// Render the footer with context-aware keybinding hints.
///
/// Key labels are derived from the live keybindings (like the help overlay)
/// so the footer stays accurate when the user remaps keys.
fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
    use crate::keybindings::{Action, KeybindingMode};
    use crate::tui::app::AppMode;

    let theme = &app.theme;

    // Look up the display label for an action's bound keys (e.g. "j/k")
    let label = |mode: KeybindingMode, actions: &[Action]| -> String {
        actions
            .iter()
            .filter_map(|a| app.keybindings.keys_for_action(mode, *a).into_iter().next())
            .collect::<Vec<String>>()
            .join("/")
    };

    // (mode, actions, description) per hint; labels resolved from bindings
    type Hint = (KeybindingMode, &'static [Action], &'static str);
    let hints: Vec<Hint> = match app.mode {
        AppMode::Interactive => {
            use Action::*;
            use KeybindingMode::{Interactive, InteractiveTable};

            // Check if we're in table mode
            if app.interactive_state.is_in_table_mode() {
                vec![
                    (
                        InteractiveTable,
                        &[InteractiveNext, InteractivePrevious],
                        "Row",
                    ),
                    (
                        InteractiveTable,
                        &[InteractiveLeft, InteractiveRight],
                        "Col",
                    ),
                    (InteractiveTable, &[InteractiveActivate], "Edit"),
                    (InteractiveTable, &[CopyTableCell], "Copy"),
                    (InteractiveTable, &[ExitMode], "Exit Table"),
                ]
            } else {
                // Get current element type for context-specific hints
                use crate::tui::interactive::ElementType;

                let nav: Hint = (
                    Interactive,
                    &[InteractiveNext, InteractivePrevious],
                    "Navigate",
                );
                let exit: Hint = (Interactive, &[ExitInteractiveMode], "Exit");
                match app.interactive_state.current_element() {
                    Some(elem) => match &elem.element_type {
                        ElementType::Checkbox { .. } => {
                            vec![nav, (Interactive, &[InteractiveActivate], "Toggle"), exit]
                        }
                        ElementType::Table { .. } => vec![
                            nav,
                            (Interactive, &[InteractiveActivate], "Enter Table"),
                            (Interactive, &[CopyContent], "Copy"),
                            exit,
                        ],
                        ElementType::Link { .. } => vec![
                            nav,
                            (Interactive, &[InteractiveActivate], "Follow"),
                            (Interactive, &[CopyContent], "Copy URL"),
                            exit,
                        ],
                        ElementType::Details { .. } => {
                            vec![nav, (Interactive, &[InteractiveActivate], "Expand"), exit]
                        }
                        ElementType::CodeBlock { .. } => {
                            vec![nav, (Interactive, &[CopyContent], "Copy"), exit]
                        }
                        ElementType::Image { .. } => {
                            vec![nav, (Interactive, &[InteractiveActivate], "Open"), exit]
                        }
                    },
                    None => vec![nav, (Interactive, &[InteractiveActivate], "Action"), exit],
                }
            }
        }
        AppMode::LinkFollow => {
            use Action::*;
            use KeybindingMode::LinkFollow;
            vec![
                (LinkFollow, &[NextLink], "Next Link"),
                (LinkFollow, &[FollowLink], "Follow"),
                (LinkFollow, &[CopyContent], "Copy URL"),
                (LinkFollow, &[ExitMode], "Exit"),
            ]
        }
        AppMode::DocSearch => {
            use Action::*;
            use KeybindingMode::DocSearch;
            vec![
                (DocSearch, &[NextMatch, PrevMatch], "Next/Prev"),
                (DocSearch, &[ToggleSearchMode], "Outline Search"),
                (DocSearch, &[ConfirmAction], "Accept"),
                (DocSearch, &[ExitMode], "Cancel"),
            ]
        }
        AppMode::CellEdit => {
            use Action::*;
            use KeybindingMode::CellEdit;
            vec![
                (CellEdit, &[ConfirmAction], "Save"),
                (CellEdit, &[CancelAction], "Cancel"),
            ]
        }
        AppMode::CommandPalette => {
            use Action::*;
            use KeybindingMode::CommandPalette;
            vec![
                (
                    CommandPalette,
                    &[CommandPaletteNext, CommandPalettePrev],
                    "Navigate",
                ),
                (CommandPalette, &[ConfirmAction], "Select"),
                (CommandPalette, &[ExitMode], "Cancel"),
            ]
        }
        _ => {
            use Action::*;
            use KeybindingMode::Normal;
            // Normal mode - show based on focus
            match app.focus {
                Focus::Outline => {
                    vec![
                        (Normal, &[Next, Previous], "Navigate"),
                        (Normal, &[ToggleExpand], "Expand"),
                        (Normal, &[EnterDocSearch], "Search"),
                        (Normal, &[EnterInteractiveMode], "Interactive"),
                        (Normal, &[EnterLinkFollowMode], "Links"),
                        (Normal, &[ToggleHelp], "Help"),
                    ]
                }
                Focus::Content => {
                    vec![
                        (Normal, &[Next, Previous], "Scroll"),
                        (Normal, &[EnterDocSearch], "Search"),
                        (Normal, &[EnterInteractiveMode], "Interactive"),
                        (Normal, &[EnterLinkFollowMode], "Links"),
                        (Normal, &[CopyContent], "Copy"),
                        (Normal, &[ToggleHelp], "Help"),
                    ]
                }
            }
        }
    };

    // Build styled spans using flat_map pattern
    let spans: Vec<Span> = hints
        .iter()
        .filter_map(|(mode, actions, desc)| {
            let key = label(*mode, actions);
            if key.is_empty() {
                return None; // action unbound in user config — hide the hint
            }
            Some(vec![
                Span::styled(format!(" {} ", key), theme.help_key_style()),
                Span::styled(format!("{} ", desc), theme.help_desc_style()),
            ])
        })
        .flatten()
        .collect();

    let line = Line::from(spans);
    let footer = Paragraph::new(line).style(theme.footer_style());

    frame.render_widget(footer, area);
}

use crate::parser::content::parse_content;
use crate::parser::output::{Block as ContentBlock, InlineElement};
use crate::parser::utils::parse_inline_html;
use crate::tui::syntax::SyntaxHighlighter;

/// Render raw markdown source with line numbers
fn render_raw_markdown(content: &str, theme: &Theme) -> Text<'static> {
    let lines: Vec<Line<'static>> = content
        .lines()
        .enumerate()
        .map(|(idx, line)| {
            // Line number with subtle styling (using border color for subtlety)
            let line_num = Span::styled(
                format!("{:4} │ ", idx + 1),
                Style::default().fg(theme.border_unfocused),
            );
            // Replace tabs with spaces to avoid terminal rendering artifacts
            let line_content = line.replace('\t', "    ");
            // Raw content with plain text styling
            let content_span = Span::styled(line_content, Style::default().fg(theme.foreground));
            Line::from(vec![line_num, content_span])
        })
        .collect();

    Text::from(lines)
}

fn render_markdown_enhanced(
    content: &str,
    highlighter: &SyntaxHighlighter,
    theme: &Theme,
    selected_element_id: Option<crate::tui::interactive::ElementId>,
    interactive_state: Option<&crate::tui::interactive::InteractiveState>,
    available_width: Option<u16>,
    _mermaid_placeholder_rows: &std::collections::HashMap<u64, usize>,
) -> Text<'static> {
    let mut lines = Vec::new();

    // Parse content into structured blocks
    let blocks = parse_content(content, 0);

    for (block_idx, block) in blocks.iter().enumerate() {
        // Check if any element in this block is selected (block-level or inline)
        let is_block_selected = selected_element_id
            .map(|id| id.block_idx == block_idx)
            .unwrap_or(false);

        // Get the selected inline element index within this block (if any)
        let selected_inline_idx = selected_element_id
            .filter(|id| id.block_idx == block_idx)
            .and_then(|id| id.sub_idx);

        match block {
            ContentBlock::Heading {
                level,
                content,
                inline,
                ..
            } => {
                // Render sub-heading with appropriate styling
                let mut formatted = if !inline.is_empty() {
                    render_inline_elements(inline, theme, selected_inline_idx)
                } else {
                    format_inline_markdown(content, theme)
                };

                // Apply heading style to all spans
                let heading_style = Style::default()
                    .fg(theme.heading_color(*level))
                    .add_modifier(Modifier::BOLD | Modifier::UNDERLINED);

                for span in &mut formatted {
                    span.style = heading_style;
                }

                // Add selection indicator if selected (with background for visibility)
                if is_block_selected {
                    formatted.insert(
                        0,
                        Span::styled(
                            "→ ",
                            Style::default()
                                .fg(theme.selection_indicator_fg)
                                .bg(theme.selection_indicator_bg)
                                .add_modifier(Modifier::BOLD),
                        ),
                    );
                }

                lines.push(Line::from(formatted));
            }
            ContentBlock::Paragraph { content, inline } => {
                let mut formatted = if !inline.is_empty() {
                    render_inline_elements(inline, theme, selected_inline_idx)
                } else {
                    format_inline_markdown(content, theme)
                };

                // Add selection indicator (with background for visibility)
                if is_block_selected {
                    formatted.insert(
                        0,
                        Span::styled(
                            "→ ",
                            Style::default()
                                .fg(theme.selection_indicator_fg)
                                .bg(theme.selection_indicator_bg)
                                .add_modifier(Modifier::BOLD),
                        ),
                    );
                }

                lines.push(Line::from(formatted));

                // If paragraph contains images, add blank lines to reserve space for them
                // Images will be rendered on top at this position, so we need to push text below down
                let has_images = inline
                    .iter()
                    .any(|elem| matches!(elem, InlineElement::Image { .. }));
                if has_images {
                    // Reserve space for image rendering overlay
                    use crate::tui::interactive::PARAGRAPH_IMAGE_PLACEHOLDER_LINES;
                    for _ in 0..PARAGRAPH_IMAGE_PLACEHOLDER_LINES {
                        lines.push(Line::from(vec![]));
                    }
                }
            }
            ContentBlock::Code {
                language, content, ..
            } => {
                let lang_str = language.as_deref().unwrap_or("");

                #[cfg(all(feature = "mermaid", unix))]
                let is_mermaid = lang_str == "mermaid";
                #[cfg(not(all(feature = "mermaid", unix)))]
                let is_mermaid = false;

                if is_mermaid {
                    // Mermaid diagram: render header + placeholder lines for image overlay
                    let mut header_spans = vec![];
                    if is_block_selected {
                        header_spans.push(Span::styled(
                            "→ ",
                            Style::default()
                                .fg(theme.selection_indicator_fg)
                                .bg(theme.selection_indicator_bg)
                                .add_modifier(Modifier::BOLD),
                        ));
                    }
                    header_spans.push(Span::styled("▸ mermaid diagram", theme.code_fence_style()));
                    lines.push(Line::from(header_spans));

                    // Reserve blank lines for the image overlay.
                    // Use pixel-accurate row count once the image has been rendered;
                    // fall back to the source-line heuristic on first load.
                    #[cfg(all(feature = "mermaid", unix))]
                    use crate::tui::app::App as MermaidApp;
                    use crate::tui::interactive::mermaid_placeholder_lines;
                    let placeholder_rows = {
                        #[cfg(all(feature = "mermaid", unix))]
                        {
                            let hash = MermaidApp::mermaid_source_hash(content);
                            _mermaid_placeholder_rows
                                .get(&hash)
                                .copied()
                                .unwrap_or_else(|| mermaid_placeholder_lines(content))
                        }
                        #[cfg(not(all(feature = "mermaid", unix)))]
                        mermaid_placeholder_lines(content)
                    };
                    for _ in 0..placeholder_rows {
                        lines.push(Line::from(vec![]));
                    }
                } else {
                    // Standard code block: opening fence + highlighted code + closing fence
                    let mut fence_spans = vec![];
                    if is_block_selected {
                        fence_spans.push(Span::styled(
                            "→ ",
                            Style::default()
                                .fg(theme.selection_indicator_fg)
                                .bg(theme.selection_indicator_bg)
                                .add_modifier(Modifier::BOLD),
                        ));
                    }
                    fence_spans.push(Span::styled(
                        format!("```{}", lang_str),
                        theme.code_fence_style(),
                    ));

                    #[cfg(not(all(feature = "mermaid", unix)))]
                    if lang_str == "mermaid" {
                        fence_spans.push(Span::styled(
                            " (enable 'mermaid' feature to render)",
                            Style::default().fg(Color::DarkGray),
                        ));
                    }

                    lines.push(Line::from(fence_spans));

                    // Highlighted code
                    let highlighted = highlighter.highlight_code(content, lang_str);
                    lines.extend(highlighted);

                    // Closing fence
                    lines.push(Line::from(vec![Span::styled(
                        "```".to_string(),
                        theme.code_fence_style(),
                    )]));
                }
            }
            ContentBlock::List { ordered, items } => {
                for (idx, item) in items.iter().enumerate() {
                    // Check if this specific list item (checkbox) is selected
                    let is_item_selected = selected_element_id
                        .map(|id| id.block_idx == block_idx && id.sub_idx == Some(idx))
                        .unwrap_or(false);

                    // Check if a link within this list item is selected
                    use crate::tui::interactive::{LINK_ITEM_MULTIPLIER, LINK_OFFSET};
                    let selected_link_inline_idx = selected_element_id.and_then(|id| {
                        if id.block_idx == block_idx {
                            id.sub_idx.and_then(|sub| {
                                // Decode: check if this is a link sub_idx for this item
                                let item_link_base = idx * LINK_ITEM_MULTIPLIER + LINK_OFFSET;
                                let next_item_link_base =
                                    (idx + 1) * LINK_ITEM_MULTIPLIER + LINK_OFFSET;
                                if sub >= LINK_OFFSET
                                    && sub >= item_link_base
                                    && sub < next_item_link_base
                                {
                                    Some(sub - item_link_base)
                                } else {
                                    None
                                }
                            })
                        } else {
                            None
                        }
                    });

                    // Determine which line should have the pointer based on selected link's line_offset
                    let selected_line_offset: Option<usize> =
                        selected_link_inline_idx.and_then(|inline_idx| {
                            item.inline.get(inline_idx).and_then(|elem| {
                                if let InlineElement::Link { line_offset, .. } = elem {
                                    // Use line_offset if provided, otherwise default to 0
                                    Some(line_offset.unwrap_or(0))
                                } else {
                                    None
                                }
                            })
                        });

                    // For checkboxes, always select line 0; for links, use their line_offset
                    let pointer_line = if is_item_selected {
                        Some(0)
                    } else {
                        selected_line_offset
                    };

                    // Check if content has nested items (contains newlines with indentation)
                    let has_nested = item.content.contains('\n');

                    if has_nested {
                        // Render multi-line item with nested items
                        let content_lines = item.content.lines();
                        for (line_idx, line) in content_lines.enumerate() {
                            // Check if this specific line should have the pointer
                            let show_pointer = pointer_line == Some(line_idx);

                            if line_idx == 0 {
                                // First line: use regular list marker
                                let mut spans = vec![];

                                // Pointer replaces leading spaces, not prepended
                                if show_pointer {
                                    spans.push(Span::styled(
                                        "→ ",
                                        Style::default()
                                            .fg(theme.selection_indicator_fg)
                                            .bg(theme.selection_indicator_bg)
                                            .add_modifier(Modifier::BOLD),
                                    ));
                                } else {
                                    spans.push(Span::raw("  "));
                                }

                                let prefix = if let Some(checked) = item.checked {
                                    let checkbox = if checked { "☑" } else { "☐" };
                                    format!("{} ", checkbox)
                                } else if *ordered {
                                    format!("{}. ", idx + 1)
                                } else {
                                    "• ".to_string()
                                };
                                let formatted = format_inline_markdown(line, theme);
                                spans.push(Span::styled(
                                    prefix,
                                    Style::default().fg(theme.list_bullet),
                                ));
                                spans.extend(formatted);
                                lines.push(Line::from(spans));
                            } else {
                                // Nested items: detect indentation and add bullet/checkbox
                                let trimmed = line.trim_start();
                                let indent_count = line.len() - trimmed.len();
                                if indent_count > 0 {
                                    // Check if this is a task list item by looking for checkbox in text
                                    let (is_task, checked, text_after_marker) =
                                        detect_checkbox_in_text(trimmed);

                                    let mut spans = vec![];

                                    // Calculate indent: reserve 2 chars for pointer at appropriate depth
                                    // Base indent (2) + nested indent, with pointer replacing last 2 chars
                                    let total_indent = indent_count + 2;
                                    if show_pointer {
                                        // Indent up to pointer position, then pointer
                                        let pre_pointer_indent =
                                            " ".repeat(total_indent.saturating_sub(2));
                                        spans.push(Span::raw(pre_pointer_indent));
                                        spans.push(Span::styled(
                                            "→ ",
                                            Style::default()
                                                .fg(theme.selection_indicator_fg)
                                                .bg(theme.selection_indicator_bg)
                                                .add_modifier(Modifier::BOLD),
                                        ));
                                    } else {
                                        spans.push(Span::raw(" ".repeat(total_indent)));
                                    }

                                    let marker = if is_task {
                                        // Task list item with checkbox
                                        if checked { "☑ " } else { "☐ " }
                                    } else {
                                        // Regular bullet
                                        "• "
                                    };

                                    let formatted =
                                        format_inline_markdown(text_after_marker, theme);
                                    spans.push(Span::styled(
                                        marker,
                                        Style::default().fg(theme.list_bullet),
                                    ));
                                    spans.extend(formatted);
                                    lines.push(Line::from(spans));
                                } else {
                                    // Empty line or continuation
                                    lines.push(Line::from(line.to_string()));
                                }
                            }
                        }
                    } else {
                        // Simple single-line item (or item with nested blocks)
                        let formatted = if !item.inline.is_empty() {
                            render_inline_elements(&item.inline, theme, selected_link_inline_idx)
                        } else {
                            format_inline_markdown(&item.content, theme)
                        };

                        let mut spans = vec![];

                        // Pointer replaces leading spaces, not prepended
                        if pointer_line.is_some() {
                            spans.push(Span::styled(
                                "→ ",
                                Style::default()
                                    .fg(theme.selection_indicator_fg)
                                    .bg(theme.selection_indicator_bg)
                                    .add_modifier(Modifier::BOLD),
                            ));
                        } else {
                            spans.push(Span::raw("  "));
                        }

                        let prefix = if let Some(checked) = item.checked {
                            let checkbox = if checked { "☑" } else { "☐" };
                            format!("{} ", checkbox)
                        } else if *ordered {
                            format!("{}. ", idx + 1)
                        } else {
                            "• ".to_string()
                        };

                        spans.push(Span::styled(prefix, Style::default().fg(theme.list_bullet)));
                        spans.extend(formatted);
                        lines.push(Line::from(spans));
                    }

                    // Render nested blocks within this list item (e.g., code blocks)
                    use crate::tui::interactive::{
                        CODE_BLOCK_OFFSET, IMAGE_OFFSET, ITEM_MULTIPLIER, NESTED_MULTIPLIER,
                        TABLE_OFFSET,
                    };
                    for (nested_idx, nested_block) in item.blocks.iter().enumerate() {
                        // Check if this nested block is selected
                        let is_nested_selected = selected_element_id
                            .map(|id| {
                                if id.block_idx != block_idx {
                                    return false;
                                }
                                if let Some(sub) = id.sub_idx {
                                    // Decode the sub_idx to check if it matches this nested block
                                    let base =
                                        idx * ITEM_MULTIPLIER + nested_idx * NESTED_MULTIPLIER;
                                    sub == base + CODE_BLOCK_OFFSET
                                        || sub == base + TABLE_OFFSET
                                        || sub == base + IMAGE_OFFSET
                                } else {
                                    false
                                }
                            })
                            .unwrap_or(false);

                        // Reduce width by indent (5 spaces)
                        let nested_width = available_width.map(|w| w.saturating_sub(5));
                        let nested_lines =
                            render_block_to_lines(nested_block, highlighter, theme, nested_width);
                        for (line_idx, nested_line) in nested_lines.into_iter().enumerate() {
                            let mut indented_spans = vec![];

                            // Add selection indicator on first line of nested block
                            if is_nested_selected && line_idx == 0 {
                                indented_spans.push(Span::styled(
                                    "→ ",
                                    Style::default()
                                        .fg(theme.selection_indicator_fg)
                                        .bg(theme.selection_indicator_bg)
                                        .add_modifier(Modifier::BOLD),
                                ));
                                indented_spans.push(Span::raw("   ")); // 3 spaces (5 - 2 for arrow)
                            } else {
                                indented_spans.push(Span::raw("     ")); // 5 spaces indent
                            }

                            indented_spans.extend(nested_line.spans);
                            lines.push(Line::from(indented_spans));
                        }
                    }
                }
            }
            ContentBlock::Blockquote {
                content,
                blocks: nested,
            } => {
                // GFM alerts / Obsidian callouts: > [!NOTE], > [!warning] …
                if let Some(callout_lines) = render_callout_lines(content, theme) {
                    lines.extend(callout_lines);
                } else
                // If we have nested blocks, render them recursively
                if !nested.is_empty() {
                    for nested_block in nested {
                        // Reduce width by blockquote prefix (2 chars)
                        let nested_width = available_width.map(|w| w.saturating_sub(2));
                        let nested_lines =
                            render_block_to_lines(nested_block, highlighter, theme, nested_width);
                        for nested_line in nested_lines {
                            let mut spans = vec![Span::styled(
                                "│ ",
                                Style::default().fg(theme.blockquote_border),
                            )];
                            spans.extend(nested_line.spans.into_iter().map(|span| {
                                Span::styled(
                                    span.content,
                                    span.style
                                        .fg(theme.blockquote_fg)
                                        .add_modifier(Modifier::ITALIC),
                                )
                            }));
                            lines.push(Line::from(spans));
                        }
                    }
                } else {
                    // Fallback to raw content
                    for line in content.lines() {
                        let formatted = format_inline_markdown(line, theme);
                        let mut spans = vec![Span::styled(
                            "│ ",
                            Style::default().fg(theme.blockquote_border),
                        )];
                        spans.extend(formatted.into_iter().map(|span| {
                            Span::styled(
                                span.content,
                                span.style
                                    .fg(theme.blockquote_fg)
                                    .add_modifier(Modifier::ITALIC),
                            )
                        }));
                        lines.push(Line::from(spans));
                    }
                }
            }
            ContentBlock::Table {
                headers,
                alignments,
                rows,
            } => {
                // Get selected cell position if in table navigation mode
                let (in_table_mode, selected_cell) = if is_block_selected {
                    let in_mode = interactive_state
                        .map(|state| state.is_in_table_mode())
                        .unwrap_or(false);
                    let cell = if in_mode {
                        interactive_state.and_then(|state| state.get_table_position())
                    } else {
                        None
                    };
                    (in_mode, cell)
                } else {
                    (false, None)
                };

                // Use available_width for smart table collapsing
                let table_lines = render_table(
                    headers,
                    alignments,
                    rows,
                    theme,
                    is_block_selected,
                    in_table_mode,
                    selected_cell,
                    available_width,
                );
                lines.extend(table_lines);
            }
            ContentBlock::Image { alt, src, .. } => {
                // Create placeholder space for image
                // The actual image will be rendered as a StatefulImage widget
                let mut img_line = vec![];
                if is_block_selected {
                    img_line.push(Span::styled(
                        "→ ",
                        Style::default()
                            .fg(theme.selection_indicator_fg)
                            .bg(theme.selection_indicator_bg)
                            .add_modifier(Modifier::BOLD),
                    ));
                }
                img_line.push(Span::styled("🖼 ", Style::default().fg(theme.list_bullet)));
                img_line.push(Span::styled(
                    alt.clone(),
                    Style::default()
                        .fg(theme.link_fg)
                        .add_modifier(Modifier::ITALIC),
                ));
                img_line.push(Span::raw(" "));
                img_line.push(Span::styled(
                    format!("({})", src),
                    Style::default().fg(theme.blockquote_fg),
                ));
                lines.push(Line::from(img_line));

                // Add empty lines as placeholder for image rendering overlay
                use crate::tui::interactive::IMAGE_PLACEHOLDER_LINES;
                for _ in 0..IMAGE_PLACEHOLDER_LINES {
                    lines.push(Line::from(""));
                }
            }
            ContentBlock::Details {
                summary,
                blocks: nested,
                ..
            } => {
                // Check if this details block is expanded
                let element_id = crate::tui::interactive::ElementId {
                    block_idx,
                    sub_idx: None,
                };
                let is_expanded = interactive_state
                    .map(|state| state.is_details_expanded(element_id))
                    .unwrap_or(false);

                // Render details block with expand/collapse indicator
                let mut summary_spans = vec![];

                // Add selection indicator (with background for visibility)
                if is_block_selected {
                    summary_spans.push(Span::styled(
                        "→ ",
                        Style::default()
                            .fg(theme.selection_indicator_fg)
                            .bg(theme.selection_indicator_bg)
                            .add_modifier(Modifier::BOLD),
                    ));
                }

                // Show ▼ when expanded, ▶ when collapsed
                let indicator = if is_expanded { "▼ " } else { "▶ " };
                summary_spans.push(Span::styled(
                    indicator,
                    Style::default().fg(theme.list_bullet),
                ));

                // Parse and render inline HTML in summary (e.g., <strong>Navigation</strong>)
                let summary_elements = parse_inline_html(summary);
                let rendered_summary = render_inline_elements(&summary_elements, theme, None);
                summary_spans.extend(rendered_summary);

                lines.push(Line::from(summary_spans));

                // Only render nested content if expanded
                if is_expanded {
                    for (nested_idx, nested_block) in nested.iter().enumerate() {
                        // Check if this nested block is selected
                        let nested_sub_idx = crate::tui::interactive::DETAILS_NESTED_BASE
                            + nested_idx * crate::tui::interactive::DETAILS_NESTED_MULTIPLIER;

                        // Check various offsets for different block types
                        let table_id = nested_sub_idx + crate::tui::interactive::TABLE_OFFSET;
                        let code_id = nested_sub_idx + crate::tui::interactive::CODE_BLOCK_OFFSET;
                        let image_id = nested_sub_idx + crate::tui::interactive::IMAGE_OFFSET;

                        let is_nested_selected = selected_element_id
                            .map(|sel_id| {
                                sel_id.block_idx == block_idx
                                    && sel_id.sub_idx.is_some_and(|sub| {
                                        sub == table_id
                                            || sub == code_id
                                            || sub == image_id
                                            || (sub
                                                >= nested_sub_idx
                                                    + crate::tui::interactive::LINK_OFFSET
                                                && sub
                                                    < nested_sub_idx
                                                        + crate::tui::interactive::LINK_OFFSET
                                                        + 100)
                                    })
                            })
                            .unwrap_or(false);

                        // Handle tables specially to preserve interactive rendering
                        if let ContentBlock::Table {
                            headers: nested_headers,
                            alignments: nested_alignments,
                            rows: nested_rows,
                        } = nested_block
                        {
                            // Check if this specific table is selected and in table mode
                            let is_this_table_selected = selected_element_id
                                .map(|sel_id| {
                                    sel_id.block_idx == block_idx
                                        && sel_id.sub_idx == Some(table_id)
                                })
                                .unwrap_or(false);

                            let (in_table_mode, selected_cell) = if is_this_table_selected {
                                let in_mode = interactive_state
                                    .map(|state| state.is_in_table_mode())
                                    .unwrap_or(false);
                                let cell = if in_mode {
                                    interactive_state.and_then(|state| state.get_table_position())
                                } else {
                                    None
                                };
                                (in_mode, cell)
                            } else {
                                (false, None)
                            };

                            // Reduce available width by indent (2 spaces)
                            let nested_width = available_width.map(|w| w.saturating_sub(2));
                            let table_lines = render_table(
                                nested_headers,
                                nested_alignments,
                                nested_rows,
                                theme,
                                is_this_table_selected,
                                in_table_mode,
                                selected_cell,
                                nested_width,
                            );

                            for nested_line in table_lines {
                                let mut spans = vec![Span::raw("  ")]; // Indent
                                spans.extend(nested_line.spans);
                                lines.push(Line::from(spans));
                            }
                        } else {
                            // Other block types use the standard renderer
                            // Reduce width by indent (2 spaces)
                            let block_width = available_width.map(|w| w.saturating_sub(2));
                            let nested_lines = render_block_to_lines(
                                nested_block,
                                highlighter,
                                theme,
                                block_width,
                            );
                            for (line_idx, nested_line) in nested_lines.into_iter().enumerate() {
                                let mut spans = vec![];

                                // Add selection indicator for first line of selected nested block
                                if is_nested_selected && line_idx == 0 {
                                    spans.push(Span::styled(
                                        "→ ",
                                        Style::default()
                                            .fg(theme.selection_indicator_fg)
                                            .bg(theme.selection_indicator_bg)
                                            .add_modifier(Modifier::BOLD),
                                    ));
                                } else {
                                    spans.push(Span::raw("  ")); // Indent
                                }

                                spans.extend(nested_line.spans);
                                lines.push(Line::from(spans));
                            }
                        }
                    }
                }
            }
            ContentBlock::HorizontalRule => {
                let width = available_width.map(|w| w as usize).unwrap_or(60).max(1);
                lines.push(Line::from(vec![Span::styled(
                    "─".repeat(width),
                    Style::default().fg(theme.border_unfocused),
                )]));
            }
        }

        // Add blank line after most blocks for spacing
        lines.push(Line::from(""));
    }

    Text::from(lines)
}

/// Apply search highlighting to rendered text while preserving original span styles.
/// This function overlays search highlight styles on top of existing styling (links, bold, etc.)
fn apply_search_highlighting(
    text: Text<'static>,
    query: &str,
    current_match_idx: Option<usize>,
    total_matches: usize,
    theme: &Theme,
) -> Text<'static> {
    if query.is_empty() {
        return text;
    }

    let query_lower = query.to_lowercase();
    let mut new_lines = Vec::new();
    let mut match_counter = 0usize;

    for line in text.lines.into_iter() {
        // Build span index: (byte_start, byte_end, span_index)
        let mut span_ranges: Vec<(usize, usize, usize)> = Vec::new();
        let mut byte_pos = 0;
        for (idx, span) in line.spans.iter().enumerate() {
            let span_len = span.content.len();
            span_ranges.push((byte_pos, byte_pos + span_len, idx));
            byte_pos += span_len;
        }

        // Join all spans to get the full line text for searching
        let full_text: String = line.spans.iter().map(|s| s.content.as_ref()).collect();
        let full_text_lower = full_text.to_lowercase();

        // Find all occurrences of the query in this line (non-overlapping)
        let mut matches_in_line: Vec<(usize, usize)> = Vec::new();
        let mut search_start = 0;
        let query_len = query.len();

        while let Some(rel_pos) = full_text_lower[search_start..].find(&query_lower) {
            let byte_start = search_start + rel_pos;
            let byte_end = byte_start + query_len;

            // Verify we're on valid char boundaries
            if full_text.is_char_boundary(byte_start) && full_text.is_char_boundary(byte_end) {
                matches_in_line.push((byte_start, byte_end));
            }

            search_start = byte_end;
            if search_start >= full_text_lower.len() {
                break;
            }
        }

        if matches_in_line.is_empty() {
            // No matches in this line - keep original
            new_lines.push(line);
        } else {
            // Rebuild line with highlighted matches while preserving original styles
            let mut new_spans: Vec<Span<'static>> = Vec::new();

            // Process each original span and split it at match boundaries
            for (span_start, span_end, span_idx) in &span_ranges {
                let original_span = &line.spans[*span_idx];
                let original_style = original_span.style;
                let span_text = original_span.content.as_ref();

                // Find which matches overlap with this span
                let mut current_pos = 0; // position within the span

                for (match_start, match_end) in &matches_in_line {
                    // Skip matches that are entirely before this span
                    if *match_end <= *span_start {
                        continue;
                    }
                    // Stop if match is entirely after this span
                    if *match_start >= *span_end {
                        break;
                    }

                    let is_current = total_matches > 0 && current_match_idx == Some(match_counter);

                    // Calculate positions relative to span
                    let rel_match_start = match_start.saturating_sub(*span_start);
                    let rel_match_end = (*match_end).min(*span_end) - *span_start;

                    // Add text before the match (with original style)
                    if current_pos < rel_match_start
                        && let Some(before_text) =
                            safe_slice(span_text, current_pos, rel_match_start)
                        && !before_text.is_empty()
                    {
                        new_spans.push(Span::styled(before_text.to_string(), original_style));
                    }

                    // Add the matched portion (with search highlight style)
                    let highlight_style = if is_current {
                        theme.search_current_style()
                    } else {
                        theme.search_match_style()
                    };

                    let actual_start = rel_match_start.max(current_pos);
                    if let Some(match_text) = safe_slice(span_text, actual_start, rel_match_end)
                        && !match_text.is_empty()
                    {
                        new_spans.push(Span::styled(match_text.to_string(), highlight_style));
                    }

                    current_pos = rel_match_end;

                    // Only increment counter when we finish the match (match_end <= span_end)
                    if *match_end <= *span_end {
                        match_counter += 1;
                    }
                }

                // Add remaining text after all matches in this span (with original style)
                if current_pos < span_text.len()
                    && let Some(after_text) = safe_slice(span_text, current_pos, span_text.len())
                    && !after_text.is_empty()
                {
                    new_spans.push(Span::styled(after_text.to_string(), original_style));
                }
            }

            new_lines.push(Line::from(new_spans));
        }
    }

    Text::from(new_lines)
}

/// Safely slice a string at byte boundaries, returning None if boundaries are invalid
fn safe_slice(s: &str, start: usize, end: usize) -> Option<&str> {
    if start > end || end > s.len() {
        return None;
    }
    if !s.is_char_boundary(start) || !s.is_char_boundary(end) {
        return None;
    }
    Some(&s[start..end])
}

/// A recognized GFM alert / Obsidian callout marker like `[!NOTE]` or
/// `[!warning] Custom title` at the start of a blockquote.
struct CalloutMarker {
    kind: String,
    title: String,
}

/// Parse the first line of a blockquote for a callout marker.
fn parse_callout_marker(first_line: &str) -> Option<CalloutMarker> {
    let trimmed = first_line.trim();
    let rest = trimmed.strip_prefix("[!")?;
    let end = rest.find(']')?;
    let kind_raw = &rest[..end];
    if kind_raw.is_empty()
        || !kind_raw
            .chars()
            .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
    {
        return None;
    }

    // Obsidian fold markers (`[!note]+` / `[!note]-`) and optional title
    let mut after = rest[end + 1..].trim_start();
    if let Some(stripped) = after.strip_prefix(['+', '-']) {
        after = stripped.trim_start();
    }

    let title = if after.is_empty() {
        // Title-case the kind: NOTE -> Note
        let lower = kind_raw.to_ascii_lowercase();
        let mut chars = lower.chars();
        let c = chars.next()?;
        c.to_uppercase().collect::<String>() + chars.as_str()
    } else {
        after.to_string()
    };

    Some(CalloutMarker {
        kind: kind_raw.to_ascii_uppercase(),
        title,
    })
}

/// Icon and accent color for a callout kind (GFM alerts + common Obsidian
/// callout kinds; unknown kinds get a neutral default).
fn callout_decoration(kind: &str, theme: &Theme) -> (&'static str, Color) {
    match kind {
        "NOTE" | "INFO" => ("ℹ", theme.link_fg),
        "TIP" | "HINT" | "SUCCESS" | "CHECK" | "DONE" => ("💡", theme.heading_3),
        "IMPORTANT" => ("❗", theme.heading_1),
        "WARNING" | "ATTENTION" => ("⚠", theme.search_match_bg),
        "CAUTION" | "DANGER" | "ERROR" | "BUG" | "FAILURE" | "FAIL" => {
            ("🛑", theme.search_current_bg)
        }
        "QUESTION" | "FAQ" | "HELP" => ("❓", theme.heading_4),
        "EXAMPLE" => ("✏", theme.heading_5),
        "QUOTE" | "CITE" => ("❝", theme.blockquote_fg),
        "TODO" => ("☐", theme.heading_2),
        "ABSTRACT" | "SUMMARY" | "TLDR" => ("📋", theme.heading_2),
        _ => ("❯", theme.link_fg),
    }
}

/// Render a blockquote as a styled callout if its first line carries a
/// callout marker. Returns None when the blockquote is not a callout.
fn render_callout_lines(content: &str, theme: &Theme) -> Option<Vec<Line<'static>>> {
    let mut content_lines = content.lines();
    let marker = parse_callout_marker(content_lines.next()?)?;
    let (icon, accent) = callout_decoration(&marker.kind, theme);

    let bar = || Span::styled("▌ ", Style::default().fg(accent));
    let mut lines = vec![Line::from(vec![
        bar(),
        Span::styled(
            format!("{} {}", icon, marker.title),
            Style::default().fg(accent).add_modifier(Modifier::BOLD),
        ),
    ])];

    for line in content_lines {
        let mut spans = vec![bar()];
        spans.extend(format_inline_markdown(line, theme));
        lines.push(Line::from(spans));
    }

    Some(lines)
}

fn render_block_to_lines(
    block: &ContentBlock,
    highlighter: &SyntaxHighlighter,
    theme: &Theme,
    available_width: Option<u16>,
) -> Vec<Line<'static>> {
    let mut lines = Vec::new();

    match block {
        ContentBlock::Heading {
            level,
            content,
            inline,
            ..
        } => {
            // Render heading with appropriate styling
            let mut formatted = if !inline.is_empty() {
                render_inline_elements(inline, theme, None)
            } else {
                format_inline_markdown(content, theme)
            };

            // Apply heading style to all spans
            let heading_style = Style::default()
                .fg(theme.heading_color(*level))
                .add_modifier(Modifier::BOLD | Modifier::UNDERLINED);

            for span in &mut formatted {
                span.style = heading_style;
            }

            lines.push(Line::from(formatted));
        }
        ContentBlock::Paragraph { content, inline } => {
            let formatted = if !inline.is_empty() {
                render_inline_elements(inline, theme, None)
            } else {
                format_inline_markdown(content, theme)
            };
            lines.push(Line::from(formatted));
        }
        ContentBlock::Code {
            language, content, ..
        } => {
            let lang_str = language.as_deref().unwrap_or("");

            // Opening fence
            lines.push(Line::from(vec![Span::styled(
                format!("```{}", lang_str),
                theme.code_fence_style(),
            )]));

            // Highlighted code
            let highlighted = highlighter.highlight_code(content, lang_str);
            lines.extend(highlighted);

            // Closing fence
            lines.push(Line::from(vec![Span::styled(
                "```".to_string(),
                theme.code_fence_style(),
            )]));
        }
        ContentBlock::Details {
            summary,
            blocks: nested,
            ..
        } => {
            // Render details with collapsed indicator
            let mut summary_spans =
                vec![Span::styled("▶ ", Style::default().fg(theme.list_bullet))];

            // Parse and render inline HTML in summary (e.g., <strong>Navigation</strong>)
            let summary_elements = parse_inline_html(summary);
            let rendered_summary = render_inline_elements(&summary_elements, theme, None);
            summary_spans.extend(rendered_summary);

            lines.push(Line::from(summary_spans));

            // Render nested content (indented)
            for nested_block in nested {
                // Reduce width by indent (2 spaces)
                let nested_width = available_width.map(|w| w.saturating_sub(2));
                let nested_lines =
                    render_block_to_lines(nested_block, highlighter, theme, nested_width);
                for nested_line in nested_lines {
                    let mut spans = vec![Span::raw("  ")];
                    spans.extend(nested_line.spans);
                    lines.push(Line::from(spans));
                }
            }
        }
        ContentBlock::Table {
            headers,
            alignments,
            rows,
        } => {
            // Render table (non-interactive, no selection)
            let table_lines = render_table(
                headers,
                alignments,
                rows,
                theme,
                false,
                false,
                None,
                available_width,
            );
            lines.extend(table_lines);
        }
        ContentBlock::List { ordered, items } => {
            for (i, item) in items.iter().enumerate() {
                let marker = if *ordered {
                    format!("{}. ", i + 1)
                } else {
                    "• ".to_string()
                };

                // Render item content
                let item_spans = if !item.inline.is_empty() {
                    render_inline_elements(&item.inline, theme, None)
                } else {
                    format_inline_markdown(&item.content, theme)
                };

                let mut line_spans =
                    vec![Span::styled(marker, Style::default().fg(theme.list_bullet))];
                line_spans.extend(item_spans);
                lines.push(Line::from(line_spans));

                // Render nested blocks (indented)
                for nested in &item.blocks {
                    // Reduce width by indent (2 spaces)
                    let nested_width = available_width.map(|w| w.saturating_sub(2));
                    let nested_lines =
                        render_block_to_lines(nested, highlighter, theme, nested_width);
                    for nested_line in nested_lines {
                        let mut spans = vec![Span::raw("  ")];
                        spans.extend(nested_line.spans);
                        lines.push(Line::from(spans));
                    }
                }
            }
        }
        ContentBlock::Blockquote { content, blocks } => {
            // GFM alerts / Obsidian callouts: > [!NOTE], > [!warning] …
            if let Some(callout_lines) = render_callout_lines(content, theme) {
                lines.extend(callout_lines);
                return lines;
            }
            // Render blockquote with > prefix
            let formatted = format_inline_markdown(content, theme);
            let mut quote_spans = vec![Span::styled(
                "│ ",
                Style::default().fg(theme.blockquote_border),
            )];
            quote_spans.extend(formatted);
            lines.push(Line::from(quote_spans));

            // Render nested blocks
            for nested in blocks {
                // Reduce width by blockquote prefix (2 chars)
                let nested_width = available_width.map(|w| w.saturating_sub(2));
                let nested_lines = render_block_to_lines(nested, highlighter, theme, nested_width);
                for nested_line in nested_lines {
                    let mut spans = vec![Span::styled(
                        "│ ",
                        Style::default().fg(theme.blockquote_border),
                    )];
                    spans.extend(nested_line.spans);
                    lines.push(Line::from(spans));
                }
            }
        }
        ContentBlock::Image { alt, src, .. } => {
            let image_spans = vec![
                Span::styled("🖼 ", Style::default().fg(theme.link_fg)),
                Span::styled(
                    format!("{} ({})", alt, src),
                    Style::default()
                        .fg(theme.link_fg)
                        .add_modifier(Modifier::ITALIC),
                ),
            ];
            lines.push(Line::from(image_spans));
        }
        ContentBlock::HorizontalRule => {
            let width = available_width.map(|w| w as usize).unwrap_or(40).max(1);
            lines.push(Line::from(vec![Span::styled(
                "─".repeat(width),
                Style::default().fg(theme.border_unfocused),
            )]));
        }
    }

    lines
}

fn render_inline_elements(
    elements: &[InlineElement],
    theme: &Theme,
    selected_inline_idx: Option<usize>,
) -> Vec<Span<'static>> {
    let mut spans = Vec::new();

    for (idx, element) in elements.iter().enumerate() {
        let is_selected = selected_inline_idx == Some(idx);

        match element {
            InlineElement::Text { value } => {
                spans.push(Span::styled(value.clone(), theme.text_style()));
            }
            InlineElement::Strong { value } => {
                spans.push(Span::styled(value.clone(), theme.bold_style()));
            }
            InlineElement::Emphasis { value } => {
                spans.push(Span::styled(value.clone(), theme.italic_style()));
            }
            InlineElement::Code { value } => {
                spans.push(Span::styled(value.clone(), theme.inline_code_style()));
            }
            InlineElement::Link { text, .. } => {
                if is_selected {
                    // Add selection indicator before selected link (with background for visibility)
                    spans.push(Span::styled(
                        "▸ ",
                        Style::default()
                            .fg(theme.selection_indicator_fg)
                            .bg(theme.selection_indicator_bg)
                            .add_modifier(Modifier::BOLD),
                    ));
                }
                let style = if is_selected {
                    // Highlighted selected link - matches table cell selection style
                    Style::default()
                        .fg(theme.link_selected_fg)
                        .bg(theme.link_selected_bg)
                        .add_modifier(Modifier::BOLD | Modifier::UNDERLINED)
                } else {
                    // Normal link style
                    Style::default()
                        .fg(theme.link_fg)
                        .add_modifier(Modifier::UNDERLINED)
                };
                spans.push(Span::styled(text.clone(), style));
            }
            InlineElement::Strikethrough { value } => {
                spans.push(Span::styled(
                    value.clone(),
                    Style::default()
                        .fg(theme.blockquote_fg)
                        .add_modifier(Modifier::CROSSED_OUT),
                ));
            }
            InlineElement::Image { .. } => {
                // Images are rendered separately, not as placeholder text
                // This allows them to appear in-place without text alongside
            }
        }
    }

    if spans.is_empty() {
        spans.push(Span::raw(""));
    }

    spans
}

pub(crate) fn format_inline_markdown<'a>(text: &str, theme: &Theme) -> Vec<Span<'a>> {
    let mut spans = Vec::new();
    let mut current = String::new();
    let chars: Vec<char> = text.chars().collect();
    let mut i = 0;

    while i < chars.len() {
        // Check for markdown link [text](url)
        if chars[i] == '[' {
            // Look for the closing ] and opening (
            let mut j = i + 1;
            let mut link_text = String::new();
            while j < chars.len() && chars[j] != ']' {
                link_text.push(chars[j]);
                j += 1;
            }
            // Check if followed by (url)
            if j + 1 < chars.len() && chars[j] == ']' && chars[j + 1] == '(' {
                let mut k = j + 2;
                let mut url = String::new();
                while k < chars.len() && chars[k] != ')' {
                    url.push(chars[k]);
                    k += 1;
                }
                if k < chars.len() && chars[k] == ')' {
                    // Valid link found
                    if !current.is_empty() {
                        spans.push(Span::raw(current.clone()));
                        current.clear();
                    }
                    // Render link text with link styling
                    spans.push(Span::styled(
                        link_text,
                        Style::default()
                            .fg(theme.link_fg)
                            .add_modifier(Modifier::UNDERLINED),
                    ));
                    i = k + 1; // Move past the closing )
                    continue;
                }
            }
            // Not a valid link, treat [ as regular character
            current.push(chars[i]);
            i += 1;
        }
        // Check for inline code `code`
        else if chars[i] == '`' {
            if !current.is_empty() {
                spans.push(Span::raw(current.clone()));
                current.clear();
            }
            i += 1;
            let mut code = String::new();
            while i < chars.len() && chars[i] != '`' {
                code.push(chars[i]);
                i += 1;
            }
            if i < chars.len() {
                i += 1; // Skip closing `
            }
            spans.push(Span::styled(code, theme.inline_code_style()));
        }
        // Check for bold **text**
        else if i + 1 < chars.len() && chars[i] == '*' && chars[i + 1] == '*' {
            if !current.is_empty() {
                spans.push(Span::raw(current.clone()));
                current.clear();
            }
            i += 2;
            let mut bold_text = String::new();
            while i + 1 < chars.len() && !(chars[i] == '*' && chars[i + 1] == '*') {
                bold_text.push(chars[i]);
                i += 1;
            }
            if i + 1 < chars.len() {
                i += 2; // Skip closing **
            }
            spans.push(Span::styled(bold_text, theme.bold_style()));
        }
        // Check for italic *text*
        else if chars[i] == '*' {
            if !current.is_empty() {
                spans.push(Span::raw(current.clone()));
                current.clear();
            }
            i += 1;
            let mut italic_text = String::new();
            while i < chars.len() && chars[i] != '*' {
                italic_text.push(chars[i]);
                i += 1;
            }
            if i < chars.len() {
                i += 1; // Skip closing *
            }
            spans.push(Span::styled(italic_text, theme.italic_style()));
        } else {
            current.push(chars[i]);
            i += 1;
        }
    }

    if !current.is_empty() {
        spans.push(Span::styled(current, theme.text_style()));
    }

    if spans.is_empty() {
        spans.push(Span::styled(text.to_string(), theme.text_style()));
    }

    spans
}

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

    #[test]
    fn callout_marker_basic_kinds() {
        let m = parse_callout_marker("[!NOTE]").unwrap();
        assert_eq!(m.kind, "NOTE");
        assert_eq!(m.title, "Note");

        let m = parse_callout_marker("[!warning]").unwrap();
        assert_eq!(m.kind, "WARNING");
        assert_eq!(m.title, "Warning");
    }

    #[test]
    fn callout_marker_custom_title_and_fold() {
        let m = parse_callout_marker("[!tip] Read this first").unwrap();
        assert_eq!(m.kind, "TIP");
        assert_eq!(m.title, "Read this first");

        let m = parse_callout_marker("[!note]- Folded title").unwrap();
        assert_eq!(m.kind, "NOTE");
        assert_eq!(m.title, "Folded title");
    }

    #[test]
    fn callout_marker_rejects_non_markers() {
        assert!(parse_callout_marker("plain quote").is_none());
        assert!(parse_callout_marker("[!]").is_none());
        assert!(parse_callout_marker("[! has space]").is_none());
        assert!(parse_callout_marker("[note]").is_none());
    }

    #[test]
    fn callout_render_produces_header_and_body() {
        let theme = Theme::ocean_dark();
        let lines = render_callout_lines("[!NOTE]\nbody text", &theme).unwrap();
        assert_eq!(lines.len(), 2);
        let header: String = lines[0].spans.iter().map(|s| s.content.as_ref()).collect();
        assert!(header.contains("Note"));
        let body: String = lines[1].spans.iter().map(|s| s.content.as_ref()).collect();
        assert!(body.contains("body text"));
    }

    #[test]
    fn non_callout_blockquote_is_untouched() {
        let theme = Theme::ocean_dark();
        assert!(render_callout_lines("just a quote", &theme).is_none());
    }
}
