mod comments;
mod images;
mod navigation;
mod normal_mode;
mod rendering;
mod search;
mod selection;
mod text_selection;
mod types;

pub use normal_mode::{PendingCharMotion, PendingYank, VisualMode};
pub use types::*;

use crate::comments::{BookComments, Comment};
use crate::images::background_image_loader::BackgroundImageLoader;
use crate::markdown::Document;
use crate::markdown_text_reader::text_selection::TextSelection;
use crate::ratatui_image::{Resize, StatefulImage, ViewportOptions, picker::Picker};
use crate::search::{SearchMode, SearchState};
use crate::terminal_overlay;
use crate::theme::{Base16Palette, theme_background};
use crate::types::LinkInfo;
use crate::widget::hud_message::{HudMessage, HudMode};
use image::{DynamicImage, GenericImageView};
use log::{info, warn};
use normal_mode::{CursorPosition, NormalModeState};
use ratatui::{
    Frame,
    layout::Rect,
    style::{Modifier, Style as RatatuiStyle},
    symbols::line,
    text::{Line, Span},
    widgets::{Block, Borders, Paragraph},
};
use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};

const HUD_NORMAL_DURATION: Duration = Duration::from_secs(2);
const HUD_ERROR_DURATION: Duration = Duration::from_secs(5);

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum CommentTextareaPlacement {
    Below,
    Above,
    Overlay,
}

#[derive(Clone, Copy, Debug)]
struct CommentTextareaLayout {
    insert_position: Option<usize>,
    lines_to_insert: usize,
    draw_start_line: usize,
}

pub struct MarkdownTextReader {
    markdown_document: Option<Arc<Document>>,
    rendered_content: RenderedContent,

    // Scrolling state
    scroll_offset: usize,
    last_scroll_time: Instant,
    scroll_speed: usize,

    // Visual highlighting
    highlight_visual_line: Option<usize>,
    highlight_end_time: Instant,

    // Content dimensions
    total_wrapped_lines: usize,
    visible_height: usize,

    // Caching
    cache_generation: u64,
    last_width: usize,
    last_focus_state: bool,

    // Text selection
    text_selection: TextSelection,
    raw_text_lines: Vec<String>, // Still needed for clipboard
    last_copied_text: Option<String>,
    last_content_area: Option<Rect>,

    last_inner_text_area: Option<Rect>, // Track the actual text rendering area
    auto_scroll_active: bool,
    auto_scroll_speed: f32,

    // Image handling
    image_picker: Option<Picker>,
    embedded_images: RefCell<HashMap<String, EmbeddedImage>>,
    last_rendered_image_rects: HashMap<String, Rect>,
    last_overlay_cleanup_key: Option<(usize, u64, u64, Rect)>,
    background_loader: BackgroundImageLoader,

    // Deferred node index to restore after rendering
    pending_node_restore: Option<usize>,

    // Raw HTML mode
    show_raw_html: bool,
    raw_html_content: Option<String>,
    raw_html_wrapped_lines: Vec<String>,
    raw_html_last_width: usize,

    // Links extracted from AST
    links: Vec<LinkInfo>,

    // Tables extracted from AST
    embedded_tables: RefCell<Vec<EmbeddedTable>>,

    /// Map of anchor IDs to their line positions in rendered content
    anchor_positions: HashMap<String, usize>,

    /// Current chapter filename (for resolving relative links)
    current_chapter_file: Option<String>,

    /// Search state for vim-like search
    search_state: SearchState,

    /// Saved normal mode cursor position when search started (for restore on cancel)
    original_cursor_for_search: Option<CursorPosition>,

    /// Pending anchor scroll after chapter navigation
    pending_anchor_scroll: Option<String>,

    /// Last active anchor for maintaining continuous highlighting
    last_active_anchor: Option<String>,

    /// Pending local search activation after a global search jump
    pending_global_search: Option<(String, usize)>,

    /// Book comments to display alongside paragraphs
    book_comments: Option<Arc<Mutex<BookComments>>>,
    current_chapter_comments: HashMap<usize, Vec<Comment>>,

    /// Comment input state
    comment_input: CommentInputState,

    chapter_title: Option<String>,

    /// Content margin level (0-20), each level adds 2 columns on each side
    content_margin: u16,

    /// Vim normal mode state
    normal_mode: NormalModeState,

    /// Transient HUD message for the bottom title area
    hud_message: Option<HudMessage>,
}

impl Default for MarkdownTextReader {
    fn default() -> Self {
        Self::new()
    }
}

