use super::agent_mode::AgentMode;
use super::paste_guard::PasteGuard;
use super::prompt_buffer::{PromptBuffer, wrapped_row_count};
use super::prompt_display::PromptDisplay;
use super::prompt_edit::{
    char_left, char_right, delete_char_backward, delete_char_forward, delete_to_line_end, delete_to_line_start,
    delete_word_backward, delete_word_forward, line_end, line_start, word_left, word_right,
};
use super::prompt_keys::{
    EditAction, edit_action, is_clear_key, is_newline_key, is_pasted_tab_key, is_submit_key, should_cycle_agent_mode,
    should_insert_tab_in_prompt,
};
use super::prompt_paste::{
    CollapsedPaste, PasteInsertCtx, PendingPaste, adjust_pastes_for_delete, apply_pasted_char_pure,
    enter_should_finalize_paste, expand_paste_markers, finalize_pending_paste, paste_block_range,
    remove_paste_block_and_adjust, shift_paste_offsets_for_insert, should_collapse_paste,
};
use crate::theme::Theme;
use iocraft::prelude::*;
use std::sync::{Arc, Mutex};
use std::time::Instant;

const PROMPT_PREFIX: &str = "> ";
const MIN_INPUT_LINES: u16 = 1;
const MAX_INPUT_LINES: u16 = 5;
/// During rapid paste bursts, sync prompt state every N inserted bytes (iocraft `TextInput` uses one `on_change` per edit).
const BURST_VALUE_SYNC_INTERVAL: usize = 48;
/// Only batch state updates once a paste run is long enough to benefit from fewer re-renders.
const BURST_BATCH_MIN_CHARS: usize = 200;
/// Fallback before the text field has been measured.
const FALLBACK_TEXT_WIDTH: u16 = 40;
/// Horizontal space taken by app padding, border padding, prefix, and border glyphs.
const HORIZONTAL_CHROME: u16 = 8;

#[derive(Default, Props)]
pub struct PromptInputProps {
    /// Prompt text state (see iocraft `form` example).
    pub value: Option<State<String>>,

    /// Model name shown below the input (e.g. `claude-fable-5`).
    pub model_name: String,

    /// Current agent mode; tints the prompt border and mode label in the footer.
    pub mode: AgentMode,

    /// Whether the text field accepts keyboard input.
    pub has_focus: bool,

    /// Called when Enter is pressed to send/submit the prompt.
    pub on_submit: HandlerMut<'static, String>,

    /// Called when the user cycles agent mode (Tab).
    pub on_mode_change: HandlerMut<'static, AgentMode>,

    /// Bumped by the parent to reset the field after Ctrl+C / SIGINT clear.
    pub reset_nonce: Option<State<u32>>,

    /// Active color palette.
    pub theme: Theme,

    /// Test-only mirror of committed prompt value (updated when value changes).
    #[doc(hidden)]
    pub value_probe: Option<Arc<Mutex<String>>>,
}

