use std::collections::{HashSet, VecDeque};
use std::path::Path;
use std::time::Instant;

use ratatui::layout::Rect;
use ratatui::text::Line;

use crate::agent::{AgentEvent, QuestionResponder, TodoItem};
use crate::tui::theme::Theme;
use crate::tui::tools::{StreamSegment, ToolCallDisplay, ToolCategory, extract_tool_detail};
use crate::tui::widgets::{
    AgentSelector, CommandPalette, FilePicker, HelpPopup, MessageContextMenu, ModelSelector,
    SessionSelector, ThinkingLevel, ThinkingSelector,
};

pub struct ChatMessage {
    pub role: String,
    pub content: String,
    pub tool_calls: Vec<ToolCallDisplay>,
    pub thinking: Option<String>,
    pub model: Option<String>,
    /// Interleaved text and tool calls in display order. When Some, used for rendering; when None, fall back to content + tool_calls.
    pub segments: Option<Vec<StreamSegment>>,
}

pub struct TokenUsage {
    pub input_tokens: u32,
    pub output_tokens: u32,
    pub total_cost: f64,
}

impl Default for TokenUsage {
    fn default() -> Self {
        Self {
            input_tokens: 0,
            output_tokens: 0,
            total_cost: 0.0,
        }
    }
}

#[derive(Debug, Clone)]
pub struct PasteBlock {
    pub start: usize,
    pub end: usize,
    pub line_count: usize,
}

#[derive(Debug, Clone, PartialEq)]
pub enum ChipKind {
    File,
    Skill,
}

#[derive(Debug, Clone)]
pub struct InputChip {
    pub start: usize,
    pub end: usize,
    pub kind: ChipKind,
}

#[derive(Debug, Clone)]
pub struct ImageAttachment {
    pub path: String,
    pub media_type: String,
    pub data: String,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum StatusLevel {
    Error,
    Info,
    Success,
}

pub struct StatusMessage {
    pub text: String,
    pub level: StatusLevel,
    pub created: Instant,
}

impl StatusMessage {
    pub fn error(text: impl Into<String>) -> Self {
        Self {
            text: text.into(),
            level: StatusLevel::Error,
            created: Instant::now(),
        }
    }

    pub fn info(text: impl Into<String>) -> Self {
        Self {
            text: text.into(),
            level: StatusLevel::Info,
            created: Instant::now(),
        }
    }

    pub fn success(text: impl Into<String>) -> Self {
        Self {
            text: text.into(),
            level: StatusLevel::Success,
            created: Instant::now(),
        }
    }

    pub fn expired(&self) -> bool {
        let ttl = match self.level {
            StatusLevel::Error => std::time::Duration::from_secs(8),
            StatusLevel::Info => std::time::Duration::from_secs(3),
            StatusLevel::Success => std::time::Duration::from_secs(4),
        };
        self.created.elapsed() > ttl
    }
}

const IMAGE_EXTENSIONS: &[&str] = &["png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"];

#[derive(Default)]
pub struct TextSelection {
    pub anchor: Option<(u16, u16)>,
    pub end: Option<(u16, u16)>,
    pub active: bool,
}

impl TextSelection {
    pub fn start(&mut self, col: u16, visual_row: u16) {
        self.anchor = Some((col, visual_row));
        self.end = Some((col, visual_row));
        self.active = true;
    }

    pub fn update(&mut self, col: u16, visual_row: u16) {
        self.end = Some((col, visual_row));
    }

    pub fn clear(&mut self) {
        self.anchor = None;
        self.end = None;
        self.active = false;
    }

    pub fn ordered(&self) -> Option<((u16, u16), (u16, u16))> {
        let a = self.anchor?;
        let e = self.end?;
        if a.1 < e.1 || (a.1 == e.1 && a.0 <= e.0) {
            Some((a, e))
        } else {
            Some((e, a))
        }
    }