impl MarkdownTextReader {
    pub fn new() -> Self {
        let image_picker = match Picker::from_query_stdio() {
            Ok(mut picker) => {
                use crate::ratatui_image::picker::{Capability, ProtocolType};
                let has_kitty = picker
                    .capabilities()
                    .iter()
                    .any(|c| matches!(c, Capability::Kitty));
                let has_sixel = picker
                    .capabilities()
                    .iter()
                    .any(|c| matches!(c, Capability::Sixel));

                // Terminal + protocol policy is centralized in terminal.rs.
                let caps = crate::terminal::detect_terminal_with_picker(&mut picker);
                let chosen_protocol = crate::terminal::choose_epub_protocol(&picker, &caps);

                picker.set_protocol_type(chosen_protocol);
                info!(
                    "Startup render caps: terminal={:?}, tmux={}, truecolor={}, graphics={}, \
                     pdf_protocol={:?}, pdf_supported={}, pdf_scroll_mode={}, pdf_comments={}, \
                     epub_picker_caps(kitty={}, sixel={}), epub_protocol={:?}",
                    caps.kind,
                    caps.env.tmux,
                    caps.supports_true_color,
                    caps.supports_graphics,
                    caps.protocol,
                    caps.pdf.supported,
                    caps.pdf.supports_scroll_mode,
                    caps.pdf.supports_comments,
                    has_kitty,
                    has_sixel,
                    picker.protocol_type(),
                );

                // Check if protocol requires true color but terminal doesn't support it
                let requires_true_color =
                    matches!(picker.protocol_type(), ProtocolType::Halfblocks);

                if requires_true_color && !crate::color_mode::supports_true_color() {
                    warn!(
                        "Image protocol {:?} requires true color, but terminal doesn't support it. Disabling image rendering.",
                        picker.protocol_type()
                    );
                    None
                } else {
                    picker.set_background_color([0, 0, 0, 0]);
                    Some(picker)
                }
            }
            Err(e) => {
                warn!(
                    "Failed to create image picker: {e}. The terminal would not support image rendering!"
                );
                None
            }
        };

        Self {
            markdown_document: None,
            rendered_content: RenderedContent {
                lines: Vec::new(),
                total_height: 0,
                generation: 0,
            },
            scroll_offset: 0,
            last_scroll_time: Instant::now(),
            scroll_speed: 1,
            highlight_visual_line: None,
            highlight_end_time: Instant::now(),
            total_wrapped_lines: 0,
            visible_height: 0,
            cache_generation: 0,
            last_width: 0,
            last_focus_state: false,
            text_selection: TextSelection::new(),
            raw_text_lines: Vec::new(),
            last_copied_text: None,
            last_content_area: None,
            last_inner_text_area: None,
            auto_scroll_active: false,
            auto_scroll_speed: 1.0,
            image_picker,
            embedded_images: RefCell::new(HashMap::new()),
            last_rendered_image_rects: HashMap::new(),
            last_overlay_cleanup_key: None,
            background_loader: BackgroundImageLoader::new(),
            pending_node_restore: None,
            raw_html_content: None,
            show_raw_html: false,
            raw_html_wrapped_lines: Vec::new(),
            raw_html_last_width: 0,
            links: Vec::new(),
            embedded_tables: RefCell::new(Vec::new()),
            anchor_positions: HashMap::new(),
            current_chapter_file: None,
            search_state: SearchState::new(),
            original_cursor_for_search: None,
            pending_anchor_scroll: None,
            last_active_anchor: None,
            pending_global_search: None,
            book_comments: None,
            current_chapter_comments: HashMap::new(),
            comment_input: CommentInputState::default(),
            chapter_title: None,
            content_margin: 0,
            normal_mode: NormalModeState::new(),
            hud_message: None,
        }
    }

    fn calculate_progress(&self, _content: &str, _width: usize, _height: usize) -> u32 {
        if self.total_wrapped_lines == 0 {
            return 0;
        }

        let visible_end = (self.scroll_offset + self.visible_height).min(self.total_wrapped_lines);
        ((visible_end as f32 / self.total_wrapped_lines as f32) * 100.0) as u32
    }

    pub fn get_comments(&self) -> Arc<Mutex<BookComments>> {
        self.book_comments.clone().unwrap_or_else(|| {
            Arc::new(Mutex::new(
                BookComments::new(std::path::Path::new(""), None).unwrap(),
            ))
        })
    }

    pub fn set_hud_message(
        &mut self,
        message: impl Into<String>,
        mode: HudMode,
        duration: Duration,
    ) {
        self.hud_message = Some(HudMessage::new(message, duration, mode));
    }

    pub fn set_normal_hud(&mut self, message: impl Into<String>) {
        self.set_hud_message(message, HudMode::Normal, HUD_NORMAL_DURATION);
    }

    pub fn set_error_hud(&mut self, message: impl Into<String>) {
        self.set_hud_message(message, HudMode::Error, HUD_ERROR_DURATION);
    }

    pub fn update_hud_message(&mut self) -> bool {
        if self
            .hud_message
            .as_ref()
            .is_some_and(|hud| hud.is_expired())
        {
            self.hud_message = None;
            return true;
        }
        false
    }