#[component]
pub fn PromptInput(mut hooks: Hooks, props: &mut PromptInputProps) -> impl Into<AnyElement<'static>> {
    let Some(mut value) = props.value else {
        panic!("value is required");
    };
    let theme = props.theme;
    let mode_color = theme.mode_accent(props.mode);
    let model_status = format!("{} • ", props.model_name);
    let mode_label = props.mode.label();
    let (terminal_width, _) = hooks.use_terminal_size();
    let fallback_width = terminal_width
        .saturating_sub(HORIZONTAL_CHROME)
        .max(FALLBACK_TEXT_WIDTH);
    let measured_width = hooks.use_state(move || fallback_width);
    let mut cursor_offset = hooks.use_state(|| 0usize);
    let mut vertical_col_pref = hooks.use_state(|| None::<u16>);
    let mut field_clear_generation = hooks.use_state(|| 0u32);
    let mut paste_guard = hooks.use_ref(PasteGuard::default);
    let mut pending_paste = hooks.use_ref(|| None::<PendingPaste>);
    let mut collapsed_pastes = hooks.use_ref(Vec::<CollapsedPaste>::new);
    let mut draft_text = hooks.use_ref(|| None::<String>);
    let mut burst_chars_since_sync = hooks.use_ref(|| 0usize);
    let mut separator_after = hooks.use_ref(|| None::<usize>);
    let current = draft_text.read().clone().unwrap_or_else(|| value.read().clone());
    let text_width = measured_width.get().max(1);
    let computed_height = visual_line_count(&current, text_width);
    let input_height = computed_height;
    let mut on_submit = props.on_submit.take();
    let mut on_mode_change = props.on_mode_change.take();
    let current_mode = props.mode;
    let has_focus = props.has_focus;
    let reset_dep = props.reset_nonce.map(|nonce| nonce.get()).unwrap_or(0);
    let mut value_probe_slot = hooks.use_ref(|| None::<Arc<Mutex<String>>>);
    if let Some(probe) = props.value_probe.take() {
        let snapshot = value.read().clone();
        if let Ok(mut guard) = probe.lock()
            && *guard != snapshot
        {
            *guard = snapshot;
        }
        *value_probe_slot.write() = Some(probe);
    }

    hooks.use_effect(
        move || {
            if reset_dep == 0 {
                return;
            }
            cursor_offset.set(0);
            vertical_col_pref.set(None);
            pending_paste.write().take();
            collapsed_pastes.write().clear();
            paste_guard.write().clear();
            draft_text.write().take();
            *burst_chars_since_sync.write() = 0;
            *separator_after.write() = None;
        },
        reset_dep,
    );

    let clear_generation = field_clear_generation.get();
    hooks.use_effect(
        move || {
            if clear_generation == 0 {
                return;
            }
            cursor_offset.set(0);
            vertical_col_pref.set(None);
            pending_paste.write().take();
            collapsed_pastes.write().clear();
            paste_guard.write().clear();
            draft_text.write().take();
            *burst_chars_since_sync.write() = 0;
            *separator_after.write() = None;
        },
        clear_generation,
    );

    hooks.use_terminal_events(move |event| {
        if !has_focus {
            return;
        }

        let TerminalEvent::Key(KeyEvent {
            code, kind, modifiers, ..
        }) = event
        else {
            return;
        };

        if kind == KeyEventKind::Release {
            return;
        }

        let wrap_width = text_width.saturating_sub(1).max(1) as usize;
        let now = Instant::now();

        let mut text = load_prompt_text(&value, &draft_text.read());
        let mut cursor = cursor_offset.get().min(text.len());

        if is_press(kind) {
            let in_burst = paste_guard.read().is_in_burst(now);
            if !in_burst {
                flush_draft_if_any(&mut value, &mut draft_text.write(), &mut burst_chars_since_sync.write());
            }
            let paste_active = paste_guard.read().is_paste_active(now);
            maybe_finalize_idle_paste(
                &mut PromptPasteCtx {
                    paste_guard: &mut paste_guard.write(),
                    pending: &mut pending_paste.write(),
                    pastes: &mut collapsed_pastes.write(),
                    separator_after: &mut separator_after.write(),
                    value: &mut value,
                    draft: &mut draft_text.write(),
                    burst_since_sync: &mut burst_chars_since_sync.write(),
                    cursor_offset: &mut cursor_offset,
                    value_probe: value_probe_slot.read().clone(),
                },
                in_burst,
                paste_active,
                wrap_width,
                now,
            );
            text = load_prompt_text(&value, &draft_text.read());
            cursor = cursor_offset.get().min(text.len());
        }

        if is_press(kind) && should_finalize_paste(code, modifiers) {
            finalize_pending_paste_input(
                &mut PromptPasteCtx {
                    paste_guard: &mut paste_guard.write(),
                    pending: &mut pending_paste.write(),
                    pastes: &mut collapsed_pastes.write(),
                    separator_after: &mut separator_after.write(),
                    value: &mut value,
                    draft: &mut draft_text.write(),
                    burst_since_sync: &mut burst_chars_since_sync.write(),
                    cursor_offset: &mut cursor_offset,
                    value_probe: value_probe_slot.read().clone(),
                },
                wrap_width,
                now,
            );
            text = load_prompt_text(&value, &draft_text.read());
            cursor = cursor_offset.get().min(text.len());
        }

        let paste_active = paste_guard.read().is_paste_active(now);
        let in_burst = paste_guard.read().is_in_burst(now);

        if is_press(kind)
            && is_pasted_tab_key(code, modifiers)
            && should_insert_tab_in_prompt(&text, paste_active, in_burst)
        {
            apply_pasted_char(
                '\t',
                &mut text,
                &mut cursor,
                &mut PromptPasteCtx {
                    paste_guard: &mut paste_guard.write(),
                    pending: &mut pending_paste.write(),
                    pastes: &mut collapsed_pastes.write(),
                    separator_after: &mut separator_after.write(),
                    value: &mut value,
                    draft: &mut draft_text.write(),
                    burst_since_sync: &mut burst_chars_since_sync.write(),
                    cursor_offset: &mut cursor_offset,
                    value_probe: value_probe_slot.read().clone(),
                },
                now,
            );
            vertical_col_pref.set(None);
            return;
        }

        if should_cycle_agent_mode(&text, paste_active, in_burst, code, modifiers) && is_press(kind) {
            on_mode_change(current_mode.next());
            return;
        }

        if is_clear_key(code, modifiers) && is_press(kind) {
            if !text.is_empty() {
                value.set(String::new());
                cursor_offset.set(0);
                vertical_col_pref.set(None);
                field_clear_generation.set(field_clear_generation.get().wrapping_add(1));
                pending_paste.write().take();
                collapsed_pastes.write().clear();
                paste_guard.write().clear();
                draft_text.write().take();
                *burst_chars_since_sync.write() = 0;
                *separator_after.write() = None;
                sync_value_probe(value_probe_slot.read().as_ref(), "");
            }
            return;
        }

        if let Some(action) = edit_action(code, modifiers)
            && is_press(kind)
        {
            let (next, new_cursor) = match action {
                EditAction::DeleteToLineStart => delete_to_line_start(&text, cursor),
                EditAction::DeleteToLineEnd => delete_to_line_end(&text, cursor),
                EditAction::DeleteWordBackward => delete_word_backward(&text, cursor),
                EditAction::DeleteWordForward => delete_word_forward(&text, cursor),
                EditAction::DeleteCharBackward => delete_char_backward(&text, cursor),
                EditAction::DeleteCharForward => delete_char_forward(&text, cursor),
                EditAction::LineStart => (text.clone(), line_start(&text, cursor)),
                EditAction::LineEnd => (text.clone(), line_end(&text, cursor)),
                EditAction::WordLeft => (text.clone(), word_left(&text, cursor)),
                EditAction::WordRight => (text.clone(), word_right(&text, cursor)),
                EditAction::CharLeft => (text.clone(), char_left(&text, cursor)),
                EditAction::CharRight => (text.clone(), char_right(&text, cursor)),
            };
            if next != text {
                let mut pastes = collapsed_pastes.write();
                adjust_pastes_for_delete(&mut pastes, new_cursor..cursor);
                commit_prompt_text(
                    &mut value,
                    &mut draft_text.write(),
                    &mut burst_chars_since_sync.write(),
                    next,
                    value_probe_slot.read().as_ref(),
                );
            }
            cursor_offset.set(new_cursor);
            vertical_col_pref.set(None);
            return;
        }

        if (is_newline_key(code, modifiers) || is_pasted_newline_char(code)) && is_press(kind) {
            vertical_col_pref.set(None);
            if is_pasted_newline_char(code) {
                apply_pasted_char(
                    '\n',
                    &mut text,
                    &mut cursor,
                    &mut PromptPasteCtx {
                        paste_guard: &mut paste_guard.write(),
                        pending: &mut pending_paste.write(),
                        pastes: &mut collapsed_pastes.write(),
                        separator_after: &mut separator_after.write(),
                        value: &mut value,
                        draft: &mut draft_text.write(),
                        burst_since_sync: &mut burst_chars_since_sync.write(),
                        cursor_offset: &mut cursor_offset,
                        value_probe: value_probe_slot.read().clone(),
                    },
                    now,
                );
                return;
            }
            let (next, new_cursor) = insert_newline_at_cursor(&text, cursor);
            if next != text {
                let inserted = next.len().saturating_sub(text.len());
                shift_paste_offsets_for_insert(&mut collapsed_pastes.write(), cursor, inserted);
                commit_prompt_text(
                    &mut value,
                    &mut draft_text.write(),
                    &mut burst_chars_since_sync.write(),
                    next,
                    value_probe_slot.read().as_ref(),
                );
            }
            cursor_offset.set(new_cursor);
            return;
        }

        if is_submit_key(code, modifiers) && is_press(kind) {
            flush_draft_if_any(&mut value, &mut draft_text.write(), &mut burst_chars_since_sync.write());
            text = load_prompt_text(&value, &draft_text.read());

            let paste_recent = paste_guard.read().is_paste_active(now);
            let in_burst = paste_guard.read().is_in_burst(now);
            let pending_snapshot = pending_paste.read().clone();

            if enter_should_finalize_paste(&text, pending_snapshot.as_ref(), paste_recent, in_burst) {
                finalize_pending_paste_input(
                    &mut PromptPasteCtx {
                        paste_guard: &mut paste_guard.write(),
                        pending: &mut pending_paste.write(),
                        pastes: &mut collapsed_pastes.write(),
                        separator_after: &mut separator_after.write(),
                        value: &mut value,
                        draft: &mut draft_text.write(),
                        burst_since_sync: &mut burst_chars_since_sync.write(),
                        cursor_offset: &mut cursor_offset,
                        value_probe: value_probe_slot.read().clone(),
                    },
                    wrap_width,
                    now,
                );
                return;
            }

            pending_paste.write().take();

            if paste_guard.write().should_block_submit() {
                return;
            }

            if !text.is_empty() {
                let submitted = expand_paste_markers(&text, &collapsed_pastes.read());
                on_submit(submitted);
                value.set(String::new());
                field_clear_generation.set(field_clear_generation.get().wrapping_add(1));
                cursor_offset.set(0);
                vertical_col_pref.set(None);
                pending_paste.write().take();
                collapsed_pastes.write().clear();
                paste_guard.write().clear();
                draft_text.write().take();
                *burst_chars_since_sync.write() = 0;
            }
            return;
        }

        if modifiers.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SUPER) {
            return;
        }

        let mut clear_vertical_pref = true;

        match code {
            KeyCode::Char(ch) => {
                if ch == '\t' {
                    if should_insert_tab_in_prompt(&text, paste_active, in_burst) {
                        apply_pasted_char(
                            '\t',
                            &mut text,
                            &mut cursor,
                            &mut PromptPasteCtx {
                                paste_guard: &mut paste_guard.write(),
                                pending: &mut pending_paste.write(),
                                pastes: &mut collapsed_pastes.write(),
                                separator_after: &mut separator_after.write(),
                                value: &mut value,
                                draft: &mut draft_text.write(),
                                burst_since_sync: &mut burst_chars_since_sync.write(),
                                cursor_offset: &mut cursor_offset,
                                value_probe: value_probe_slot.read().clone(),
                            },
                            now,
                        );
                    }
                } else {
                    apply_pasted_char(
                        ch,
                        &mut text,
                        &mut cursor,
                        &mut PromptPasteCtx {
                            paste_guard: &mut paste_guard.write(),
                            pending: &mut pending_paste.write(),
                            pastes: &mut collapsed_pastes.write(),
                            separator_after: &mut separator_after.write(),
                            value: &mut value,
                            draft: &mut draft_text.write(),
                            burst_since_sync: &mut burst_chars_since_sync.write(),
                            cursor_offset: &mut cursor_offset,
                            value_probe: value_probe_slot.read().clone(),
                        },
                        now,
                    );
                }
            }
            KeyCode::Backspace => {
                let paste_snapshot = collapsed_pastes.read().clone();
                if let Some(range) = paste_block_range(&text, cursor.saturating_sub(1), &paste_snapshot) {
                    if let Some(next) =
                        remove_paste_block_and_adjust(&text, range.clone(), &mut collapsed_pastes.write())
                    {
                        commit_prompt_text(
                            &mut value,
                            &mut draft_text.write(),
                            &mut burst_chars_since_sync.write(),
                            next,
                            value_probe_slot.read().as_ref(),
                        );
                        cursor_offset.set(range.start);
                    }
                } else if cursor > 0 {
                    let (next, new_cursor) = delete_char_backward(&text, cursor);
                    adjust_pastes_for_delete(&mut collapsed_pastes.write(), new_cursor..cursor);
                    commit_prompt_text(
                        &mut value,
                        &mut draft_text.write(),
                        &mut burst_chars_since_sync.write(),
                        next,
                        value_probe_slot.read().as_ref(),
                    );
                    cursor_offset.set(new_cursor);
                }
            }
            KeyCode::Delete => {
                let paste_snapshot = collapsed_pastes.read().clone();
                if let Some(range) = paste_block_range(&text, cursor, &paste_snapshot) {
                    if let Some(next) =
                        remove_paste_block_and_adjust(&text, range.clone(), &mut collapsed_pastes.write())
                    {
                        commit_prompt_text(
                            &mut value,
                            &mut draft_text.write(),
                            &mut burst_chars_since_sync.write(),
                            next,
                            value_probe_slot.read().as_ref(),
                        );
                        cursor_offset.set(range.start);
                    }
                } else if cursor < text.len() {
                    let (next, new_cursor) = delete_char_forward(&text, cursor);
                    adjust_pastes_for_delete(&mut collapsed_pastes.write(), cursor..new_cursor);
                    commit_prompt_text(
                        &mut value,
                        &mut draft_text.write(),
                        &mut burst_chars_since_sync.write(),
                        next,
                        value_probe_slot.read().as_ref(),
                    );
                    cursor_offset.set(new_cursor);
                }
            }
            KeyCode::Left => {
                cursor = cursor_offset.get();
                let buffer = PromptBuffer::new(&text, wrap_width);
                cursor_offset.set(buffer.left_of_offset(cursor));
            }
            KeyCode::Right => {
                cursor = cursor_offset.get();
                let buffer = PromptBuffer::new(&text, wrap_width);
                cursor_offset.set(buffer.right_of_offset(cursor));
            }
            KeyCode::Up => {
                cursor = cursor_offset.get();
                let buffer = PromptBuffer::new(&text, wrap_width);
                clear_vertical_pref = false;
                if vertical_col_pref.get().is_none() {
                    let (_, col) = buffer.row_column_for_offset(cursor);
                    vertical_col_pref.set(Some(col));
                }
                cursor_offset.set(buffer.above_offset(cursor, vertical_col_pref.get()));
            }
            KeyCode::Down => {
                cursor = cursor_offset.get();
                let buffer = PromptBuffer::new(&text, wrap_width);
                clear_vertical_pref = false;
                if vertical_col_pref.get().is_none() {
                    let (_, col) = buffer.row_column_for_offset(cursor);
                    vertical_col_pref.set(Some(col));
                }
                cursor_offset.set(buffer.below_offset(cursor, vertical_col_pref.get()));
            }
            KeyCode::Home => {
                cursor = cursor_offset.get();
                let buffer = PromptBuffer::new(&text, wrap_width);
                cursor_offset.set(buffer.row_start_offset(cursor));
            }
            KeyCode::End => {
                cursor = cursor_offset.get();
                let buffer = PromptBuffer::new(&text, wrap_width);
                cursor_offset.set(buffer.row_end_offset(cursor));
            }
            _ => {
                clear_vertical_pref = false;
            }
        }

        if clear_vertical_pref {
            vertical_col_pref.set(None);
        }
    });

    element! {
        View(
            width: 100pct,
            flex_shrink: 0.0,
            flex_direction: FlexDirection::Column,
        ) {
            View(
                border_style: BorderStyle::Round,
                border_color: mode_color,
                width: 100pct,
                overflow: Overflow::Hidden,
                padding_left: 1,
                padding_right: 1,
            ) {
                View(
                    flex_direction: FlexDirection::Row,
                    align_items: AlignItems::FlexStart,
                    width: 100pct,
                    height: input_height,
                    overflow: Overflow::Hidden,
                ) {
                    View(width: 2, height: input_height, justify_content: JustifyContent::FlexStart) {
                        Text(color: theme.prompt_prefix, content: PROMPT_PREFIX)
                    }
                    View(flex_grow: 1.0, width: 100pct, height: input_height) {
                        PromptDisplay(
                            value: current,
                            cursor_offset: cursor_offset.get(),
                            height: input_height,
                            has_focus: props.has_focus,
                            theme,
                            collapsed_pastes: collapsed_pastes.read().clone(),
                            measured_width: Some(measured_width),
                        )
                    }
                }
            }
            View(
                width: 100pct,
                flex_direction: FlexDirection::Row,
                justify_content: JustifyContent::FlexEnd,
            ) {
                Text(color: theme.muted, content: model_status)
                Text(color: mode_color, content: mode_label)
            }
        }
    }
}