    pub fn is_empty_selection(&self) -> bool {
        match (self.anchor, self.end) {
            (Some(a), Some(e)) => a == e,
            _ => true,
        }
    }
}

pub fn media_type_for_path(path: &str) -> Option<String> {
    let ext = Path::new(path).extension()?.to_str()?.to_lowercase();
    match ext.as_str() {
        "png" => Some("image/png".into()),
        "jpg" | "jpeg" => Some("image/jpeg".into()),
        "gif" => Some("image/gif".into()),
        "webp" => Some("image/webp".into()),
        "bmp" => Some("image/bmp".into()),
        "svg" => Some("image/svg+xml".into()),
        _ => None,
    }
}

pub fn is_image_path(path: &str) -> bool {
    Path::new(path)
        .extension()
        .and_then(|e| e.to_str())
        .map(|e| IMAGE_EXTENSIONS.contains(&e.to_lowercase().as_str()))
        .unwrap_or(false)
}

pub const PASTE_COLLAPSE_THRESHOLD: usize = 5;

#[derive(Debug)]
pub struct PendingQuestion {
    pub question: String,
    pub options: Vec<String>,
    pub selected: usize,
    pub custom_input: String,
    pub responder: Option<QuestionResponder>,
}

pub struct SubagentState {
    pub id: String,
    pub description: String,
    pub output: String,
    pub current_tool: Option<String>,
    pub current_tool_detail: Option<String>,
    pub tools_completed: usize,
    pub background: bool,
}

pub struct BackgroundSubagentInfo {
    pub id: String,
    pub description: String,
    pub output: String,
    pub tools_completed: usize,
    pub done: bool,
}

#[derive(Debug)]
pub struct PendingPermission {
    pub tool_name: String,
    pub input_summary: String,
    pub selected: usize,
    pub responder: Option<QuestionResponder>,
}

pub struct QueuedMessage {
    pub text: String,
    pub images: Vec<(String, String)>,
}

#[derive(PartialEq, Clone, Copy)]
pub enum AppMode {
    Normal,
    Insert,
}

#[derive(Default)]
pub struct LayoutRects {
    pub header: Rect,
    pub messages: Rect,
    pub input: Rect,
    pub status: Rect,
    pub model_selector: Option<Rect>,
    pub agent_selector: Option<Rect>,
    pub command_palette: Option<Rect>,
    pub thinking_selector: Option<Rect>,
    pub session_selector: Option<Rect>,
    pub help_popup: Option<Rect>,
    pub context_menu: Option<Rect>,
    pub question_popup: Option<Rect>,
    pub permission_popup: Option<Rect>,
    pub file_picker: Option<Rect>,
}

pub struct RenderCache {
    pub lines: Vec<Line<'static>>,
    pub line_to_msg: Vec<usize>,
    pub line_to_tool: Vec<Option<(usize, usize)>>,
    pub total_visual: u32,
    pub width: u16,
}

pub struct App {
    pub messages: Vec<ChatMessage>,
    pub input: String,
    pub cursor_pos: usize,
    pub scroll_offset: u16,
    pub max_scroll: u16,
    pub is_streaming: bool,
    pub current_response: String,
    pub current_thinking: String,
    pub should_quit: bool,
    pub mode: AppMode,
    pub usage: TokenUsage,
    pub model_name: String,
    pub provider_name: String,
    pub agent_name: String,
    pub theme: Theme,
    pub tick_count: u64,
    pub layout: LayoutRects,

    pub pending_tool_name: Option<String>,
    pub pending_tool_input: String,
    pub current_tool_calls: Vec<ToolCallDisplay>,
    pub streaming_segments: Vec<StreamSegment>,
    pub status_message: Option<StatusMessage>,
    pub model_selector: ModelSelector,
    pub agent_selector: AgentSelector,
    pub command_palette: CommandPalette,
    pub thinking_selector: ThinkingSelector,
    pub session_selector: SessionSelector,
    pub help_popup: HelpPopup,
    pub streaming_started: Option<Instant>,

    pub thinking_expanded: bool,
    pub thinking_budget: u32,
    pub last_escape_time: Option<Instant>,
    pub follow_bottom: bool,

    pub paste_blocks: Vec<PasteBlock>,
    pub attachments: Vec<ImageAttachment>,
    pub conversation_title: Option<String>,
    pub vim_mode: bool,

    pub selection: TextSelection,
    pub visual_lines: Vec<String>,
    pub content_width: u16,

    pub context_window: u32,
    pub last_input_tokens: u32,

    pub esc_hint_until: Option<Instant>,
    pub todos: Vec<TodoItem>,
    pub message_line_map: Vec<usize>,
    pub tool_line_map: Vec<Option<(usize, usize)>>,
    pub expanded_tool_calls: HashSet<(usize, usize)>,
    pub context_menu: MessageContextMenu,
    pub pending_question: Option<PendingQuestion>,
    pub pending_permission: Option<PendingPermission>,
    pub message_queue: VecDeque<QueuedMessage>,
    pub history: Vec<String>,
    pub history_index: Option<usize>,
    pub history_draft: String,
    pub skill_entries: Vec<(String, String)>,
    pub custom_command_names: Vec<String>,
    pub rename_input: String,
    pub rename_visible: bool,
    pub favorite_models: Vec<String>,
    pub file_picker: FilePicker,
    pub chips: Vec<InputChip>,
    pub active_subagent: Option<SubagentState>,
    pub background_subagents: Vec<BackgroundSubagentInfo>,