    pub fn dismiss_error_hud(&mut self) -> bool {
        if self
            .hud_message
            .as_ref()
            .is_some_and(|hud| hud.mode == HudMode::Error)
        {
            self.hud_message = None;
            return true;
        }
        false
    }
}

impl MarkdownTextReader {
    fn compute_comment_textarea_layout(
        &self,
        viewport_start: usize,
        viewport_end: usize,
    ) -> Option<CommentTextareaLayout> {
        if !self.comment_input.is_active() || viewport_start >= viewport_end {
            return None;
        }

        let textarea = self.comment_input.textarea.as_ref()?;
        let mut anchor_start = self
            .comment_input
            .target_start_line
            .or(self.comment_input.target_line)?;
        let mut anchor_end = self
            .comment_input
            .target_end_line
            .or(self.comment_input.target_line)?;

        if anchor_start > anchor_end {
            std::mem::swap(&mut anchor_start, &mut anchor_end);
        }

        if self.rendered_content.lines.is_empty() {
            return None;
        }

        let max_line = self.rendered_content.lines.len().saturating_sub(1);
        anchor_start = anchor_start.min(max_line);
        anchor_end = anchor_end.min(max_line);

        if anchor_end < viewport_start || anchor_start >= viewport_end {
            return None;
        }

        let anchor_visible_start = anchor_start.max(viewport_start);
        let anchor_visible_end = anchor_end.min(viewport_end.saturating_sub(1));

        let content_lines = textarea.lines().len().max(3);
        let desired_height = content_lines + 2;
        // One empty line before and one after textarea.
        let reserve_lines = desired_height + 2;

        let space_above = anchor_visible_start.saturating_sub(viewport_start);
        let space_below = viewport_end.saturating_sub(anchor_visible_end.saturating_add(1));
        let can_fit_below = space_below >= reserve_lines;
        let can_fit_above = space_above >= reserve_lines;

        let placement = if can_fit_below {
            CommentTextareaPlacement::Below
        } else if can_fit_above {
            CommentTextareaPlacement::Above
        } else {
            CommentTextareaPlacement::Overlay
        };

        match placement {
            CommentTextareaPlacement::Below => {
                let insert_position = anchor_visible_end.saturating_add(1);
                Some(CommentTextareaLayout {
                    insert_position: Some(insert_position),
                    lines_to_insert: reserve_lines,
                    draw_start_line: insert_position.saturating_add(1),
                })
            }
            CommentTextareaPlacement::Above => {
                let insert_position = anchor_visible_start.saturating_sub(reserve_lines);
                Some(CommentTextareaLayout {
                    insert_position: Some(insert_position),
                    lines_to_insert: reserve_lines,
                    draw_start_line: insert_position.saturating_add(1),
                })
            }
            CommentTextareaPlacement::Overlay => {
                let prefer_above = space_above > space_below;
                let max_start = viewport_end.saturating_sub(desired_height);
                let preferred_start = if prefer_above {
                    anchor_visible_start.saturating_sub(desired_height.saturating_sub(1))
                } else {
                    anchor_visible_end.saturating_add(1)
                };
                let draw_start_line = if max_start < viewport_start {
                    viewport_start
                } else {
                    preferred_start.clamp(viewport_start, max_start)
                };
                Some(CommentTextareaLayout {
                    insert_position: None,
                    lines_to_insert: 0,
                    draw_start_line,
                })
            }
        }
    }

