use super::{PLACEHOLDER_COLOR, Session, measure_text_width, ratatui_style_from_inline};
use crate::config::constants::ui;
use crate::ui::tui::types::InlineTextStyle;
use anstyle::{Color as AnsiColorEnum, Effects};
use ratatui::{
    buffer::Buffer,
    prelude::*,
    widgets::{Block, Padding, Paragraph, Wrap},
};
use regex::Regex;
use std::fmt::Write;
use std::path::Path;
use std::sync::LazyLock;
use tui_shimmer::shimmer_spans_with_style_at_phase;
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use vtcode_commons::fs::is_image_path;

use super::utils::line_truncation::truncate_line_with_ellipsis_if_overflow;

struct InputRender {
    text: Text<'static>,
    cursor_x: u16,
    cursor_y: u16,
}

#[derive(Default)]
struct InputLineBuffer {
    prefix: String,
    text: String,
    prefix_width: u16,
    text_width: u16,
    /// Character index in the original input where this buffer's text starts.
    char_start: usize,
}

impl InputLineBuffer {
    fn new(prefix: String, prefix_width: u16, char_start: usize) -> Self {
        Self {
            prefix,
            text: String::new(),
            prefix_width,
            text_width: 0,
            char_start,
        }
    }
}

/// Token type for syntax highlighting in the input field.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum InputTokenKind {
    Normal,
    SlashCommand,
    AgentReference,
    FileReference,
    InlineCode,
}

/// A contiguous range of characters sharing the same token kind.
struct InputToken {
    kind: InputTokenKind,
    /// Start char index (inclusive).
    start: usize,
    /// End char index (exclusive).
    end: usize,
}

/// Tokenize input text into styled regions for syntax highlighting.
fn tokenize_input(content: &str) -> Vec<InputToken> {
    let chars: Vec<char> = content.chars().collect();
    let len = chars.len();
    if len == 0 {
        return Vec::new();
    }

    // Assign a token kind to each character position.
    let mut kinds = vec![InputTokenKind::Normal; len];

    // 1. Slash commands: `/word` at the start or after whitespace.
    //    Mark the leading `/` and subsequent non-whitespace chars.
    {
        let mut i = 0;
        while i < len {
            if chars[i] == '/'
                && (i == 0 || chars[i - 1].is_whitespace())
                && i + 1 < len
                && chars[i + 1].is_alphanumeric()
            {
                let start = i;
                i += 1;
                while i < len && !chars[i].is_whitespace() {
                    i += 1;
                }
                for kind in &mut kinds[start..i] {
                    *kind = InputTokenKind::SlashCommand;
                }
                continue;
            }
            i += 1;
        }
    }

    // 2. @agent references: canonical `@agent-...` mentions.
    {
        let mut i = 0;
        while i < len {
            if chars[i] == '@'
                && (i == 0 || chars[i - 1].is_whitespace())
                && chars[i..].starts_with(&['@', 'a', 'g', 'e', 'n', 't', '-'])
            {
                let start = i;
                i += 1;
                while i < len && !chars[i].is_whitespace() {
                    i += 1;
                }
                for kind in &mut kinds[start..i] {
                    *kind = InputTokenKind::AgentReference;
                }
                continue;
            }
            i += 1;
        }
    }

    // 3. @file references: `@` at word boundary followed by non-whitespace.
    {
        let mut i = 0;
        while i < len {
            if chars[i] == '@'
                && (i == 0 || chars[i - 1].is_whitespace())
                && i + 1 < len
                && !chars[i + 1].is_whitespace()
            {
                let start = i;
                i += 1;
                while i < len && !chars[i].is_whitespace() {
                    i += 1;
                }
                if kinds[start..i]
                    .iter()
                    .all(|kind| *kind == InputTokenKind::Normal)
                {
                    for kind in &mut kinds[start..i] {
                        *kind = InputTokenKind::FileReference;
                    }
                }
                continue;
            }
            i += 1;
        }
    }

    // 4. Inline code: backtick-delimited spans (single or triple).
    {
        let mut i = 0;
        while i < len {
            if chars[i] == '`' {
                let tick_start = i;
                let mut tick_len = 0;
                while i < len && chars[i] == '`' {
                    tick_len += 1;
                    i += 1;
                }
                // Find matching closing backticks.
                let mut found = false;
                let content_start = i;
                while i <= len.saturating_sub(tick_len) {
                    if chars[i] == '`' {
                        let mut close_len = 0;
                        while i < len && chars[i] == '`' {
                            close_len += 1;
                            i += 1;
                        }
                        if close_len == tick_len {
                            for kind in &mut kinds[tick_start..i] {
                                *kind = InputTokenKind::InlineCode;
                            }
                            found = true;
                            break;
                        }
                    } else {
                        i += 1;
                    }
                }
                if !found {
                    i = content_start;
                }
                continue;
            }
            i += 1;
        }
    }

    // Coalesce adjacent chars with the same kind into tokens.
    let mut tokens = Vec::new();
    let mut cur_kind = kinds[0];
    let mut cur_start = 0;
    for (i, kind) in kinds.iter().enumerate().skip(1) {
        if *kind != cur_kind {
            tokens.push(InputToken {
                kind: cur_kind,
                start: cur_start,
                end: i,
            });
            cur_kind = *kind;
            cur_start = i;
        }
    }
    tokens.push(InputToken {
        kind: cur_kind,
        start: cur_start,
        end: len,
    });
    tokens
}

struct InputLayout {
    buffers: Vec<InputLineBuffer>,
    cursor_line_idx: usize,
    cursor_column: u16,
}