    pub render_dirty: bool,
    pub render_cache: Option<RenderCache>,
}
impl App {
    pub fn new(
        model_name: String,
        provider_name: String,
        agent_name: String,
        theme_name: &str,
        vim_mode: bool,
    ) -> Self {
        Self {
            messages: Vec::new(),
            input: String::new(),
            cursor_pos: 0,
            scroll_offset: 0,
            max_scroll: 0,
            is_streaming: false,
            current_response: String::new(),
            current_thinking: String::new(),
            should_quit: false,
            mode: AppMode::Insert,
            usage: TokenUsage::default(),
            model_name,
            provider_name,
            agent_name,
            theme: Theme::from_config(theme_name),
            tick_count: 0,
            layout: LayoutRects::default(),
            pending_tool_name: None,
            pending_tool_input: String::new(),
            current_tool_calls: Vec::new(),
            streaming_segments: Vec::new(),
            status_message: None,
            model_selector: ModelSelector::new(),
            agent_selector: AgentSelector::new(),
            command_palette: CommandPalette::new(),
            thinking_selector: ThinkingSelector::new(),
            session_selector: SessionSelector::new(),
            help_popup: HelpPopup::new(),
            streaming_started: None,
            thinking_expanded: false,
            thinking_budget: 0,
            last_escape_time: None,
            follow_bottom: true,
            paste_blocks: Vec::new(),
            attachments: Vec::new(),
            conversation_title: None,
            vim_mode,
            selection: TextSelection::default(),
            visual_lines: Vec::new(),
            content_width: 0,
            context_window: 0,
            last_input_tokens: 0,
            esc_hint_until: None,
            todos: Vec::new(),
            message_line_map: Vec::new(),
            tool_line_map: Vec::new(),
            expanded_tool_calls: HashSet::new(),
            context_menu: MessageContextMenu::new(),
            pending_question: None,
            pending_permission: None,
            message_queue: VecDeque::new(),
            history: Vec::new(),
            history_index: None,
            history_draft: String::new(),
            skill_entries: Vec::new(),
            custom_command_names: Vec::new(),
            rename_input: String::new(),
            rename_visible: false,
            favorite_models: Vec::new(),
            file_picker: FilePicker::new(),
            chips: Vec::new(),
            active_subagent: None,
            background_subagents: Vec::new(),
            render_dirty: true,
            render_cache: None,
        }
    }

    pub fn mark_dirty(&mut self) {
        self.render_dirty = true;
    }

    pub fn streaming_elapsed_secs(&self) -> Option<f64> {
        self.streaming_started
            .map(|start| start.elapsed().as_secs_f64())
    }

    pub fn thinking_level(&self) -> ThinkingLevel {
        ThinkingLevel::from_budget(self.thinking_budget)
    }