    #[allow(clippy::too_many_arguments)]
    pub fn render(
        &mut self,
        frame: &mut Frame,
        area: Rect,
        current_chapter: usize,
        total_chapters: usize,
        palette: &Base16Palette,
        is_focused: bool,
        zen_mode: bool,
        suppress_images: bool,
    ) {
        // Store content area for hit-testing and mouse interactions
        self.last_content_area = Some(area);
        // Trim borders plus footer space to get actual viewport height
        self.visible_height = area.height.saturating_sub(3) as usize;

        if self.show_raw_html {
            self.render_raw_html(
                frame,
                area,
                current_chapter,
                total_chapters,
                palette,
                is_focused,
            );
            return;
        }

        // Account for borders, side padding, and content margin
        let margin_width = (self.content_margin * 2) as usize;
        let width = (area.width.saturating_sub(4) as usize)
            .saturating_sub(margin_width * 2)
            .max(1);

        // Re-render when dimensions, focus, or cached content change
        if self.last_width != width
            || self.last_focus_state != is_focused
            || self.rendered_content.generation != self.cache_generation
        {
            if let Some(doc) = self.markdown_document.clone() {
                self.rendered_content =
                    self.render_document_to_lines(doc.as_ref(), width, palette, is_focused);
                self.total_wrapped_lines = self.rendered_content.total_height;
                self.last_width = width;
                self.last_focus_state = is_focused;

                if let Some(node_index) = self.pending_node_restore.take() {
                    self.perform_node_restore(node_index);
                }

                if let Some(anchor_id) = self.pending_anchor_scroll.take() {
                    if let Some(target_line) = self.get_anchor_position(&anchor_id) {
                        self.scroll_to_line(target_line);
                        self.highlight_line_temporarily(target_line, Duration::from_secs(2));
                    } else {
                        warn!("Pending anchor '{anchor_id}' not found after re-render");
                    }
                }

                if let Some((query, node_index)) = self.pending_global_search.take() {
                    self.activate_local_search_from_global(query, node_index);
                }
            }
        }
        let title_text = if let Some(ref title) = self.chapter_title {
            format!("[{current_chapter}/{total_chapters}] {title}")
        } else {
            format!("Chapter {current_chapter}/{total_chapters}")
        };

        let progress = self.calculate_progress("", width, self.visible_height);

        let (_text_color, border_color, _bg_color) = palette.get_panel_colors(is_focused);
        let mode_title = if self.comment_input.is_active() {
            let border_style = RatatuiStyle::default().fg(border_color);
            let mode_style = RatatuiStyle::default()
                .fg(palette.base_07)
                .bg(palette.base_03)
                .add_modifier(Modifier::BOLD);
            Some(
                Line::from(vec![
                    Span::styled(line::HORIZONTAL, border_style),
                    Span::styled(" COMMENT ", mode_style),
                ])
                .left_aligned(),
            )
        } else if self.is_visual_mode_active() {
            let border_style = RatatuiStyle::default().fg(border_color);
            let mode_style = RatatuiStyle::default()
                .fg(palette.base_07)
                .bg(palette.base_0e)
                .add_modifier(Modifier::BOLD);
            Some(
                Line::from(vec![
                    Span::styled(line::HORIZONTAL, border_style),
                    Span::styled(" VISUAL ", mode_style),
                ])
                .left_aligned(),
            )
        } else if self.normal_mode.is_active() {
            let border_style = RatatuiStyle::default().fg(border_color);
            let mode_style = RatatuiStyle::default()
                .fg(palette.base_07)
                .bg(palette.base_0d)
                .add_modifier(Modifier::BOLD);
            Some(
                Line::from(vec![
                    Span::styled(line::HORIZONTAL, border_style),
                    Span::styled(" NORMAL ", mode_style),
                ])
                .left_aligned(),
            )
        } else {
            None
        };
        let progress_title = Line::from(format!(" {progress}% ")).right_aligned();

        if self
            .hud_message
            .as_ref()
            .is_some_and(|hud| hud.is_expired())
        {
            self.hud_message = None;
        }

        let mut block = Block::default()
            .borders(Borders::ALL)
            .title(title_text)
            .title_bottom(progress_title)
            .border_style(RatatuiStyle::default().fg(border_color));
        if let Some(mode_title) = mode_title {
            block = block.title_bottom(mode_title);
        }
        if let Some(hud) = self.hud_message.as_ref() {
            block = block.title_bottom(hud.styled_line(palette));
        }

        if zen_mode && self.search_state.active {
            let search_hint = match self.search_state.mode {
                SearchMode::InputMode => {
                    let query = &self.search_state.query;
                    let match_info = if self.search_state.matches.is_empty() && !query.is_empty() {
                        " No matches".to_string()
                    } else if !self.search_state.matches.is_empty() {
                        format!(" {} matches", self.search_state.matches.len())
                    } else {
                        String::new()
                    };
                    format!(" / {query}█ {match_info}  ESC: Cancel | Enter: Search ")
                }
                SearchMode::NavigationMode => {
                    let query = &self.search_state.query;
                    let match_info = self.search_state.get_match_info();
                    format!(" /{query}  {match_info}  n/N: Navigate | ESC: Exit ")
                }
                SearchMode::Inactive => String::new(),
            };
            if !search_hint.is_empty() {
                block = block.title_bottom(Line::from(search_hint).left_aligned());
            }
        }

        // Remove borders so the text sits inside the frame cleanly
        let mut inner_area = block.inner(area);
        inner_area.y = inner_area.y.saturating_add(1);
        inner_area.height = inner_area.height.saturating_sub(1);
        inner_area.x = inner_area.x.saturating_add(1);

        // Apply content margin
        let margin_pixels = self.content_margin * 2;
        inner_area.x = inner_area.x.saturating_add(margin_pixels);
        inner_area.width = inner_area.width.saturating_sub(margin_pixels * 2);

        // Remember the focused text area for mouse hover/selection logic
        self.last_inner_text_area = Some(inner_area);

        let image_clear_style = RatatuiStyle::default().bg(theme_background());
        let overlay_images_need_clear = self.image_picker.as_ref().is_some_and(|picker| {
            matches!(
                picker.protocol_type(),
                crate::ratatui_image::picker::ProtocolType::Iterm2
                    | crate::ratatui_image::picker::ProtocolType::Sixel
            )
        });
        if overlay_images_need_clear {
            let cleanup_key = (
                self.scroll_offset,
                self.cache_generation,
                self.rendered_content.generation,
                inner_area,
            );
            let content_moved = self.last_overlay_cleanup_key != Some(cleanup_key);
            if terminal_overlay::kitty_delete_overlay_hack_enabled() && content_moved {
                terminal_overlay::emit_kitty_delete_all();
            }
            if terminal_overlay::overlay_force_clear_enabled() {
                terminal_overlay::clear_rects_direct(
                    self.last_rendered_image_rects.values().copied(),
                );
            }
            for rect in self.last_rendered_image_rects.values() {
                frame.render_widget(Block::default().style(image_clear_style), *rect);
            }
            self.last_overlay_cleanup_key = Some(cleanup_key);
        }

        // First pass: render text lines (no images yet)
        let mut visible_lines = Vec::new();
        let end_offset =
            (self.scroll_offset + self.visible_height).min(self.rendered_content.lines.len());

        // Selection background depends on focus state
        let selection_bg = if is_focused {
            palette.base_02
        } else {
            palette.base_01
        };

        // Reserve empty lines where the comment textarea will be drawn.
        let textarea_layout = self.compute_comment_textarea_layout(self.scroll_offset, end_offset);
        let textarea_lines_to_insert = textarea_layout
            .as_ref()
            .map(|layout| layout.lines_to_insert)
            .unwrap_or(0);
        let textarea_insert_position = textarea_layout
            .as_ref()
            .and_then(|layout| layout.insert_position);

        for line_idx in self.scroll_offset..end_offset {
            if let Some(insert_pos) = textarea_insert_position {
                if line_idx == insert_pos {
                    for _ in 0..textarea_lines_to_insert {
                        visible_lines.push(Line::from(""));
                    }
                }
            }

            if let Some(rendered_line) = self.rendered_content.lines.get(line_idx) {
                let visual_line_idx = line_idx - self.scroll_offset;

                let skip_placeholder =
                    if let LineType::ImagePlaceholder { src } = &rendered_line.line_type {
                        if let Some(embedded_image) = self.embedded_images.borrow().get(src) {
                            matches!(embedded_image.state, ImageLoadState::Loaded { .. })
                        } else {
                            false
                        }
                    } else {
                        false
                    };

                if skip_placeholder {
                    visible_lines.push(Line::from(""));
                    continue;
                }

                let mut line_spans = if self.highlight_visual_line == Some(visual_line_idx) {
                    rendered_line
                        .spans
                        .iter()
                        .map(|span| {
                            Span::styled(span.content.clone(), span.style.bg(palette.base_02))
                        })
                        .collect()
                } else {
                    rendered_line.spans.clone()
                };

                if self.text_selection.has_selection() {
                    let line_with_selection = self.text_selection.apply_selection_highlighting(
                        line_idx,
                        line_spans,
                        selection_bg,
                    );
                    line_spans = line_with_selection.spans;
                }

                line_spans = self.apply_search_highlighting(line_idx, line_spans, palette);

                // Apply yank highlight (before cursor so cursor shows on top)
                line_spans = self.apply_yank_highlight(line_idx, line_spans, palette);

                // Apply visual mode selection highlight
                line_spans = self.apply_visual_highlight(line_idx, line_spans, palette);

                if self.normal_mode.is_active() {
                    line_spans = self.apply_normal_mode_cursor(line_idx, line_spans, palette);
                }

                visible_lines.push(Line::from(line_spans));
            }
        }

        let paragraph = Paragraph::new(vec![])
            .block(block.clone())
            .wrap(ratatui::widgets::Wrap { trim: false });

        frame.render_widget(paragraph, area);

        let inner_text_paragraph =
            Paragraph::new(visible_lines).block(Block::default().borders(Borders::NONE));

        frame.render_widget(inner_text_paragraph, inner_area);

        // Second pass: draw inline images over the text block
        let scroll_offset = self.scroll_offset;

        let mut current_image_rects = HashMap::new();

        // Clear overlay images when suppressing (e.g., popup is shown)
        if suppress_images {
            if terminal_overlay::kitty_delete_overlay_hack_enabled() {
                terminal_overlay::emit_kitty_delete_all();
            }
            if terminal_overlay::overlay_force_clear_enabled() {
                terminal_overlay::clear_rects_direct(
                    self.last_rendered_image_rects.values().copied(),
                );
            }
        }

        if !self.show_raw_html && !suppress_images {
            self.check_for_loaded_images();
            if !self.embedded_images.borrow().is_empty() && self.image_picker.is_some() {
                let area_height = inner_area.height as usize;

                for (src, embedded_image) in self.embedded_images.borrow_mut().iter_mut() {
                    let image_height_cells = embedded_image.height_cells as usize;
                    let mut image_start_line = embedded_image.lines_before_image;
                    let mut image_end_line = image_start_line + image_height_cells;

                    if let Some(insert_pos) = textarea_insert_position {
                        if image_start_line >= insert_pos {
                            image_start_line += textarea_lines_to_insert;
                            image_end_line += textarea_lines_to_insert;
                        }
                    }

                    if scroll_offset < image_end_line
                        && scroll_offset + area_height > image_start_line
                    {
                        if let ImageLoadState::Loaded {
                            ref image,
                            ref mut protocol,
                        } = embedded_image.state
                        {
                            let scaled_image = image;

                            if let Some(ref picker) = self.image_picker {
                                let image_screen_start =
                                    image_start_line.saturating_sub(scroll_offset);

                                // Clip the top portion if the image starts above the viewport
                                let image_top_clipped =
                                    scroll_offset.saturating_sub(image_start_line);

                                let visible_image_height = (image_height_cells - image_top_clipped)
                                    .min(area_height - image_screen_start);

                                if visible_image_height > 0 {
                                    let image_height_cells =
                                        calculate_image_height_in_cells(scaled_image);

                                    let (render_y, render_height) = if image_top_clipped > 0 {
                                        (
                                            inner_area.y,
                                            ((image_height_cells as usize)
                                                .saturating_sub(image_top_clipped))
                                            .min(area_height)
                                                as u16,
                                        )
                                    } else {
                                        (
                                            inner_area.y + image_screen_start as u16,
                                            (image_height_cells as usize)
                                                .min(area_height.saturating_sub(image_screen_start))
                                                as u16,
                                        )
                                    };

                                    // Determine the terminal width required for the pixels
                                    let (image_width_pixels, _image_height_pixels) =
                                        scaled_image.dimensions();
                                    let font_size = picker.font_size();
                                    let image_width_cells =
                                        (image_width_pixels as f32 / font_size.0 as f32).ceil()
                                            as u16;

                                    // Center the image horizontally in the text area
                                    let text_area_width = inner_area.width;
                                    let image_display_width =
                                        image_width_cells.min(text_area_width);
                                    let x_offset =
                                        (text_area_width.saturating_sub(image_display_width)) / 2;

                                    let image_area = Rect {
                                        x: inner_area.x + x_offset,
                                        y: render_y,
                                        width: image_display_width,
                                        height: render_height,
                                    };

                                    // Render using the ratatui_image viewport for scrolling
                                    let current_font_size = picker.font_size();
                                    let y_offset_pixels = (image_top_clipped as f32
                                        * current_font_size.1 as f32)
                                        as u32;

                                    let viewport_options = ViewportOptions {
                                        y_offset: y_offset_pixels,
                                        x_offset: 0, // No horizontal scrolling for now
                                    };

                                    let image_widget = StatefulImage::new()
                                        .resize(Resize::Viewport(viewport_options));

                                    frame.render_stateful_widget(
                                        image_widget,
                                        image_area,
                                        protocol,
                                    );
                                    current_image_rects.insert(src.clone(), image_area);
                                }
                            }
                        }
                    }
                }
            }
        }

        self.last_rendered_image_rects = current_image_rects;

        if let (Some(layout), Some(textarea)) =
            (textarea_layout, self.comment_input.textarea.as_mut())
        {
            let visual_position = layout.draw_start_line.saturating_sub(self.scroll_offset);
            let textarea_y = inner_area.y + visual_position as u16;

            if textarea_y < inner_area.y + inner_area.height {
                // Compute minimum height so borders never collapse.
                let content_lines = textarea.lines().len();
                let min_lines = 3;
                let actual_content_lines = content_lines.max(min_lines);
                let desired_height = (actual_content_lines + 2) as u16;

                // Constrain height to the remaining view.
                let textarea_height =
                    desired_height.min(inner_area.y + inner_area.height - textarea_y);

                // Shift left to align with paragraph text (inner_area.x already has padding).
                let left_adjust = 2;

                let textarea_rect = Rect {
                    x: inner_area.x.saturating_sub(left_adjust),
                    y: textarea_y,
                    width: inner_area.width + left_adjust,
                    height: textarea_height,
                };

                let clear_block =
                    Block::default().style(RatatuiStyle::default().bg(theme_background()));
                frame.render_widget(clear_block, textarea_rect);

                let padded_rect = Rect {
                    x: textarea_rect.x + 2,
                    y: textarea_y,
                    width: textarea_rect.width.saturating_sub(4),
                    height: textarea_height,
                };

                textarea.set_style(
                    RatatuiStyle::default()
                        .fg(palette.base_05)
                        .bg(theme_background()),
                );
                textarea.set_cursor_style(
                    RatatuiStyle::default()
                        .fg(palette.base_00)
                        .bg(palette.base_05),
                );
                textarea.set_cursor_line_style(RatatuiStyle::default());

                let title = match self.comment_input.edit_mode {
                    Some(CommentEditMode::Editing { .. }) => "Edit Comment (Esc to save)",
                    _ => "Add Comment (Esc to save)",
                };
                let block = Block::default().borders(Borders::ALL).title(title).style(
                    RatatuiStyle::default()
                        .fg(palette.base_04)
                        .bg(theme_background()),
                );
                textarea.set_block(block);

                frame.render_widget(&*textarea, padded_rect);
            }
        }
    }