const SHELL_MODE_BORDER_TITLE: &str = " ! Shell mode ";
const SHELL_MODE_STATUS_HINT: &str = "Shell mode (!): direct command execution";

impl Session {
    pub(crate) fn render_input(&mut self, frame: &mut Frame<'_>, area: Rect) {
        if area.height == 0 {
            self.set_input_area(None);
            return;
        }

        let mut input_area = area;
        let mut status_area = None;
        if area.height > ui::INLINE_INPUT_STATUS_HEIGHT {
            let block_height = area.height.saturating_sub(ui::INLINE_INPUT_STATUS_HEIGHT);
            input_area.height = block_height.max(1);
            status_area = Some(Rect::new(
                area.x,
                area.y + block_height,
                area.width,
                ui::INLINE_INPUT_STATUS_HEIGHT,
            ));
        }

        let background_style = self.styles.input_background_style();
        let shell_mode_title = self.shell_mode_border_title();
        let active_subagent_title = self.active_subagent_input_title();
        let active_subagent_border_style = self.active_subagent_input_border_style();
        let mut block = if shell_mode_title.is_some() || active_subagent_title.is_some() {
            Block::bordered()
        } else {
            Block::new()
        };
        block = block
            .style(background_style)
            .padding(self.input_block_padding());
        if shell_mode_title.is_some() || active_subagent_title.is_some() {
            block = block
                .border_type(super::terminal_capabilities::get_border_type())
                .border_style(
                    active_subagent_border_style
                        .unwrap_or_else(|| self.styles.accent_style().add_modifier(Modifier::BOLD)),
                );
        }
        if let Some(title) = shell_mode_title {
            block = block.title_top(Line::from(title).left_aligned());
        }
        if let Some(title) = active_subagent_title {
            block = block.title_top(title);
        }
        let inner = block.inner(input_area);
        self.set_input_area(Some(inner));
        let input_render = self.build_input_render(inner.width, inner.height);
        let paragraph = Paragraph::new(input_render.text)
            .style(background_style)
            .wrap(Wrap { trim: false });
        frame.render_widget(paragraph.block(block), input_area);
        self.apply_input_selection_highlight(frame.buffer_mut(), inner);
        if self.input_manager.selection_needs_copy() {
            let _ = self.copy_input_selection_to_clipboard();
        }

        if self.cursor_should_be_visible() && inner.width > 0 && inner.height > 0 {
            let cursor_x = input_render
                .cursor_x
                .min(inner.width.saturating_sub(1))
                .saturating_add(inner.x);
            let cursor_y = input_render
                .cursor_y
                .min(inner.height.saturating_sub(1))
                .saturating_add(inner.y);
            if self.use_fake_cursor() {
                render_fake_cursor(frame.buffer_mut(), cursor_x, cursor_y);
            } else {
                frame.set_cursor_position(Position::new(cursor_x, cursor_y));
            }
        }

        if let Some(status_area) = status_area {
            let status_line = self
                .render_input_status_line(status_area.width)
                .unwrap_or_default();
            let status = Paragraph::new(status_line)
                .style(self.styles.default_style())
                .wrap(Wrap { trim: false });
            frame.render_widget(status, status_area);
        }
    }

    pub(crate) fn desired_input_lines(&self, inner_width: u16) -> u16 {
        if inner_width == 0 {
            return 1;
        }

        if self.input_compact_mode
            && self.input_manager.cursor() == self.input_manager.content().len()
            && self.input_compact_placeholder().is_some()
        {
            return 1;
        }

        if self.input_manager.content().is_empty() {
            return 1;
        }

        let prompt_width = UnicodeWidthStr::width(self.prompt_prefix.as_str()) as u16;
        let prompt_display_width = prompt_width.min(inner_width);
        let layout = self.input_layout(inner_width, prompt_display_width);
        let line_count = layout.buffers.len().max(1);
        let capped = line_count.min(ui::INLINE_INPUT_MAX_LINES.max(1));
        capped as u16
    }

    pub(crate) fn apply_input_height(&mut self, height: u16) {
        let resolved = height.max(Self::input_block_height_for_lines(1));
        if self.input_height != resolved {
            self.input_height = resolved;
            self.recalculate_transcript_rows();
        }
    }

    pub(crate) fn input_block_height_for_lines(lines: u16) -> u16 {
        lines
            .max(1)
            .saturating_add(ui::INLINE_INPUT_PADDING_VERTICAL.saturating_mul(2))
    }

    pub(crate) fn input_block_extra_height(&self) -> u16 {
        if self.active_subagent_input_title().is_some() && !self.input_uses_shell_prefix() {
            2
        } else {
            0
        }
    }