    pub fn handle_agent_event(&mut self, event: AgentEvent) {
        match event {
            AgentEvent::TextDelta(text) => {
                self.current_response.push_str(&text);
            }
            AgentEvent::ThinkingDelta(text) => {
                self.current_thinking.push_str(&text);
            }
            AgentEvent::TextComplete(text) => {
                if !text.is_empty()
                    || !self.current_response.is_empty()
                    || !self.streaming_segments.is_empty()
                {
                    if !self.current_response.is_empty() {
                        self.streaming_segments
                            .push(StreamSegment::Text(std::mem::take(
                                &mut self.current_response,
                            )));
                    }
                    let content: String = self
                        .streaming_segments
                        .iter()
                        .filter_map(|s| {
                            if let StreamSegment::Text(t) = s {
                                Some(t.as_str())
                            } else {
                                None
                            }
                        })
                        .collect();
                    let content = if content.is_empty() {
                        text.clone()
                    } else {
                        content
                    };
                    let thinking = if self.current_thinking.is_empty() {
                        None
                    } else {
                        Some(self.current_thinking.clone())
                    };
                    self.messages.push(ChatMessage {
                        role: "assistant".to_string(),
                        content,
                        tool_calls: std::mem::take(&mut self.current_tool_calls),
                        thinking,
                        model: Some(self.model_name.clone()),
                        segments: Some(std::mem::take(&mut self.streaming_segments)),
                    });
                }
                self.current_response.clear();
                self.current_thinking.clear();
                self.streaming_segments.clear();
                self.is_streaming = false;
                self.streaming_started = None;
                self.scroll_to_bottom();
            }
            AgentEvent::ToolCallStart { name, .. } => {
                self.pending_tool_name = Some(name);
                self.pending_tool_input.clear();
            }
            AgentEvent::ToolCallInputDelta(delta) => {
                self.pending_tool_input.push_str(&delta);
            }
            AgentEvent::ToolCallExecuting { name, input, .. } => {
                self.pending_tool_name = Some(name.clone());
                self.pending_tool_input = input;
            }
            AgentEvent::ToolCallResult {
                name,
                output,
                is_error,
                ..
            } => {
                if !self.current_response.is_empty() {
                    self.streaming_segments
                        .push(StreamSegment::Text(std::mem::take(
                            &mut self.current_response,
                        )));
                }
                let input = std::mem::take(&mut self.pending_tool_input);
                let category = ToolCategory::from_name(&name);
                let detail = extract_tool_detail(&name, &input);
                let display = ToolCallDisplay {
                    name: name.clone(),
                    input,
                    output: Some(output),
                    is_error,
                    category,
                    detail,
                };
                self.current_tool_calls.push(display.clone());
                self.streaming_segments
                    .push(StreamSegment::ToolCall(display));
                self.pending_tool_name = None;
            }
            AgentEvent::Done { usage } => {
                self.is_streaming = false;
                self.streaming_started = None;
                self.last_input_tokens = usage.input_tokens;
                self.usage.input_tokens += usage.input_tokens;
                self.usage.output_tokens += usage.output_tokens;
                self.scroll_to_bottom();
            }
            AgentEvent::Error(msg) => {
                self.is_streaming = false;
                self.streaming_started = None;
                self.status_message = Some(StatusMessage::error(msg));
            }
            AgentEvent::Compacting => {
                self.messages.push(ChatMessage {
                    role: "compact".to_string(),
                    content: "\u{26a1} context compacted".to_string(),
                    tool_calls: Vec::new(),
                    thinking: None,
                    model: None,
                    segments: None,
                });
            }
            AgentEvent::TitleGenerated(title) => {
                self.conversation_title = Some(title);
            }
            AgentEvent::Compacted { messages_removed } => {
                if let Some(last) = self.messages.last_mut()
                    && last.role == "compact"
                {
                    last.content = format!(
                        "\u{26a1} compacted \u{2014} {} messages summarized",
                        messages_removed
                    );
                }
            }
            AgentEvent::TodoUpdate(items) => {
                self.todos = items;
            }
            AgentEvent::Question {
                question,
                options,
                responder,
                ..
            } => {
                self.pending_question = Some(PendingQuestion {
                    question,
                    options,
                    selected: 0,
                    custom_input: String::new(),
                    responder: Some(responder),
                });
            }
            AgentEvent::PermissionRequest {
                tool_name,
                input_summary,
                responder,
            } => {
                self.pending_permission = Some(PendingPermission {
                    tool_name,
                    input_summary,
                    selected: 0,
                    responder: Some(responder),
                });
            }
            AgentEvent::SubagentStart {
                id,
                description,
                background,
            } => {
                if background {
                    self.background_subagents.push(BackgroundSubagentInfo {
                        id,
                        description,
                        output: String::new(),
                        tools_completed: 0,
                        done: false,
                    });
                } else {
                    self.active_subagent = Some(SubagentState {
                        id,
                        description,
                        output: String::new(),
                        current_tool: None,
                        current_tool_detail: None,
                        tools_completed: 0,
                        background: false,
                    });
                }
            }
            AgentEvent::SubagentDelta { id, text } => {
                if let Some(ref mut state) = self.active_subagent
                    && state.id == id
                {
                    state.output.push_str(&text);
                } else if let Some(bg) = self.background_subagents.iter_mut().find(|b| b.id == id) {
                    bg.output.push_str(&text);
                }
            }
            AgentEvent::SubagentToolStart {
                id,
                tool_name,
                detail,
            } => {
                if let Some(ref mut state) = self.active_subagent
                    && state.id == id
                {
                    state.current_tool = Some(tool_name);
                    state.current_tool_detail = Some(detail);
                }
            }
            AgentEvent::SubagentToolComplete { id, .. } => {
                if let Some(ref mut state) = self.active_subagent
                    && state.id == id
                {
                    state.current_tool = None;
                    state.current_tool_detail = None;
                    state.tools_completed += 1;
                } else if let Some(bg) = self.background_subagents.iter_mut().find(|b| b.id == id) {
                    bg.tools_completed += 1;
                }
            }
            AgentEvent::SubagentComplete { id, .. } => {
                if self.active_subagent.as_ref().is_some_and(|s| s.id == id) {
                    self.active_subagent = None;
                }
            }
            AgentEvent::SubagentBackgroundDone {
                id, description, ..
            } => {
                if let Some(bg) = self.background_subagents.iter_mut().find(|b| b.id == id) {
                    bg.done = true;
                }
                self.status_message = Some(StatusMessage::success(format!(
                    "Background subagent done: {}",
                    description
                )));
            }
            AgentEvent::MemoryExtracted {
                added,
                updated,
                deleted,
            } => {
                let parts: Vec<String> = [
                    (added > 0).then(|| format!("+{added}")),
                    (updated > 0).then(|| format!("~{updated}")),
                    (deleted > 0).then(|| format!("-{deleted}")),
                ]
                .into_iter()
                .flatten()
                .collect();
                if !parts.is_empty() {
                    self.status_message = Some(StatusMessage::success(format!(
                        "memory {}",
                        parts.join(" ")
                    )));
                }
            }
        }
        self.mark_dirty();
    }