    pub fn render_raw_html(
        &mut self,
        frame: &mut ratatui::Frame,
        area: Rect,
        current_chapter: usize,
        total_chapters: usize,
        palette: &Base16Palette,
        is_focused: bool,
    ) {
        self.last_content_area = Some(area);
        self.visible_height = area.height.saturating_sub(3) as usize;

        let title_text = if let Some(ref title) = self.chapter_title {
            format!("[{current_chapter}/{total_chapters}] {title} [RAW HTML]")
        } else {
            format!("Chapter {current_chapter}/{total_chapters} [RAW HTML]")
        };

        if self
            .hud_message
            .as_ref()
            .is_some_and(|hud| hud.is_expired())
        {
            self.hud_message = None;
        }

        // Calculate inner area and width for wrapping
        let margin_pixels = self.content_margin * 2;
        let width = area
            .width
            .saturating_sub(4)
            .saturating_sub(margin_pixels * 2) as usize;

        // Wrap raw HTML content if needed (width changed or content changed)
        if self.raw_html_last_width != width || self.raw_html_wrapped_lines.is_empty() {
            let raw_content = self
                .raw_html_content
                .as_deref()
                .unwrap_or("Raw HTML content not available");

            self.raw_html_wrapped_lines.clear();
            for line in raw_content.lines() {
                if line.is_empty() {
                    self.raw_html_wrapped_lines.push(String::new());
                } else {
                    for wrapped in textwrap::wrap(line, width) {
                        self.raw_html_wrapped_lines.push(wrapped.to_string());
                    }
                }
            }
            self.raw_html_last_width = width;
            self.total_wrapped_lines = self.raw_html_wrapped_lines.len();

            // Sync raw_text_lines for selection/normal mode operations
            self.raw_text_lines = self.raw_html_wrapped_lines.clone();
        }

        // Calculate progress
        let progress = if self.total_wrapped_lines == 0 {
            0
        } else {
            let visible_end =
                (self.scroll_offset + self.visible_height).min(self.total_wrapped_lines);
            ((visible_end as f32 / self.total_wrapped_lines as f32) * 100.0) as u32
        };

        // Build mode title for normal mode (matches EPUB styling)
        let border_color = palette.base_09; // Orange for raw HTML mode
        let mode_title = if self.is_visual_mode_active() {
            let border_style = RatatuiStyle::default().fg(border_color);
            let mode_style = RatatuiStyle::default()
                .fg(palette.base_07)
                .bg(palette.base_0e)
                .add_modifier(Modifier::BOLD);
            Some(
                Line::from(vec![
                    Span::styled(line::HORIZONTAL, border_style),
                    Span::styled(" VISUAL ", mode_style),
                ])
                .left_aligned(),
            )
        } else if self.normal_mode.is_active() {
            let border_style = RatatuiStyle::default().fg(border_color);
            let mode_style = RatatuiStyle::default()
                .fg(palette.base_07)
                .bg(palette.base_0d)
                .add_modifier(Modifier::BOLD);
            Some(
                Line::from(vec![
                    Span::styled(line::HORIZONTAL, border_style),
                    Span::styled(" NORMAL ", mode_style),
                ])
                .left_aligned(),
            )
        } else {
            None
        };

        let progress_title = Line::from(format!(" {progress}% ")).right_aligned();

        let mut block = ratatui::widgets::Block::default()
            .borders(ratatui::widgets::Borders::ALL)
            .title(title_text)
            .title_bottom(progress_title)
            .style(RatatuiStyle::default().fg(palette.base_09)); // Red border for raw mode

        if let Some(mode_title) = mode_title {
            block = block.title_bottom(mode_title);
        }
        if let Some(hud) = self.hud_message.as_ref() {
            block = block.title_bottom(hud.styled_line(palette));
        }

        // Add search hint if active
        if self.search_state.active {
            let search_hint = match self.search_state.mode {
                SearchMode::InputMode => {
                    let query = &self.search_state.query;
                    let match_info = if self.search_state.matches.is_empty() && !query.is_empty() {
                        " No matches".to_string()
                    } else if !self.search_state.matches.is_empty() {
                        format!(" {} matches", self.search_state.matches.len())
                    } else {
                        String::new()
                    };
                    format!(" / {query}█ {match_info}  ESC: Cancel | Enter: Search ")
                }
                SearchMode::NavigationMode => {
                    let query = &self.search_state.query;
                    let match_info = self.search_state.get_match_info();
                    format!(" /{query}  {match_info}  n/N: Navigate | ESC: Exit ")
                }
                SearchMode::Inactive => String::new(),
            };
            if !search_hint.is_empty() {
                block = block.title_bottom(Line::from(search_hint).left_aligned());
            }
        }

        // Calculate inner area for text
        let mut inner_area = block.inner(area);
        inner_area.y = inner_area.y.saturating_add(1);
        inner_area.height = inner_area.height.saturating_sub(1);
        inner_area.x = inner_area.x.saturating_add(1);
        inner_area.x = inner_area.x.saturating_add(margin_pixels);
        inner_area.width = inner_area.width.saturating_sub(margin_pixels * 2);

        self.last_inner_text_area = Some(inner_area);

        // Selection background color
        let selection_bg = if is_focused {
            palette.base_02
        } else {
            palette.base_01
        };

        // Build visible lines with highlighting
        let mut visible_lines = Vec::new();
        let end_offset =
            (self.scroll_offset + self.visible_height).min(self.raw_html_wrapped_lines.len());

        for line_idx in self.scroll_offset..end_offset {
            if let Some(line_text) = self.raw_html_wrapped_lines.get(line_idx) {
                let mut line_spans = vec![Span::styled(
                    line_text.clone(),
                    RatatuiStyle::default().fg(palette.base_05),
                )];

                // Apply text selection highlighting
                if self.text_selection.has_selection() {
                    let line_with_selection = self.text_selection.apply_selection_highlighting(
                        line_idx,
                        line_spans,
                        selection_bg,
                    );
                    line_spans = line_with_selection.spans;
                }

                // Apply search highlighting
                line_spans = self.apply_search_highlighting(line_idx, line_spans, palette);

                // Apply yank highlight
                line_spans = self.apply_yank_highlight(line_idx, line_spans, palette);

                // Apply visual mode selection
                line_spans = self.apply_visual_highlight(line_idx, line_spans, palette);

                // Apply normal mode cursor
                if self.normal_mode.is_active() {
                    line_spans = self.apply_normal_mode_cursor(line_idx, line_spans, palette);
                }

                visible_lines.push(Line::from(line_spans));
            }
        }

        // Render the block frame
        let paragraph = Paragraph::new(vec![])
            .block(block.clone())
            .wrap(ratatui::widgets::Wrap { trim: false });
        frame.render_widget(paragraph, area);

        // Render the text content
        let inner_text_paragraph =
            Paragraph::new(visible_lines).block(Block::default().borders(Borders::NONE));
        frame.render_widget(inner_text_paragraph, inner_area);
    }