    fn input_layout(&self, width: u16, prompt_display_width: u16) -> InputLayout {
        let indent_prefix = " ".repeat(prompt_display_width as usize);
        let mut buffers = vec![InputLineBuffer::new(
            self.prompt_prefix.clone(),
            prompt_display_width,
            0,
        )];
        let secure_prompt_active = self.secure_prompt_active();
        let mut cursor_line_idx = 0usize;
        let mut cursor_column = prompt_display_width;
        let input_content = self.input_manager.content();
        let cursor_pos = self.input_manager.cursor();
        let mut cursor_set = cursor_pos == 0;
        let mut char_idx: usize = 0;

        for (idx, ch) in input_content.char_indices() {
            if !cursor_set
                && cursor_pos == idx
                && let Some(current) = buffers.last()
            {
                cursor_line_idx = buffers.len() - 1;
                cursor_column = current.prefix_width + current.text_width;
                cursor_set = true;
            }

            if ch == '\n' {
                let end = idx + ch.len_utf8();
                char_idx += 1;
                buffers.push(InputLineBuffer::new(
                    indent_prefix.clone(),
                    prompt_display_width,
                    char_idx,
                ));
                if !cursor_set && cursor_pos == end {
                    cursor_line_idx = buffers.len() - 1;
                    cursor_column = prompt_display_width;
                    cursor_set = true;
                }
                continue;
            }

            let display_ch = if secure_prompt_active { '•' } else { ch };
            let char_width = UnicodeWidthChar::width(display_ch).unwrap_or(0) as u16;

            if let Some(current) = buffers.last_mut() {
                let capacity = width.saturating_sub(current.prefix_width);
                if capacity > 0
                    && current.text_width + char_width > capacity
                    && !current.text.is_empty()
                {
                    buffers.push(InputLineBuffer::new(
                        indent_prefix.clone(),
                        prompt_display_width,
                        char_idx,
                    ));
                }
            }

            if let Some(current) = buffers.last_mut() {
                current.text.push(display_ch);
                current.text_width = current.text_width.saturating_add(char_width);
            }

            char_idx += 1;

            let end = idx + ch.len_utf8();
            if !cursor_set
                && cursor_pos == end
                && let Some(current) = buffers.last()
            {
                cursor_line_idx = buffers.len() - 1;
                cursor_column = current.prefix_width + current.text_width;
                cursor_set = true;
            }
        }

        if !cursor_set && let Some(current) = buffers.last() {
            cursor_line_idx = buffers.len() - 1;
            cursor_column = current.prefix_width + current.text_width;
        }

        InputLayout {
            buffers,
            cursor_line_idx,
            cursor_column,
        }
    }

    fn visible_input_window(&self, width: u16, height: u16) -> (InputLayout, usize, usize) {
        let prompt_width = UnicodeWidthStr::width(self.prompt_prefix.as_str()) as u16;
        let prompt_display_width = prompt_width.min(width);
        let layout = self.input_layout(width, prompt_display_width);
        let total_lines = layout.buffers.len();
        let visible_limit = height.max(1).min(ui::INLINE_INPUT_MAX_LINES as u16) as usize;
        let mut start = total_lines.saturating_sub(visible_limit);
        if layout.cursor_line_idx < start {
            start = layout.cursor_line_idx.saturating_sub(visible_limit - 1);
        }
        let end = (start + visible_limit).min(total_lines);
        (layout, start, end)
    }