    pub fn take_input(&mut self) -> Option<String> {
        let trimmed = self.input.trim().to_string();
        if trimmed.is_empty() && self.attachments.is_empty() {
            return None;
        }
        let display = if self.attachments.is_empty() {
            trimmed.clone()
        } else {
            let att_names: Vec<String> = self
                .attachments
                .iter()
                .map(|a| {
                    Path::new(&a.path)
                        .file_name()
                        .map(|f| f.to_string_lossy().to_string())
                        .unwrap_or_else(|| a.path.clone())
                })
                .collect();
            if trimmed.is_empty() {
                format!("[{}]", att_names.join(", "))
            } else {
                format!("{} [{}]", trimmed, att_names.join(", "))
            }
        };
        self.messages.push(ChatMessage {
            role: "user".to_string(),
            content: display,
            tool_calls: Vec::new(),
            thinking: None,
            model: None,
            segments: None,
        });
        self.input.clear();
        self.cursor_pos = 0;
        self.paste_blocks.clear();
        self.chips.clear();
        self.history.push(trimmed.clone());
        self.history_index = None;
        self.history_draft.clear();
        self.is_streaming = true;
        self.streaming_started = Some(Instant::now());
        self.current_response.clear();
        self.current_thinking.clear();
        self.current_tool_calls.clear();
        self.streaming_segments.clear();
        self.status_message = None;
        self.scroll_to_bottom();
        self.mark_dirty();
        Some(trimmed)
    }

    pub fn take_attachments(&mut self) -> Vec<ImageAttachment> {
        std::mem::take(&mut self.attachments)
    }

    pub fn queue_input(&mut self) -> bool {
        let trimmed = self.input.trim().to_string();
        if trimmed.is_empty() && self.attachments.is_empty() {
            return false;
        }
        let display = if self.attachments.is_empty() {
            trimmed.clone()
        } else {
            let names: Vec<String> = self
                .attachments
                .iter()
                .map(|a| {
                    Path::new(&a.path)
                        .file_name()
                        .map(|f| f.to_string_lossy().to_string())
                        .unwrap_or_else(|| a.path.clone())
                })
                .collect();
            if trimmed.is_empty() {
                format!("[{}]", names.join(", "))
            } else {
                format!("{} [{}]", trimmed, names.join(", "))
            }
        };
        self.messages.push(ChatMessage {
            role: "user".to_string(),
            content: display,
            tool_calls: Vec::new(),
            thinking: None,
            model: None,
            segments: None,
        });
        let images: Vec<(String, String)> = self
            .attachments
            .drain(..)
            .map(|a| (a.media_type, a.data))
            .collect();
        self.history.push(trimmed.clone());
        self.history_index = None;
        self.history_draft.clear();
        self.message_queue.push_back(QueuedMessage {
            text: trimmed,
            images,
        });
        self.input.clear();
        self.cursor_pos = 0;
        self.paste_blocks.clear();
        self.chips.clear();
        self.scroll_to_bottom();
        self.mark_dirty();
        true
    }