    pub fn set_content_from_string(
        &mut self,
        content_raw_html: &str,
        chapter_title: Option<String>,
    ) {
        self.clear_content();

        use crate::parsing::html_to_markdown::HtmlToMarkdownConverter;
        let mut converter = HtmlToMarkdownConverter::new();
        let doc = Arc::new(converter.convert(content_raw_html));

        self.markdown_document = Some(doc);
        self.chapter_title = chapter_title;

        // Mark cached render as stale so next draw rebuilds it
        self.cache_generation += 1;
    }

    pub fn clear_content(&mut self) {
        self.scroll_offset = 0;
        self.text_selection.clear_selection();

        // IMPORTANT: Clear the markdown document so new content can be parsed
        self.markdown_document = None;

        self.cache_generation += 1;

        self.links.clear();
        self.embedded_tables.borrow_mut().clear();
        self.raw_text_lines.clear();
        self.rendered_content = RenderedContent {
            lines: Vec::new(),
            total_height: 0,
            generation: 0,
        };
        self.embedded_images.borrow_mut().clear();
        self.last_rendered_image_rects.clear();
        self.last_overlay_cleanup_key = None;
    }

    pub fn set_raw_html(&mut self, html: String) {
        self.raw_html_content = Some(html);
        self.raw_html_wrapped_lines.clear();
        self.raw_html_last_width = 0;
    }