    fn build_input_render(&self, width: u16, height: u16) -> InputRender {
        if width == 0 || height == 0 {
            return InputRender {
                text: Text::default(),
                cursor_x: 0,
                cursor_y: 0,
            };
        }

        let max_visible_lines = height.max(1).min(ui::INLINE_INPUT_MAX_LINES as u16) as usize;

        let mut prompt_style = self.prompt_style.clone();
        if prompt_style.color.is_none() {
            prompt_style.color = self.theme.primary.or(self.theme.foreground);
        }
        if self.suggested_prompt_state.active {
            prompt_style.color = self
                .theme
                .tool_accent
                .or(self.theme.secondary)
                .or(self.theme.primary)
                .or(self.theme.foreground);
            prompt_style.effects |= Effects::BOLD;
        }
        let prompt_style = ratatui_style_from_inline(&prompt_style, self.theme.foreground);
        let prompt_width = UnicodeWidthStr::width(self.prompt_prefix.as_str()) as u16;
        let prompt_display_width = prompt_width.min(width);

        let cursor_at_end = self.input_manager.cursor() == self.input_manager.content().len();
        if self.input_compact_mode
            && cursor_at_end
            && let Some(placeholder) = self.input_compact_placeholder()
        {
            let placeholder_style = InlineTextStyle {
                color: Some(AnsiColorEnum::Rgb(PLACEHOLDER_COLOR)),
                bg_color: None,
                effects: Effects::DIMMED,
            };
            let style = ratatui_style_from_inline(
                &placeholder_style,
                Some(AnsiColorEnum::Rgb(PLACEHOLDER_COLOR)),
            );
            let placeholder_width = UnicodeWidthStr::width(placeholder.as_str()) as u16;
            return InputRender {
                text: Text::from(vec![Line::from(vec![
                    Span::styled(self.prompt_prefix.clone(), prompt_style),
                    Span::styled(placeholder, style),
                ])]),
                cursor_x: prompt_display_width.saturating_add(placeholder_width),
                cursor_y: 0,
            };
        }

        if self.input_manager.content().is_empty() {
            let mut spans = Vec::new();
            spans.push(Span::styled(self.prompt_prefix.clone(), prompt_style));

            if let Some(suffix) = self.visible_inline_prompt_suggestion_suffix() {
                let ghost_style = ratatui_style_from_inline(
                    &InlineTextStyle {
                        color: Some(AnsiColorEnum::Rgb(PLACEHOLDER_COLOR)),
                        bg_color: None,
                        effects: Effects::DIMMED | Effects::ITALIC,
                    },
                    Some(AnsiColorEnum::Rgb(PLACEHOLDER_COLOR)),
                );
                spans.push(Span::styled(suffix, ghost_style));
            } else if let Some(placeholder) = &self.placeholder {
                let placeholder_style = self.placeholder_style.clone().unwrap_or(InlineTextStyle {
                    color: Some(AnsiColorEnum::Rgb(PLACEHOLDER_COLOR)),
                    bg_color: None,
                    effects: Effects::ITALIC,
                });
                let style = ratatui_style_from_inline(
                    &placeholder_style,
                    Some(AnsiColorEnum::Rgb(PLACEHOLDER_COLOR)),
                );
                spans.push(Span::styled(placeholder.clone(), style));
            }

            return InputRender {
                text: Text::from(vec![Line::from(spans)]),
                cursor_x: prompt_display_width,
                cursor_y: 0,
            };
        }

        let accent_style =
            ratatui_style_from_inline(&self.styles.accent_inline_style(), self.theme.foreground);
        let slash_style = accent_style.fg(Color::Yellow).add_modifier(Modifier::BOLD);
        let file_ref_style = accent_style
            .fg(Color::Cyan)
            .add_modifier(Modifier::UNDERLINED);
        let code_style = accent_style.fg(Color::Green).add_modifier(Modifier::BOLD);

        let (layout, start, end) = self.visible_input_window(width, max_visible_lines as u16);
        let tokens = tokenize_input(self.input_manager.content());
        let cursor_y = layout.cursor_line_idx.saturating_sub(start) as u16;

        let vis_count = end.saturating_sub(start);
        let mut lines = Vec::with_capacity(vis_count);
        for buffer in &layout.buffers[start..end] {
            let mut spans = Vec::with_capacity(4);
            spans.push(Span::styled(buffer.prefix.clone(), prompt_style));
            if !buffer.text.is_empty() {
                let buf_chars: Vec<char> = buffer.text.chars().collect();
                let buf_len = buf_chars.len();
                let buf_start = buffer.char_start;
                let buf_end = buf_start + buf_len;

                let mut pos = 0usize;
                for token in &tokens {
                    if token.end <= buf_start || token.start >= buf_end {
                        continue;
                    }
                    let seg_start = token.start.max(buf_start).saturating_sub(buf_start);
                    let seg_end = token.end.min(buf_end).saturating_sub(buf_start);
                    if seg_start > pos {
                        let text: String = buf_chars[pos..seg_start].iter().collect();
                        spans.push(Span::styled(text, accent_style));
                    }
                    let text: String = buf_chars[seg_start..seg_end].iter().collect();
                    let style = match token.kind {
                        InputTokenKind::SlashCommand => slash_style,
                        InputTokenKind::AgentReference | InputTokenKind::FileReference => {
                            file_ref_style
                        }
                        InputTokenKind::InlineCode => code_style,
                        InputTokenKind::Normal => accent_style,
                    };
                    spans.push(Span::styled(text, style));
                    pos = seg_end;
                }
                if pos < buf_len {
                    let text: String = buf_chars[pos..].iter().collect();
                    spans.push(Span::styled(text, accent_style));
                }
            }
            lines.push(Line::from(spans));
        }

        if let Some(suffix) = self.visible_inline_prompt_suggestion_suffix() {
            let ghost_style = ratatui_style_from_inline(
                &InlineTextStyle {
                    color: Some(AnsiColorEnum::Rgb(PLACEHOLDER_COLOR)),
                    bg_color: None,
                    effects: Effects::DIMMED | Effects::ITALIC,
                },
                Some(AnsiColorEnum::Rgb(PLACEHOLDER_COLOR)),
            );
            if let Some(line) = lines.get_mut(cursor_y as usize) {
                line.spans.push(Span::styled(suffix, ghost_style));
            }
        }

        if lines.is_empty() {
            lines.push(Line::from(vec![Span::styled(
                self.prompt_prefix.clone(),
                prompt_style,
            )]));
        }

        InputRender {
            text: Text::from(lines),
            cursor_x: layout.cursor_column,
            cursor_y,
        }
    }

    fn apply_input_selection_highlight(&self, buf: &mut Buffer, area: Rect) {
        let Some((selection_start, selection_end)) = self.input_manager.selection_range() else {
            return;
        };
        if area.width == 0 || area.height == 0 || selection_start == selection_end {
            return;
        }

        let (layout, start, end) = self.visible_input_window(area.width, area.height);
        let selection_start_char =
            byte_index_to_char_index(self.input_manager.content(), selection_start);
        let selection_end_char =
            byte_index_to_char_index(self.input_manager.content(), selection_end);

        for (row_offset, buffer) in layout.buffers[start..end].iter().enumerate() {
            let line_char_start = buffer.char_start;
            let line_char_end = buffer.char_start + buffer.text.chars().count();
            let highlight_start = selection_start_char.max(line_char_start);
            let highlight_end = selection_end_char.min(line_char_end);
            if highlight_start >= highlight_end {
                continue;
            }

            let local_start = highlight_start.saturating_sub(line_char_start);
            let local_end = highlight_end.saturating_sub(line_char_start);
            let start_x = area
                .x
                .saturating_add(buffer.prefix_width)
                .saturating_add(display_width_for_char_range(&buffer.text, local_start));
            let end_x = area
                .x
                .saturating_add(buffer.prefix_width)
                .saturating_add(display_width_for_char_range(&buffer.text, local_end));
            let y = area.y.saturating_add(row_offset as u16);

            for x in start_x..end_x.min(area.x.saturating_add(area.width)) {
                if let Some(cell) = buf.cell_mut((x, y)) {
                    let mut style = cell.style();
                    style = style.add_modifier(Modifier::REVERSED);
                    cell.set_style(style);
                    if cell.symbol().is_empty() {
                        cell.set_symbol(" ");
                    }
                }
            }
        }
    }