    pub fn input_height(&self, width: u16) -> u16 {
        if self.is_streaming && self.input.is_empty() && self.attachments.is_empty() {
            return 3;
        }
        let w = width as usize;
        if w < 4 {
            return 3;
        }
        let has_input = !self.input.is_empty() || !self.attachments.is_empty();
        if !has_input {
            return 3;
        }
        let mut visual = 0usize;
        if !self.attachments.is_empty() {
            visual += 1;
        }
        let display = self.display_input();
        if display.is_empty() {
            if self.attachments.is_empty() {
                visual += 1;
            }
        } else {
            for line in display.split('\n') {
                let total = 2 + line.chars().count();
                visual += if total == 0 {
                    1
                } else {
                    total.div_ceil(w).max(1)
                };
            }
        }
        (visual as u16 + 1).clamp(3, 12)
    }

    pub fn handle_paste(&mut self, text: String) {
        let line_count = text.lines().count();
        let start = self.cursor_pos;
        let len = text.len();
        self.input.insert_str(start, &text);
        self.adjust_chips(start, 0, len);
        self.cursor_pos = start + len;
        if line_count >= PASTE_COLLAPSE_THRESHOLD {
            self.paste_blocks.push(PasteBlock {
                start,
                end: start + len,
                line_count,
            });
        }
    }

    pub fn paste_block_at_cursor(&self) -> Option<usize> {
        self.paste_blocks
            .iter()
            .position(|pb| self.cursor_pos > pb.start && self.cursor_pos <= pb.end)
    }

    pub fn delete_paste_block(&mut self, idx: usize) {
        let pb = self.paste_blocks.remove(idx);
        let len = pb.end - pb.start;
        self.input.replace_range(pb.start..pb.end, "");
        self.cursor_pos = pb.start;
        for remaining in &mut self.paste_blocks {
            if remaining.start >= pb.end {
                remaining.start -= len;
                remaining.end -= len;
            }
        }
    }

    pub fn chip_at_cursor(&self) -> Option<usize> {
        self.chips
            .iter()
            .position(|c| self.cursor_pos > c.start && self.cursor_pos <= c.end)
    }

    pub fn delete_chip(&mut self, idx: usize) {
        let chip = self.chips.remove(idx);
        let len = chip.end - chip.start;
        self.input.replace_range(chip.start..chip.end, "");
        self.cursor_pos = chip.start;
        self.adjust_chips(chip.start, len, 0);
    }

    pub fn adjust_chips(&mut self, edit_start: usize, old_len: usize, new_len: usize) {
        let edit_end = edit_start + old_len;
        let delta = new_len as isize - old_len as isize;
        self.chips.retain_mut(|c| {
            if c.start >= edit_end {
                c.start = (c.start as isize + delta) as usize;
                c.end = (c.end as isize + delta) as usize;
                true
            } else {
                c.end <= edit_start
            }
        });
    }

    pub fn add_image_attachment(&mut self, path: &str) -> Result<(), String> {
        let resolved = if path.starts_with('~') {
            if let Ok(home) = std::env::var("HOME") {
                path.replacen('~', &home, 1)
            } else {
                path.to_string()
            }
        } else {
            path.to_string()
        };

        let fs_path = Path::new(&resolved);
        if !fs_path.exists() {
            return Err(format!("file not found: {}", path));
        }

        let media_type = media_type_for_path(&resolved)
            .ok_or_else(|| format!("unsupported image format: {}", path))?;

        let data = std::fs::read(fs_path).map_err(|e| format!("failed to read {}: {}", path, e))?;
        let encoded = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &data);

        if self.attachments.iter().any(|a| a.path == resolved) {
            return Ok(());
        }

        self.attachments.push(ImageAttachment {
            path: resolved,
            media_type,
            data: encoded,
        });
        Ok(())
    }

    pub fn display_input(&self) -> String {
        if self.paste_blocks.is_empty() {
            return self.input.clone();
        }
        let mut result = String::new();
        let mut pos = 0;
        let mut sorted_blocks: Vec<&PasteBlock> = self.paste_blocks.iter().collect();
        sorted_blocks.sort_by_key(|pb| pb.start);
        for pb in sorted_blocks {
            if pb.start > pos {
                result.push_str(&self.input[pos..pb.start]);
            }
            result.push_str(&format!("[pasted {} lines]", pb.line_count));
            pos = pb.end;
        }
        if pos < self.input.len() {
            result.push_str(&self.input[pos..]);
        }
        result
    }