fn is_press(kind: KeyEventKind) -> bool {
    kind == KeyEventKind::Press
}

fn is_pasted_newline_char(code: KeyCode) -> bool {
    matches!(code, KeyCode::Char('\n') | KeyCode::Char('\r'))
}

fn should_finalize_paste(code: KeyCode, modifiers: KeyModifiers) -> bool {
    if is_submit_key(code, modifiers) || is_newline_key(code, modifiers) {
        return false;
    }
    !matches!(code, KeyCode::Char(_) | KeyCode::Tab | KeyCode::BackTab)
}

fn load_prompt_text(value: &State<String>, draft: &Option<String>) -> String {
    draft.clone().unwrap_or_else(|| value.read().clone())
}

fn commit_prompt_text(
    value: &mut State<String>,
    draft: &mut Option<String>,
    burst_since_sync: &mut usize,
    text: String,
    value_probe: Option<&Arc<Mutex<String>>>,
) {
    value.set(text.clone());
    sync_value_probe(value_probe, &text);
    draft.take();
    *burst_since_sync = 0;
}

fn flush_draft_if_any(value: &mut State<String>, draft: &mut Option<String>, burst_since_sync: &mut usize) {
    if let Some(text) = draft.take() {
        value.set(text);
        *burst_since_sync = 0;
    }
}