    pub(crate) fn cursor_index_for_input_point(&self, column: u16, row: u16) -> Option<usize> {
        let area = self.input_area?;
        if row < area.y
            || row >= area.y.saturating_add(area.height)
            || column < area.x
            || column >= area.x.saturating_add(area.width)
        {
            return None;
        }

        if self.input_compact_mode
            && self.input_manager.cursor() == self.input_manager.content().len()
            && self.input_compact_placeholder().is_some()
        {
            return Some(self.input_manager.content().len());
        }

        let relative_row = row.saturating_sub(area.y);
        let relative_column = column.saturating_sub(area.x);
        let (layout, start, end) = self.visible_input_window(area.width, area.height);
        if start >= end {
            return Some(0);
        }

        let line_index = (start + usize::from(relative_row)).min(end.saturating_sub(1));
        let buffer = layout.buffers.get(line_index)?;
        if relative_column <= buffer.prefix_width {
            return Some(char_index_to_byte_index(
                self.input_manager.content(),
                buffer.char_start,
            ));
        }

        let target_width = relative_column.saturating_sub(buffer.prefix_width);
        let mut consumed_width = 0u16;
        let mut char_offset = 0usize;
        for ch in buffer.text.chars() {
            let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0) as u16;
            let next_width = consumed_width.saturating_add(ch_width);
            if target_width < next_width {
                break;
            }
            consumed_width = next_width;
            char_offset += 1;
        }