    pub fn toggle_raw_html(&mut self) {
        self.show_raw_html = !self.show_raw_html;
        self.scroll_offset = 0;
        self.text_selection.clear_selection();
        if self.normal_mode.is_active() {
            self.normal_mode.deactivate();
        }
    }

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

    pub fn handle_terminal_resize(&mut self) {
        self.cache_generation += 1;
    }

    pub fn increase_margin(&mut self) {
        self.content_margin = self.content_margin.saturating_add(1).min(20);
        self.cache_generation += 1;
    }

    pub fn decrease_margin(&mut self) {
        self.content_margin = self.content_margin.saturating_sub(1);
        self.cache_generation += 1;
    }

    pub fn set_margin(&mut self, margin: u16) {
        self.content_margin = margin.min(20);
        self.cache_generation += 1;
    }

    pub fn get_margin(&self) -> u16 {
        self.content_margin
    }

    pub fn invalidate_render_cache(&mut self) {
        self.cache_generation += 1;
    }

    pub fn request_overlay_cleanup_on_next_frame(&mut self) {
        self.last_overlay_cleanup_key = None;
    }
}

fn calculate_image_height_in_cells(image: &DynamicImage) -> u16 {
    let (width, height) = image.dimensions();
    EmbeddedImage::height_in_cells(width, height)
}