struct PromptPasteCtx<'a> {
    paste_guard: &'a mut PasteGuard,
    pending: &'a mut Option<PendingPaste>,
    pastes: &'a mut Vec<CollapsedPaste>,
    separator_after: &'a mut Option<usize>,
    value: &'a mut State<String>,
    draft: &'a mut Option<String>,
    burst_since_sync: &'a mut usize,
    cursor_offset: &'a mut State<usize>,
    value_probe: Option<Arc<Mutex<String>>>,
}

fn sync_value_probe(value_probe: Option<&Arc<Mutex<String>>>, text: &str) {
    if let Some(probe) = value_probe
        && let Ok(mut guard) = probe.lock()
        && *guard != text
    {
        *guard = text.to_string();
    }
}

fn apply_pasted_char(ch: char, text: &mut String, cursor: &mut usize, ctx: &mut PromptPasteCtx<'_>, now: Instant) {
    let mut insert_ctx = PasteInsertCtx {
        text: std::mem::take(text),
        cursor: *cursor,
        pending: ctx.pending.take(),
        pastes: std::mem::take(ctx.pastes),
        guard: *ctx.paste_guard,
        separator_after: *ctx.separator_after,
    };
    apply_pasted_char_pure(&mut insert_ctx, ch, now);
    *text = insert_ctx.text;
    *cursor = insert_ctx.cursor;
    *ctx.pending = insert_ctx.pending;
    *ctx.pastes = insert_ctx.pastes;
    *ctx.paste_guard = insert_ctx.guard;
    *ctx.separator_after = insert_ctx.separator_after;

    *ctx.draft = Some(text.clone());
    *ctx.burst_since_sync += ch.len_utf8();
    let paste_run_len = ctx
        .pending
        .as_ref()
        .map(|run| run.end.saturating_sub(run.start))
        .unwrap_or(0);
    let defer_sync = ctx.paste_guard.is_paste_active(now)
        && ctx.paste_guard.is_in_burst(now)
        && paste_run_len >= BURST_BATCH_MIN_CHARS
        && *ctx.burst_since_sync < BURST_VALUE_SYNC_INTERVAL;
    if !defer_sync {
        commit_prompt_text(
            ctx.value,
            ctx.draft,
            ctx.burst_since_sync,
            text.clone(),
            ctx.value_probe.as_ref(),
        );
    }
    ctx.cursor_offset.set(*cursor);
}