        let char_index = buffer.char_start.saturating_add(char_offset);
        Some(char_index_to_byte_index(
            self.input_manager.content(),
            char_index,
        ))
    }

    pub(crate) fn input_compact_placeholder(&self) -> Option<String> {
        let content = self.input_manager.content();
        let trimmed = content.trim();
        let attachment_count = self.input_manager.attachments().len();
        if trimmed.is_empty() && attachment_count == 0 {
            return None;
        }

        if let Some(label) = compact_image_label(trimmed) {
            return Some(format!("[Image: {label}]"));
        }

        if attachment_count > 0 {
            let label = if attachment_count == 1 {
                "1 attachment".to_string()
            } else {
                format!("{attachment_count} attachments")
            };
            if trimmed.is_empty() {
                return Some(format!("[Image: {label}]"));
            }
            if let Some(compact) = compact_image_placeholders(content) {
                return Some(format!("[Image: {label}] {compact}"));
            }
            return Some(format!("[Image: {label}] {trimmed}"));
        }

        let line_count = content.split('\n').count();
        if line_count >= ui::INLINE_PASTE_COLLAPSE_LINE_THRESHOLD {
            let char_count = content.chars().count();
            return Some(format!("[Pasted Content {char_count} chars]"));
        }

        if let Some(compact) = compact_image_placeholders(content) {
            return Some(compact);
        }

        None
    }

    pub(crate) fn visible_inline_prompt_suggestion_suffix(&self) -> Option<String> {
        if !self.input_enabled
            || self.has_active_overlay()
            || self.input_compact_mode
            || self.input_manager.cursor() != self.input_manager.content().len()
        {
            return None;
        }

        let suggestion = self.inline_prompt_suggestion.suggestion.as_deref()?;
        inline_prompt_suggestion_suffix(self.input_manager.content(), suggestion)
    }

    pub(crate) fn render_input_status_line(&self, width: u16) -> Option<Line<'static>> {
        if width == 0 {
            return None;
        }

        let mut left = self
            .copy_notification_text()
            .map(str::to_owned)
            .or_else(|| self.status_left_text().map(str::to_owned));
        let right = self.status_right_text().map(str::to_owned);

        if let Some(shell_hint) = self.shell_mode_status_hint() {
            left = Some(match left {
                Some(existing) => format!("{existing} · {shell_hint}"),
                None => shell_hint.to_string(),
            });
        }
        if let Some(local_agents_hint) = self.local_agents_input_status_hint() {
            left = Some(match left {
                Some(existing) => format!("{existing} · {local_agents_hint}"),
                None => local_agents_hint,
            });
        }

        let right = match (right, self.vim_state.status_label()) {
            (Some(existing), Some(vim_label)) => Some(format!("{vim_label} · {existing}")),
            (None, Some(vim_label)) => Some(vim_label.to_string()),
            (existing, None) => existing,
        };

        // Build scroll indicator if enabled
        let scroll_indicator = if ui::SCROLL_INDICATOR_ENABLED {
            Some(self.build_scroll_indicator())
        } else {
            None
        };

        if left.is_none()
            && right.is_none()
            && scroll_indicator.is_none()
            && !self.thinking_spinner.is_active
        {
            return None;
        }

        let dim_style = self.styles.default_style().add_modifier(Modifier::DIM);
        let mut spans = Vec::new();

        // Add left content (git status or shimmered activity)
        if let Some(left_value) = left.as_ref() {
            if status_requires_shimmer(left_value)
                && self.appearance.should_animate_progress_status()
            {
                spans.extend(shimmer_spans_with_style_at_phase(
                    left_value,
                    self.styles.accent_style().add_modifier(Modifier::DIM),
                    self.shimmer_state.phase(),
                ));
            } else {
                spans.extend(self.create_git_status_spans(left_value, dim_style));
            }
        } else if self.thinking_spinner.is_active {
            spans.push(Span::styled(
                self.thinking_spinner.current_frame(),
                dim_style,
            ));
            spans.push(Span::raw(" "));
            spans.push(Span::styled("Thinking", dim_style));
        }

        // Build right side spans (scroll indicator + optional right content)
        let mut right_spans: Vec<Span<'static>> = Vec::new();
        if let Some(scroll) = &scroll_indicator {
            right_spans.push(Span::styled(scroll.clone(), dim_style));
        }
        if let Some(right_value) = &right {
            if !right_spans.is_empty() {
                right_spans.push(Span::raw(" "));
            }
            right_spans.push(Span::styled(right_value.clone(), dim_style));
        }

        if !right_spans.is_empty() {
            let left_width: u16 = spans.iter().map(|s| measure_text_width(&s.content)).sum();
            let right_width: u16 = right_spans
                .iter()
                .map(|s| measure_text_width(&s.content))
                .sum();
            let padding = width.saturating_sub(left_width + right_width);

            if padding > 0 {
                spans.push(Span::raw(" ".repeat(padding as usize)));
            } else if !spans.is_empty() {
                spans.push(Span::raw(" "));
            }
            spans.extend(right_spans);
        }

        if spans.is_empty() {
            return None;
        }

        let mut line = Line::from(spans);
        // Apply ellipsis truncation to prevent status line from overflowing
        line = truncate_line_with_ellipsis_if_overflow(line, usize::from(width));
        Some(line)
    }

    pub(crate) fn input_uses_shell_prefix(&self) -> bool {
        self.input_manager.content().trim_start().starts_with('!')
    }

    pub(crate) fn input_block_padding(&self) -> Padding {
        if self.input_uses_shell_prefix() {
            Padding::new(0, 0, 0, 0)
        } else {
            Padding::new(
                ui::INLINE_INPUT_PADDING_HORIZONTAL,
                ui::INLINE_INPUT_PADDING_HORIZONTAL,
                ui::INLINE_INPUT_PADDING_VERTICAL,
                ui::INLINE_INPUT_PADDING_VERTICAL,
            )
        }
    }

    pub(crate) fn shell_mode_border_title(&self) -> Option<&'static str> {
        self.input_uses_shell_prefix()
            .then_some(SHELL_MODE_BORDER_TITLE)
    }

    pub(crate) fn active_subagent_input_title(&self) -> Option<Line<'static>> {
        let badge = self.header_context.subagent_badges.first()?;
        let hidden = self.header_context.subagent_badges.len().saturating_sub(1);
        let label = if hidden == 0 {
            badge.text.clone()
        } else {
            format!("{} +{}", badge.text, hidden)
        };

        let mut style = ratatui_style_from_inline(&badge.style, self.theme.foreground);
        if badge.full_background {
            style = style.add_modifier(Modifier::BOLD);
        }

        Some(Line::from(Span::styled(format!(" {label} "), style)).right_aligned())
    }

    fn active_subagent_input_border_style(&self) -> Option<Style> {
        let badge = self.header_context.subagent_badges.first()?;
        let mut title_style = ratatui_style_from_inline(&badge.style, self.theme.foreground);
        if badge.full_background {
            title_style = title_style.add_modifier(Modifier::BOLD);
        }

        let color = if badge.full_background {
            title_style.bg.or(title_style.fg)
        } else {
            title_style.fg.or(title_style.bg)
        }?;

        Some(
            self.styles
                .accent_style()
                .fg(color)
                .add_modifier(Modifier::BOLD),
        )
    }

    fn shell_mode_status_hint(&self) -> Option<&'static str> {
        self.input_uses_shell_prefix()
            .then_some(SHELL_MODE_STATUS_HINT)
    }

    fn local_agents_input_status_hint(&self) -> Option<String> {
        if self.input_uses_shell_prefix() || !self.input_manager.content().trim().is_empty() {
            return None;
        }

        if !self.has_delegated_local_agents() {
            return None;
        }

        Some("↓ or Alt+S local agents · Ctrl+B background".to_string())
    }

    /// Build scroll indicator string with percentage
    fn build_scroll_indicator(&self) -> String {
        let percent = self.scroll_manager.progress_percent();
        format!("{} {:>3}%", ui::SCROLL_INDICATOR_FORMAT, percent)
    }

    #[expect(dead_code)]
    fn create_git_status_spans(&self, text: &str, default_style: Style) -> Vec<Span<'static>> {
        if let Some((branch_part, indicator_part)) = text.rsplit_once(" | ") {
            let mut spans = Vec::new();
            let branch_trim = branch_part.trim_end();
            if !branch_trim.is_empty() {
                spans.push(Span::styled(branch_trim.to_owned(), default_style));
            }
            spans.push(Span::raw(" "));

            let indicator_trim = indicator_part.trim();
            let indicator_style = if indicator_trim == ui::HEADER_GIT_DIRTY_SUFFIX {
                Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)
            } else if indicator_trim == ui::HEADER_GIT_CLEAN_SUFFIX {
                Style::default()
                    .fg(Color::Green)
                    .add_modifier(Modifier::BOLD)
            } else {
                self.styles.accent_style().add_modifier(Modifier::BOLD)
            };

            spans.push(Span::styled(indicator_trim.to_owned(), indicator_style));
            spans
        } else {
            vec![Span::styled(text.to_owned(), default_style)]
        }
    }

    fn cursor_should_be_visible(&self) -> bool {
        let loading_state = self.is_running_activity() || self.has_status_spinner();
        self.cursor_visible && (self.input_enabled || loading_state)
    }

    fn use_fake_cursor(&self) -> bool {
        self.has_status_spinner()
    }

    fn secure_prompt_active(&self) -> bool {
        self.modal_state()
            .and_then(|modal| modal.secure_prompt.as_ref())
            .is_some()
    }

    /// Build input render data for external widgets
    pub fn build_input_widget_data(&self, width: u16, height: u16) -> InputWidgetData {
        let input_render = self.build_input_render(width, height);
        let background_style = self.styles.input_background_style();

        InputWidgetData {
            text: input_render.text,
            cursor_x: input_render.cursor_x,
            cursor_y: input_render.cursor_y,
            cursor_should_be_visible: self.cursor_should_be_visible(),
            use_fake_cursor: self.use_fake_cursor(),
            background_style,
            default_style: self.styles.default_style(),
        }
    }

    /// Build input status line for external widgets
    pub fn build_input_status_widget_data(&self, width: u16) -> Option<Vec<Span<'static>>> {
        self.render_input_status_line(width).map(|line| line.spans)
    }
}