    pub fn scroll_up(&mut self, n: u16) {
        self.follow_bottom = false;
        self.scroll_offset = self.scroll_offset.saturating_sub(n);
    }

    pub fn scroll_down(&mut self, n: u16) {
        self.scroll_offset = self.scroll_offset.saturating_add(n).min(self.max_scroll);
        if self.scroll_offset >= self.max_scroll {
            self.follow_bottom = true;
        }
    }

    pub fn scroll_to_top(&mut self) {
        self.follow_bottom = false;
        self.scroll_offset = 0;
    }

    pub fn scroll_to_bottom(&mut self) {
        self.follow_bottom = true;
        self.scroll_offset = self.max_scroll;
    }

    pub fn clear_conversation(&mut self) {
        self.messages.clear();
        self.current_response.clear();
        self.current_thinking.clear();
        self.current_tool_calls.clear();
        self.streaming_segments.clear();
        self.scroll_offset = 0;
        self.max_scroll = 0;
        self.follow_bottom = true;
        self.usage = TokenUsage::default();
        self.last_input_tokens = 0;
        self.status_message = None;
        self.paste_blocks.clear();
        self.chips.clear();
        self.attachments.clear();
        self.conversation_title = None;
        self.selection.clear();
        self.visual_lines.clear();
        self.todos.clear();
        self.message_line_map.clear();
        self.tool_line_map.clear();
        self.expanded_tool_calls.clear();
        self.esc_hint_until = None;
        self.context_menu.close();
        self.pending_question = None;
        self.pending_permission = None;
        self.active_subagent = None;
        self.background_subagents.clear();
        self.message_queue.clear();
        self.render_cache = None;
        self.mark_dirty();
    }

    pub fn insert_char(&mut self, c: char) {
        let pos = self.cursor_pos;
        self.input.insert(pos, c);
        let len = c.len_utf8();
        self.adjust_chips(pos, 0, len);
        self.cursor_pos += len;
    }

    pub fn delete_char_before(&mut self) {
        if self.cursor_pos > 0 {
            let prev = self.input[..self.cursor_pos]
                .chars()
                .last()
                .map(|c| c.len_utf8())
                .unwrap_or(0);
            self.cursor_pos -= prev;
            self.input.remove(self.cursor_pos);
            self.adjust_chips(self.cursor_pos, prev, 0);
        }
    }

    pub fn move_cursor_left(&mut self) {
        if self.cursor_pos > 0 {
            let prev = self.input[..self.cursor_pos]
                .chars()
                .last()
                .map(|c| c.len_utf8())
                .unwrap_or(0);
            self.cursor_pos -= prev;
        }
    }

    pub fn move_cursor_right(&mut self) {
        if self.cursor_pos < self.input.len() {
            let next = self.input[self.cursor_pos..]
                .chars()
                .next()
                .map(|c| c.len_utf8())
                .unwrap_or(0);
            self.cursor_pos += next;
        }
    }

    pub fn move_cursor_home(&mut self) {
        self.cursor_pos = 0;
    }

    pub fn move_cursor_end(&mut self) {
        self.cursor_pos = self.input.len();
    }

    pub fn delete_word_before(&mut self) {
        if self.cursor_pos == 0 {
            return;
        }
        let before = &self.input[..self.cursor_pos];
        let trimmed = before.trim_end();
        let new_end = if trimmed.is_empty() {
            0
        } else if let Some(pos) = trimmed.rfind(|c: char| c.is_whitespace()) {
            pos + trimmed[pos..]
                .chars()
                .next()
                .map(|c| c.len_utf8())
                .unwrap_or(1)
        } else {
            0
        };
        let old_len = self.cursor_pos - new_end;
        self.input.replace_range(new_end..self.cursor_pos, "");
        self.adjust_chips(new_end, old_len, 0);
        self.cursor_pos = new_end;
    }

    pub fn delete_to_end(&mut self) {
        let old_len = self.input.len() - self.cursor_pos;
        self.input.truncate(self.cursor_pos);
        self.adjust_chips(self.cursor_pos, old_len, 0);
    }

    pub fn delete_to_start(&mut self) {
        let old_len = self.cursor_pos;
        self.input.replace_range(..self.cursor_pos, "");
        self.adjust_chips(0, old_len, 0);
        self.cursor_pos = 0;
    }