fn maybe_finalize_idle_paste(
    ctx: &mut PromptPasteCtx<'_>,
    in_burst: bool,
    _paste_active: bool,
    wrap_width: usize,
    now: Instant,
) {
    let Some(run) = ctx.pending.as_ref() else {
        return;
    };

    if in_burst || run.tab_follows_paste(now) {
        return;
    }

    finalize_pending_paste_input(ctx, wrap_width, now);
}

fn finalize_pending_paste_input(ctx: &mut PromptPasteCtx<'_>, wrap_width: usize, now: Instant) {
    flush_draft_if_any(ctx.value, ctx.draft, ctx.burst_since_sync);

    let Some(run) = ctx.pending.take() else {
        return;
    };

    let mut text = ctx.value.read().clone();
    let notify = ctx.paste_guard.is_paste_active(now) || should_collapse_paste(run.slice(&text));
    let mut cursor = ctx.cursor_offset.get().min(text.len());
    let before = text.clone();
    let cursor_before = cursor;
    *ctx.pending = finalize_pending_paste(Some(run), &mut text, &mut cursor, wrap_width, ctx.pastes, true);
    if text != before || cursor != cursor_before {
        commit_prompt_text(
            ctx.value,
            ctx.draft,
            ctx.burst_since_sync,
            text,
            ctx.value_probe.as_ref(),
        );
        ctx.cursor_offset.set(cursor);
    } else if notify {
        commit_prompt_text(
            ctx.value,
            ctx.draft,
            ctx.burst_since_sync,
            text,
            ctx.value_probe.as_ref(),
        );
    }
    if notify {
        ctx.paste_guard.release_paste_active();
    }
}