fn inline_prompt_suggestion_suffix(current: &str, suggestion: &str) -> Option<String> {
    if current.trim().is_empty() {
        return Some(suggestion.to_string());
    }

    let suggestion_lower = suggestion.to_lowercase();
    let current_lower = current.to_lowercase();
    if !suggestion_lower.starts_with(&current_lower) {
        return None;
    }

    Some(suggestion.chars().skip(current.chars().count()).collect())
}

fn compact_image_label(content: &str) -> Option<String> {
    let trimmed = content.trim();
    if trimmed.is_empty() {
        return None;
    }

    let unquoted = trimmed
        .strip_prefix('"')
        .and_then(|value| value.strip_suffix('"'))
        .or_else(|| {
            trimmed
                .strip_prefix('\'')
                .and_then(|value| value.strip_suffix('\''))
        })
        .unwrap_or(trimmed);

    if unquoted.starts_with("data:image/") {
        return Some("inline image".to_string());
    }

    let windows_drive = unquoted.as_bytes().get(1).is_some_and(|ch| *ch == b':')
        && unquoted
            .as_bytes()
            .get(2)
            .is_some_and(|ch| *ch == b'\\' || *ch == b'/');
    let starts_like_path = unquoted.starts_with('@')
        || unquoted.starts_with("file://")
        || unquoted.starts_with('/')
        || unquoted.starts_with("./")
        || unquoted.starts_with("../")
        || unquoted.starts_with("~/")
        || windows_drive;
    if !starts_like_path {
        return None;
    }

    let without_at = unquoted.strip_prefix('@').unwrap_or(unquoted);

    // Skip npm scoped package patterns like @scope/package@version
    if without_at.contains('/')
        && !without_at.starts_with('.')
        && !without_at.starts_with('/')
        && !without_at.starts_with("~/")
    {
        // Check if this looks like @scope/package (npm package)
        let parts: Vec<&str> = without_at.split('/').collect();
        if parts.len() >= 2 && !parts[0].is_empty() {
            // Reject if it looks like a package name (no extension on second component)
            if !parts[parts.len() - 1].contains('.') {
                return None;
            }
        }
    }

    let without_scheme = without_at.strip_prefix("file://").unwrap_or(without_at);
    let path = Path::new(without_scheme);
    if !is_image_path(path) {
        return None;
    }

    let label = path
        .file_name()
        .and_then(|name| name.to_str())
        .unwrap_or(without_scheme);
    Some(label.to_string())
}