    pub fn extract_selected_text(&self) -> Option<String> {
        let ((sc, sr), (ec, er)) = self.selection.ordered()?;
        if self.visual_lines.is_empty() || self.content_width == 0 {
            return None;
        }
        let mut text = String::new();
        for row in sr..=er {
            if row as usize >= self.visual_lines.len() {
                break;
            }
            let line = &self.visual_lines[row as usize];
            let chars: Vec<char> = line.chars().collect();
            let start_col = if row == sr {
                (sc as usize).min(chars.len())
            } else {
                0
            };
            let end_col = if row == er {
                (ec as usize).min(chars.len())
            } else {
                chars.len()
            };
            if start_col <= end_col {
                let s = start_col.min(chars.len());
                let e = end_col.min(chars.len());
                text.extend(&chars[s..e]);
            }
            if row < er {
                text.push('\n');
            }
        }
        Some(text)
    }

    pub fn move_cursor_up(&mut self) -> bool {
        let before = &self.input[..self.cursor_pos];
        let line_start = before.rfind('\n').map(|p| p + 1).unwrap_or(0);
        if line_start == 0 {
            return false;
        }
        let col = before[line_start..].chars().count();
        let prev_end = line_start - 1;
        let prev_start = self.input[..prev_end]
            .rfind('\n')
            .map(|p| p + 1)
            .unwrap_or(0);
        let prev_line = &self.input[prev_start..prev_end];
        let target_col = col.min(prev_line.chars().count());
        let offset: usize = prev_line
            .chars()
            .take(target_col)
            .map(|c| c.len_utf8())
            .sum();
        self.cursor_pos = prev_start + offset;
        true
    }

    pub fn move_cursor_down(&mut self) -> bool {
        let after = &self.input[self.cursor_pos..];
        let next_nl = after.find('\n');
        let Some(nl_offset) = next_nl else {
            return false;
        };
        let before = &self.input[..self.cursor_pos];
        let line_start = before.rfind('\n').map(|p| p + 1).unwrap_or(0);
        let col = before[line_start..].chars().count();
        let next_start = self.cursor_pos + nl_offset + 1;
        let next_end = self.input[next_start..]
            .find('\n')
            .map(|p| next_start + p)
            .unwrap_or(self.input.len());
        let next_line = &self.input[next_start..next_end];
        let target_col = col.min(next_line.chars().count());
        let offset: usize = next_line
            .chars()
            .take(target_col)
            .map(|c| c.len_utf8())
            .sum();
        self.cursor_pos = next_start + offset;
        true
    }

    pub fn history_prev(&mut self) {
        if self.history.is_empty() {
            return;
        }
        match self.history_index {
            None => {
                self.history_draft = self.input.clone();
                self.history_index = Some(self.history.len() - 1);
            }
            Some(0) => return,
            Some(i) => {
                self.history_index = Some(i - 1);
            }
        }
        self.input = self.history[self.history_index.unwrap()].clone();
        self.cursor_pos = self.input.len();
        self.paste_blocks.clear();
        self.chips.clear();
    }

    pub fn history_next(&mut self) {
        let Some(idx) = self.history_index else {
            return;
        };
        if idx + 1 >= self.history.len() {
            self.history_index = None;
            self.input = std::mem::take(&mut self.history_draft);
        } else {
            self.history_index = Some(idx + 1);
            self.input = self.history[idx + 1].clone();
        }
        self.cursor_pos = self.input.len();
        self.paste_blocks.clear();
        self.chips.clear();
    }
}

pub fn copy_to_clipboard(text: &str) {
    let encoded =
        base64::Engine::encode(&base64::engine::general_purpose::STANDARD, text.as_bytes());
    let osc = format!("\x1b]52;c;{}\x07", encoded);
    let _ = std::io::Write::write_all(&mut std::io::stderr(), osc.as_bytes());

    #[cfg(target_os = "macos")]
    {
        use std::process::{Command, Stdio};
        if let Ok(mut child) = Command::new("pbcopy").stdin(Stdio::piped()).spawn() {
            if let Some(ref mut stdin) = child.stdin {
                let _ = std::io::Write::write_all(stdin, text.as_bytes());
            }
            let _ = child.wait();
        }
    }

    #[cfg(target_os = "linux")]
    {
        use std::process::{Command, Stdio};
        let result = Command::new("xclip")
            .args(["-selection", "clipboard"])
            .stdin(Stdio::piped())
            .spawn();
        if let Ok(mut child) = result {
            if let Some(ref mut stdin) = child.stdin {
                let _ = std::io::Write::write_all(stdin, text.as_bytes());
            }
            let _ = child.wait();
        }
    }
}