/// Returns `true` when the cursor sits on a trailing empty line created by a single newline.
fn is_on_trailing_empty_line(text: &str, cursor: usize) -> bool {
    let cursor = cursor.min(text.len());
    cursor > 0 && cursor == text.len() && text.as_bytes().get(cursor - 1) == Some(&b'\n')
}

/// Inserts a newline at the cursor and moves the cursor to the following line.
fn insert_newline_at_cursor(text: &str, cursor: usize) -> (String, usize) {
    let cursor = cursor.min(text.len());
    if is_on_trailing_empty_line(text, cursor) {
        return (text.to_string(), cursor);
    }

    let mut next = text.to_string();
    next.insert(cursor, '\n');
    (next, cursor + 1)
}

/// Visible height for the prompt field: grows with content up to [`MAX_INPUT_LINES`].
fn visual_line_count(value: &str, width: u16) -> u16 {
    if value.is_empty() {
        return MIN_INPUT_LINES;
    }

    let wrap_width = width.max(1).saturating_sub(1) as usize;
    let lines = logical_line_segments(value)
        .into_iter()
        .map(|line| wrapped_line_count(line, wrap_width))
        .sum::<u16>();

    lines.clamp(MIN_INPUT_LINES, MAX_INPUT_LINES)
}

/// Collapses consecutive trailing empty lines down to a single cursor row.
fn logical_line_segments(value: &str) -> Vec<&str> {
    let mut segments: Vec<&str> = value.split('\n').collect();
    while segments.len() > 1 && segments.last() == Some(&"") && segments[segments.len() - 2].is_empty() {
        segments.pop();
    }
    segments
}