static IMAGE_PATH_INLINE_REGEX: LazyLock<Regex> = LazyLock::new(|| {
    match Regex::new(
        r#"(?ix)
        (?:^|[\s\(\[\{<\"'`])
        (
            @?
            (?:file://)?
            (?:
                ~/(?:[^\n/]+/)+
              | /(?:[^\n/]+/)+
              | [A-Za-z]:[\\/](?:[^\n\\\/]+[\\/])+
            )
            [^\n]*?
            \.(?:png|jpe?g|gif|bmp|webp|tiff?|svg)
        )"#,
    ) {
        Ok(regex) => regex,
        Err(error) => panic!("Failed to compile inline image path regex: {error}"),
    }
});

fn compact_image_placeholders(content: &str) -> Option<String> {
    let mut matches = Vec::new();
    for capture in IMAGE_PATH_INLINE_REGEX.captures_iter(content) {
        let Some(path_match) = capture.get(1) else {
            continue;
        };
        let raw = path_match.as_str();
        let Some(label) = image_label_for_path(raw) else {
            continue;
        };
        matches.push((path_match.start(), path_match.end(), label));
    }

    if matches.is_empty() {
        return None;
    }

    let mut result = String::with_capacity(content.len());
    let mut last_end = 0usize;
    for (start, end, label) in matches {
        if start < last_end {
            continue;
        }
        result.push_str(&content[last_end..start]);
        let _ = write!(result, "[Image: {label}]");
        last_end = end;
    }
    if last_end < content.len() {
        result.push_str(&content[last_end..]);
    }

    Some(result)
}

fn image_label_for_path(raw: &str) -> Option<String> {
    let trimmed = raw.trim_matches(|ch: char| matches!(ch, '"' | '\'')).trim();
    if trimmed.is_empty() {
        return None;
    }

    let without_at = trimmed.strip_prefix('@').unwrap_or(trimmed);
    let without_scheme = without_at.strip_prefix("file://").unwrap_or(without_at);
    let unescaped = unescape_whitespace(without_scheme);
    let path = Path::new(unescaped.as_str());
    if !is_image_path(path) {
        return None;
    }

    let label = path
        .file_name()
        .and_then(|name| name.to_str())
        .unwrap_or(unescaped.as_str());
    Some(label.to_string())
}

fn unescape_whitespace(token: &str) -> String {
    let mut result = String::with_capacity(token.len());
    let mut chars = token.chars().peekable();
    while let Some(ch) = chars.next() {
        if ch == '\\'
            && let Some(next) = chars.peek()
            && next.is_ascii_whitespace()
        {
            result.push(*next);
            chars.next();
            continue;
        }
        result.push(ch);
    }
    result
}

fn is_spinner_frame(indicator: &str) -> bool {
    matches!(
        indicator,
        "⠋" | "⠙"
            | "⠹"
            | "⠸"
            | "⠼"
            | "⠴"
            | "⠦"
            | "⠧"
            | "⠇"
            | "⠏"
            | "-"
            | "\\"
            | "|"
            | "/"
            | "."
    )
}

pub(crate) fn status_requires_shimmer(text: &str) -> bool {
    let normalized = text.trim().to_ascii_lowercase();

    if normalized.contains("running command:")
        || normalized.contains("running tool:")
        || normalized.contains("running:")
        || normalized.contains("running ")
        || normalized.contains("executing ")
        || normalized.contains("approval required")
        || normalized.contains("permission required")
        || normalized.contains("action required")
        || normalized.contains("input required")
        || normalized.contains("waiting for approval")
        || normalized.contains("waiting for input")
        || normalized.contains("ctrl+c")
        || normalized.contains("/stop to stop")
    {
        return true;
    }
    let Some((indicator, rest)) = text.split_once(' ') else {
        return false;
    };
    if indicator.chars().count() != 1 || rest.trim().is_empty() {
        return false;
    }
    is_spinner_frame(indicator)
}

/// Data structure for input widget rendering
#[derive(Clone, Debug)]
pub struct InputWidgetData {
    pub text: Text<'static>,
    pub cursor_x: u16,
    pub cursor_y: u16,
    pub cursor_should_be_visible: bool,
    pub use_fake_cursor: bool,
    pub background_style: Style,
    pub default_style: Style,
}

fn render_fake_cursor(buf: &mut Buffer, cursor_x: u16, cursor_y: u16) {
    if let Some(cell) = buf.cell_mut((cursor_x, cursor_y)) {
        let mut style = cell.style();
        style = style.add_modifier(Modifier::REVERSED);
        cell.set_style(style);
        if cell.symbol().is_empty() {
            cell.set_symbol(" ");
        }
    }
}

fn char_index_to_byte_index(content: &str, char_index: usize) -> usize {
    if char_index == 0 {
        return 0;
    }

    content
        .char_indices()
        .nth(char_index)
        .map(|(byte_index, _)| byte_index)
        .unwrap_or(content.len())
}

fn byte_index_to_char_index(content: &str, byte_index: usize) -> usize {
    content[..byte_index.min(content.len())].chars().count()
}

fn display_width_for_char_range(content: &str, char_count: usize) -> u16 {
    content
        .chars()
        .take(char_count)
        .map(|ch| UnicodeWidthChar::width(ch).unwrap_or(0) as u16)
        .fold(0_u16, u16::saturating_add)
}

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

    fn kinds(input: &str) -> Vec<(InputTokenKind, String)> {
        tokenize_input(input)
            .into_iter()
            .map(|t| {
                let text: String = input.chars().skip(t.start).take(t.end - t.start).collect();
                (t.kind, text)
            })
            .collect()
    }

    #[test]
    fn slash_command_at_start() {
        let tokens = kinds("/use skill-name");
        assert_eq!(tokens[0].0, InputTokenKind::SlashCommand);
        assert_eq!(tokens[0].1, "/use");
        assert_eq!(tokens[1].0, InputTokenKind::Normal);
    }

    #[test]
    fn slash_command_with_following_text() {
        let tokens = kinds("/doctor hello");
        assert_eq!(tokens[0].0, InputTokenKind::SlashCommand);
        assert_eq!(tokens[0].1, "/doctor");
        assert_eq!(tokens[1].0, InputTokenKind::Normal);
    }

    #[test]
    fn at_file_reference() {
        let tokens = kinds("check @src/main.rs please");
        assert_eq!(tokens[0].0, InputTokenKind::Normal);
        assert_eq!(tokens[1].0, InputTokenKind::FileReference);
        assert_eq!(tokens[1].1, "@src/main.rs");
        assert_eq!(tokens[2].0, InputTokenKind::Normal);
    }

    #[test]
    fn inline_backtick_code() {
        let tokens = kinds("run `cargo test` now");
        assert_eq!(tokens[0].0, InputTokenKind::Normal);
        assert_eq!(tokens[1].0, InputTokenKind::InlineCode);
        assert_eq!(tokens[1].1, "`cargo test`");
        assert_eq!(tokens[2].0, InputTokenKind::Normal);
    }

    #[test]
    fn no_false_slash_mid_word() {
        let tokens = kinds("path/to/file");
        assert_eq!(tokens.len(), 1);
        assert_eq!(tokens[0].0, InputTokenKind::Normal);
    }

    #[test]
    fn empty_input() {
        assert!(tokenize_input("").is_empty());
    }

    #[test]
    fn mixed_tokens() {
        let tokens = kinds("/use @file.rs `code`");
        assert_eq!(tokens[0].0, InputTokenKind::SlashCommand);
        assert_eq!(tokens[2].0, InputTokenKind::FileReference);
        assert_eq!(tokens[4].0, InputTokenKind::InlineCode);
    }

    #[test]
    fn agent_reference_has_dedicated_token_kind() {
        let tokens = kinds("use @agent-explorer for this");
        assert_eq!(tokens[1].0, InputTokenKind::AgentReference);
        assert_eq!(tokens[1].1, "@agent-explorer");
    }

    #[test]
    fn plugin_agent_reference_has_dedicated_token_kind() {
        let tokens = kinds("use @agent-github:reviewer for this");
        assert_eq!(tokens[1].0, InputTokenKind::AgentReference);
        assert_eq!(tokens[1].1, "@agent-github:reviewer");
    }
}