fn wrapped_line_count(line: &str, wrap_width: usize) -> u16 {
    wrapped_row_count(line, wrap_width).max(1)
}

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

    #[test]
    fn insert_newline_at_cursor_from_middle_of_line() {
        let (text, cursor) = insert_newline_at_cursor("hello world", 5);
        assert_eq!(text, "hello\n world");
        assert_eq!(cursor, 6);
    }

    #[test]
    fn insert_newline_at_cursor_when_cursor_already_at_end() {
        let (text, cursor) = insert_newline_at_cursor("hello", 5);
        assert_eq!(text, "hello\n");
        assert_eq!(cursor, text.len());
    }

    #[test]
    fn insert_newline_on_trailing_empty_line_is_noop() {
        let (text, cursor) = insert_newline_at_cursor("a\n", 2);
        assert_eq!(text, "a\n");
        assert_eq!(cursor, 2);
    }

    #[test]
    fn visual_line_count_defaults_to_one() {
        assert_eq!(visual_line_count("", 40), 1);
        assert_eq!(visual_line_count("hello", 40), 1);
    }

    #[test]
    fn visual_line_count_grows_with_newlines() {
        assert_eq!(visual_line_count("a\nb", 40), 2);
        assert_eq!(visual_line_count("a\nb\nc", 40), 3);
    }

    #[test]
    fn visual_line_count_collapses_double_trailing_newlines() {
        assert_eq!(visual_line_count("a\n\n", 40), 2);
    }

    #[test]
    fn visual_line_count_wraps_long_lines() {
        assert_eq!(visual_line_count(&"a".repeat(20), 10), 3);
    }

    #[test]
    fn tab_inserts_when_prompt_has_content() {
        assert!(should_insert_tab_in_prompt("{\"name\"", false, false));
    }

    #[test]
    fn tab_cycles_only_on_empty_prompt() {
        assert!(should_cycle_agent_mode(
            "",
            false,
            false,
            KeyCode::Tab,
            KeyModifiers::empty()
        ));
        assert!(!should_cycle_agent_mode(
            "json",
            false,
            false,
            KeyCode::Tab,
            KeyModifiers::empty()
        ));
    }

    #[test]
    fn ctrl_tab_always_cycles_mode() {
        assert!(should_cycle_agent_mode(
            "json",
            true,
            true,
            KeyCode::Tab,
            KeyModifiers::CONTROL
        ));
    }

    #[test]
    fn pasted_newline_char_is_detected() {
        assert!(is_pasted_newline_char(KeyCode::Char('\n')));
        assert!(is_pasted_newline_char(KeyCode::Char('\r')));
        assert!(!is_pasted_newline_char(KeyCode::Char('a')));
    }

    #[test]
    fn visual_line_count_wraps_tab_indented_json() {
        let json = "{\n\t\"name\": \"elph\"\n}";
        assert!(visual_line_count(json, 20) >= 3);
    }

    #[test]
    fn visual_line_count_caps_at_five_lines() {
        let value = "line\n".repeat(7);
        assert_eq!(visual_line_count(value.trim_end(), 40), MAX_INPUT_LINES);
    }
}
