use kordophoned_client as daemon;

use anyhow::Result;
use crossterm::event::{Event as CEvent, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use ratatui::prelude::*;
use ratatui::widgets::*;
use std::sync::mpsc;
use std::time::{Duration, Instant};
use unicode_width::UnicodeWidthStr;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ViewMode {
    List,
    Chat,
    Split,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Focus {
    Navigation,
    Input,
}

struct AppState {
    conversations: Vec<daemon::ConversationSummary>,
    selected_idx: usize,
    selected_conversation_id: Option<String>,
    messages: Vec<daemon::ChatMessage>,
    active_conversation_id: Option<String>,
    active_conversation_title: String,
    status: String,
    input: String,
    focus: Focus,
    transcript_scroll: u16,
    pinned_to_bottom: bool,
    refresh_conversations_in_flight: bool,
    refresh_messages_in_flight: bool,
}

impl AppState {
    fn new() -> Self {
        Self {
            conversations: Vec::new(),
            selected_idx: 0,
            selected_conversation_id: None,
            messages: Vec::new(),
            active_conversation_id: None,
            active_conversation_title: String::new(),
            status: String::new(),
            input: String::new(),
            focus: Focus::Navigation,
            transcript_scroll: 0,
            pinned_to_bottom: true,
            refresh_conversations_in_flight: false,
            refresh_messages_in_flight: false,
        }
    }

    fn select_next(&mut self) {
        if self.conversations.is_empty() {
            self.selected_idx = 0;
            self.selected_conversation_id = None;
            return;
        }
        self.selected_idx = (self.selected_idx + 1).min(self.conversations.len() - 1);
        self.selected_conversation_id = self
            .conversations
            .get(self.selected_idx)
            .map(|c| c.id.clone());
    }

    fn select_prev(&mut self) {
        if self.conversations.is_empty() {
            self.selected_idx = 0;
            self.selected_conversation_id = None;
            return;
        }
        self.selected_idx = self.selected_idx.saturating_sub(1);
        self.selected_conversation_id = self
            .conversations
            .get(self.selected_idx)
            .map(|c| c.id.clone());
    }

    fn open_selected_conversation(&mut self) {
        if let Some(conv) = self.conversations.get(self.selected_idx) {
            self.active_conversation_id = Some(conv.id.clone());
            self.active_conversation_title = conv.title.clone();
            self.selected_conversation_id = Some(conv.id.clone());
            self.messages.clear();
            self.transcript_scroll = 0;
            self.pinned_to_bottom = true;
            self.focus = Focus::Input;
            self.status = "Loading…".to_string();
        }
    }
}

fn view_mode(width: u16, has_active_conversation: bool, requested: ViewMode) -> ViewMode {
    let min_conversations = 24u16;
    let min_chat = 44u16;
    let min_total = min_conversations + 1 + min_chat;
    if width >= min_total {
        return ViewMode::Split;
    }
    if has_active_conversation {
        requested
    } else {
        ViewMode::List
    }
}

fn ui(frame: &mut Frame, app: &AppState, requested_view: ViewMode) {
    let area = frame.area();
    let mode = view_mode(
        area.width,
        app.active_conversation_id.is_some(),
        requested_view,
    );

    let show_input =
        matches!(mode, ViewMode::Chat | ViewMode::Split) && app.active_conversation_id.is_some();
    let chunks = if show_input {
        Layout::default()
            .direction(Direction::Vertical)
            .constraints([
                Constraint::Min(1),
                Constraint::Length(3),
                Constraint::Length(1),
            ])
            .split(area)
    } else {
        Layout::default()
            .direction(Direction::Vertical)
            .constraints([Constraint::Min(1), Constraint::Length(1)])
            .split(area)
    };

    let (main_area, input_area, status_area) = if show_input {
        (chunks[0], Some(chunks[1]), chunks[2])
    } else {
        (chunks[0], None, chunks[1])
    };

    match mode {
        ViewMode::Split => {
            let left_width = (main_area.width / 3).clamp(24, 40);
            let cols = Layout::default()
                .direction(Direction::Horizontal)
                .constraints([Constraint::Length(left_width), Constraint::Min(1)])
                .split(main_area);
            render_conversations(frame, app, cols[0], true);
            render_transcript(frame, app, cols[1], true);
        }
        ViewMode::List => render_conversations(frame, app, main_area, false),
        ViewMode::Chat => render_transcript(frame, app, main_area, false),
    }

    if let Some(input_area) = input_area {
        let input_scroll_x = render_input(frame, app, input_area);
        if app.focus == Focus::Input {
            let cursor_col = visual_width_u16(app.input.as_str());
            let mut x = input_area
                .x
                .saturating_add(1)
                .saturating_add(cursor_col.saturating_sub(input_scroll_x));
            let max_x = input_area
                .x
                .saturating_add(input_area.width.saturating_sub(2));
            x = x.min(max_x);
            let y = input_area.y + 1;
            frame.set_cursor_position(Position { x, y });
        }
    }

    render_status(frame, app, status_area, mode);
}

fn render_conversations(frame: &mut Frame, app: &AppState, area: Rect, _in_split: bool) {
    let title = "Kordophone";
    let items = app
        .conversations
        .iter()
        .map(|c| {
            let is_active = app.active_conversation_id.as_deref() == Some(c.id.as_str());
            let header = Line::from(vec![
                Span::styled(
                    if c.unread_count > 0 { "● " } else { "  " },
                    Style::default()
                        .fg(if c.unread_count > 0 {
                            Color::LightYellow
                        } else {
                            Color::DarkGray
                        })
                        .add_modifier(Modifier::BOLD),
                ),
                Span::styled(
                    c.title.clone(),
                    Style::default().add_modifier(Modifier::BOLD),
                ),
            ]);
            let preview = Line::from(Span::styled(
                c.preview.clone(),
                Style::default().fg(if is_active {
                    Color::Gray
                } else {
                    Color::DarkGray
                }),
            ));
            ListItem::new(vec![header, preview]).style(if is_active {
                Style::default().bg(Color::DarkGray)
            } else {
                Style::default()
            })
        })
        .collect::<Vec<_>>();

    let mut state = ListState::default();
    state.select(if app.conversations.is_empty() {
        None
    } else {
        Some(app.selected_idx)
    });

    let list = List::new(items)
        .block(Block::default().borders(Borders::ALL).title(title))
        .highlight_style(
            Style::default()
                .bg(Color::Blue)
                .fg(Color::White)
                .add_modifier(Modifier::BOLD),
        )
        .highlight_symbol("▸ ");

    frame.render_stateful_widget(list, area, &mut state);
}

fn render_transcript(frame: &mut Frame, app: &AppState, area: Rect, in_split: bool) {
    let title = if let Some(_) = app.active_conversation_id {
        if in_split {
            format!("{} (Esc: nav, Tab: focus)", app.active_conversation_title)
        } else {
            format!("{} (Esc: back)", app.active_conversation_title)
        }
    } else {
        "Chat".to_string()
    };

    let lines = transcript_lines(&app.messages);

    let paragraph = Paragraph::new(Text::from(lines))
        .block(Block::default().borders(Borders::ALL).title(title))
        .wrap(Wrap { trim: false })
        .scroll((app.transcript_scroll, 0));

    frame.render_widget(paragraph, area);
}

fn render_input(frame: &mut Frame, app: &AppState, area: Rect) -> u16 {
    let title = if app.focus == Focus::Input {
        "Reply (Enter to send)"
    } else {
        "Reply (press i to type)"
    };

    let inner_width = area.width.saturating_sub(2).max(1);
    let cursor_col = visual_width_u16(app.input.as_str());
    let input_scroll_x = cursor_col.saturating_sub(inner_width.saturating_sub(1));

    let input = Paragraph::new(app.input.as_str())
        .block(Block::default().borders(Borders::ALL).title(title))
        .scroll((0, input_scroll_x));
    frame.render_widget(input, area);
    input_scroll_x
}

fn render_status(frame: &mut Frame, app: &AppState, area: Rect, mode: ViewMode) {
    let mut parts = vec![
        format!("{} convs", app.conversations.len()),
        match mode {
            ViewMode::Split => "split".to_string(),
            ViewMode::List => "list".to_string(),
            ViewMode::Chat => "chat".to_string(),
        },
    ];
    if !app.status.trim().is_empty() {
        parts.push(app.status.clone());
    }
    let line = parts.join(" | ");
    frame.render_widget(
        Paragraph::new(line).block(Block::default().borders(Borders::TOP)),
        area,
    );
}

fn main() -> Result<()> {
    enable_raw_mode()?;
    let mut stdout = std::io::stdout();
    crossterm::execute!(
        stdout,
        crossterm::terminal::EnterAlternateScreen,
        crossterm::event::EnableMouseCapture
    )?;
    let backend = ratatui::backend::CrosstermBackend::new(stdout);
    let mut terminal = ratatui::Terminal::new(backend)?;

    let res = run_app(&mut terminal);

    disable_raw_mode()?;
    crossterm::execute!(
        terminal.backend_mut(),
        crossterm::event::DisableMouseCapture,
        crossterm::terminal::LeaveAlternateScreen
    )?;
    terminal.show_cursor()?;

    res
}

fn run_app(
    terminal: &mut ratatui::Terminal<ratatui::backend::CrosstermBackend<std::io::Stdout>>,
) -> Result<()> {
    let (request_tx, request_rx) = mpsc::channel::<daemon::Request>();
    let (event_tx, event_rx) = mpsc::channel::<daemon::Event>();
    let _worker = daemon::spawn_worker(request_rx, event_tx);

    let tick_rate = Duration::from_millis(150);
    let refresh_rate = Duration::from_secs(2);
    let mut last_tick = Instant::now();
    let mut last_refresh = Instant::now() - refresh_rate;

    let mut requested_view = ViewMode::List;
    let mut app = AppState::new();
    app.status = "Connecting…".to_string();
    request_tx.send(daemon::Request::RefreshConversations).ok();
    app.refresh_conversations_in_flight = true;

    loop {
        let size = terminal.size()?;

        while let Ok(evt) = event_rx.try_recv() {
            match evt {
                daemon::Event::Conversations(convs) => {
                    let keep_selected_id = app
                        .selected_conversation_id
                        .clone()
                        .or_else(|| app.active_conversation_id.clone());

                    app.refresh_conversations_in_flight = false;
                    app.status.clear();
                    app.conversations = convs;
                    if app.conversations.is_empty() {
                        app.selected_idx = 0;
                        app.selected_conversation_id = None;
                    } else if let Some(id) = keep_selected_id {
                        if let Some(idx) = app.conversations.iter().position(|c| c.id == id) {
                            app.selected_idx = idx;
                            app.selected_conversation_id = Some(id);
                        } else {
                            app.selected_idx = 0;
                            app.selected_conversation_id = Some(app.conversations[0].id.clone());
                        }
                    } else {
                        app.selected_idx = app.selected_idx.min(app.conversations.len() - 1);
                        app.selected_conversation_id =
                            Some(app.conversations[app.selected_idx].id.clone());
                    }
                }
                daemon::Event::Messages {
                    conversation_id,
                    messages,
                } => {
                    app.refresh_messages_in_flight = false;
                    if app.active_conversation_id.as_deref() == Some(conversation_id.as_str()) {
                        let was_pinned = app.pinned_to_bottom;
                        app.messages = messages;
                        app.pinned_to_bottom = was_pinned;
                    }
                }
                daemon::Event::MessageSent {
                    conversation_id,
                    outgoing_id,
                } => {
                    if app.active_conversation_id.as_deref() == Some(conversation_id.as_str()) {
                        app.status = outgoing_id
                            .as_deref()
                            .map(|id| format!("Sent ({id})"))
                            .unwrap_or_else(|| "Sent".to_string());
                        app.refresh_messages_in_flight = false;
                        request_tx
                            .send(daemon::Request::RefreshMessages { conversation_id })
                            .ok();
                        app.refresh_messages_in_flight = true;
                    }
                }
                daemon::Event::MarkedRead
                | daemon::Event::ConversationListSyncTriggered
                | daemon::Event::AllConversationsSyncTriggered
                | daemon::Event::AttachmentDownloadQueued { .. }
                | daemon::Event::AttachmentUploadQueued { .. }
                | daemon::Event::AttachmentUploaded { .. }
                | daemon::Event::AttachmentInfo { .. }
                | daemon::Event::SettingsLoaded(_)
                | daemon::Event::SettingsSaved => {}
                daemon::Event::ConversationSyncTriggered { conversation_id } => {
                    if app.active_conversation_id.as_deref() == Some(conversation_id.as_str()) {
                        app.status = "Syncing…".to_string();
                    }
                }
                daemon::Event::AttachmentDownloaded { .. } => {
                    if let Some(cid) = app.active_conversation_id.clone() {
                        if !app.refresh_messages_in_flight {
                            request_tx
                                .send(daemon::Request::RefreshMessages {
                                    conversation_id: cid,
                                })
                                .ok();
                            app.refresh_messages_in_flight = true;
                        }
                    }
                }
                daemon::Event::AttachmentDownloadFailed {
                    attachment_guid,
                    error,
                } => {
                    app.status = format!("Attachment {attachment_guid} failed: {error}");
                }
                daemon::Event::ConversationsUpdated => {
                    if !app.refresh_conversations_in_flight {
                        request_tx.send(daemon::Request::RefreshConversations).ok();
                        app.refresh_conversations_in_flight = true;
                    }
                    if let Some(cid) = app.active_conversation_id.clone() {
                        if !app.refresh_messages_in_flight {
                            request_tx
                                .send(daemon::Request::RefreshMessages {
                                    conversation_id: cid,
                                })
                                .ok();
                            app.refresh_messages_in_flight = true;
                        }
                    }
                }
                daemon::Event::MessagesUpdated { conversation_id } => {
                    if !app.refresh_conversations_in_flight {
                        request_tx.send(daemon::Request::RefreshConversations).ok();
                        app.refresh_conversations_in_flight = true;
                    }
                    if app.active_conversation_id.as_deref() == Some(conversation_id.as_str()) {
                        if !app.refresh_messages_in_flight {
                            request_tx
                                .send(daemon::Request::RefreshMessages { conversation_id })
                                .ok();
                            app.refresh_messages_in_flight = true;
                        }
                    }
                }
                daemon::Event::UpdateStreamReconnected => {
                    if !app.refresh_conversations_in_flight {
                        request_tx.send(daemon::Request::RefreshConversations).ok();
                        app.refresh_conversations_in_flight = true;
                    }
                    if let Some(cid) = app.active_conversation_id.clone() {
                        if !app.refresh_messages_in_flight {
                            request_tx
                                .send(daemon::Request::RefreshMessages {
                                    conversation_id: cid,
                                })
                                .ok();
                            app.refresh_messages_in_flight = true;
                        }
                    }
                }
                daemon::Event::Error(e) => {
                    app.refresh_conversations_in_flight = false;
                    app.refresh_messages_in_flight = false;
                    app.status = e;
                }
            }
        }

        apply_transcript_scroll_policy(&mut app, size, requested_view);
        terminal.draw(|f| ui(f, &app, requested_view))?;

        let timeout = tick_rate.saturating_sub(last_tick.elapsed());
        if crossterm::event::poll(timeout)? {
            if let CEvent::Key(key) = crossterm::event::read()? {
                if key.kind != KeyEventKind::Press {
                    continue;
                }

                let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
                match (key.code, ctrl) {
                    (KeyCode::Char('c'), true) => return Ok(()),
                    _ => {}
                }

                let screen_mode = view_mode(
                    size.width,
                    app.active_conversation_id.is_some(),
                    requested_view,
                );

                let max_scroll = max_transcript_scroll(&app, size, requested_view);

                match screen_mode {
                    ViewMode::List => match key.code {
                        KeyCode::Up => app.select_prev(),
                        KeyCode::Down => app.select_next(),
                        KeyCode::Enter => {
                            app.open_selected_conversation();
                            if app.active_conversation_id.is_some() {
                                requested_view = ViewMode::Chat;
                                if let Some(cid) = app.active_conversation_id.clone() {
                                    request_tx
                                        .send(daemon::Request::MarkRead {
                                            conversation_id: cid.clone(),
                                        })
                                        .ok();
                                    request_tx
                                        .send(daemon::Request::SyncConversation {
                                            conversation_id: cid.clone(),
                                        })
                                        .ok();
                                    request_tx
                                        .send(daemon::Request::RefreshMessages {
                                            conversation_id: cid,
                                        })
                                        .ok();
                                    app.refresh_messages_in_flight = true;
                                }
                            }
                        }
                        _ => {}
                    },
                    ViewMode::Chat => match key.code {
                        KeyCode::Esc => {
                            requested_view = ViewMode::List;
                            app.focus = Focus::Navigation;
                        }
                        KeyCode::Char('i') if app.focus != Focus::Input => app.focus = Focus::Input,
                        _ => {
                            handle_chat_keys(&mut app, &request_tx, key, max_scroll);
                        }
                    },
                    ViewMode::Split => match key.code {
                        KeyCode::Tab => {
                            app.focus = match app.focus {
                                Focus::Navigation => Focus::Input,
                                Focus::Input => Focus::Navigation,
                            }
                        }
                        KeyCode::Esc => app.focus = Focus::Navigation,
                        KeyCode::Char('i') if app.focus != Focus::Input => app.focus = Focus::Input,
                        KeyCode::Up => {
                            if app.focus == Focus::Navigation {
                                app.select_prev()
                            } else {
                                scroll_up(&mut app, 1);
                            }
                        }
                        KeyCode::Down => {
                            if app.focus == Focus::Navigation {
                                app.select_next()
                            } else {
                                scroll_down(&mut app, 1, max_scroll);
                            }
                        }
                        KeyCode::Enter => {
                            if app.focus == Focus::Navigation {
                                app.open_selected_conversation();
                                requested_view = ViewMode::Chat;
                                if let Some(cid) = app.active_conversation_id.clone() {
                                    request_tx
                                        .send(daemon::Request::MarkRead {
                                            conversation_id: cid.clone(),
                                        })
                                        .ok();
                                    request_tx
                                        .send(daemon::Request::SyncConversation {
                                            conversation_id: cid.clone(),
                                        })
                                        .ok();
                                    request_tx
                                        .send(daemon::Request::RefreshMessages {
                                            conversation_id: cid,
                                        })
                                        .ok();
                                    app.refresh_messages_in_flight = true;
                                }
                            } else {
                                handle_chat_keys(&mut app, &request_tx, key, max_scroll);
                            }
                        }
                        _ => handle_chat_keys(&mut app, &request_tx, key, max_scroll),
                    },
                }
            }
        }

        if last_refresh.elapsed() >= refresh_rate {
            if !app.refresh_conversations_in_flight {
                request_tx.send(daemon::Request::RefreshConversations).ok();
                app.refresh_conversations_in_flight = true;
            }

            if let Some(cid) = app.active_conversation_id.clone() {
                if !app.refresh_messages_in_flight {
                    request_tx
                        .send(daemon::Request::RefreshMessages {
                            conversation_id: cid,
                        })
                        .ok();
                    app.refresh_messages_in_flight = true;
                }
            }

            last_refresh = Instant::now();
        }

        if last_tick.elapsed() >= tick_rate {
            last_tick = Instant::now();
        }
    }
}

fn handle_chat_keys(
    app: &mut AppState,
    request_tx: &mpsc::Sender<daemon::Request>,
    key: KeyEvent,
    max_scroll: u16,
) {
    let code = key.code;
    let modifiers = key.modifiers;

    match code {
        KeyCode::PageUp => scroll_up(app, 10),
        KeyCode::PageDown => scroll_down(app, 10, max_scroll),
        _ => {}
    }

    if app.focus != Focus::Input {
        return;
    }

    match code {
        KeyCode::Char('u') if modifiers.contains(KeyModifiers::CONTROL) => {
            app.input.clear();
        }
        KeyCode::Backspace if modifiers.contains(KeyModifiers::ALT) => {
            delete_prev_word(&mut app.input);
        }
        KeyCode::Char('w') if modifiers.contains(KeyModifiers::CONTROL) => {
            delete_prev_word(&mut app.input);
        }
        KeyCode::Enter => {
            let text = app.input.trim().to_string();
            if text.is_empty() {
                return;
            }
            let Some(conversation_id) = app.active_conversation_id.clone() else {
                app.status = "No conversation selected".to_string();
                return;
            };
            request_tx
                .send(daemon::Request::SendMessage {
                    conversation_id,
                    text,
                })
                .ok();
            app.refresh_messages_in_flight = true;
            app.input.clear();
        }
        KeyCode::Backspace => {
            app.input.pop();
        }
        KeyCode::Char(c) => {
            if !c.is_control() {
                app.input.push(c);
            }
        }
        _ => {}
    }
}

fn delete_prev_word(input: &mut String) {
    while input.chars().last().is_some_and(|c| c.is_whitespace()) {
        input.pop();
    }
    while input.chars().last().is_some_and(|c| !c.is_whitespace()) {
        input.pop();
    }
}

fn scroll_up(app: &mut AppState, amount: u16) {
    if amount > 0 {
        app.pinned_to_bottom = false;
    }
    app.transcript_scroll = app.transcript_scroll.saturating_sub(amount);
}

fn scroll_down(app: &mut AppState, amount: u16, max_scroll: u16) {
    app.transcript_scroll = app.transcript_scroll.saturating_add(amount);
    if app.transcript_scroll >= max_scroll {
        app.transcript_scroll = max_scroll;
        app.pinned_to_bottom = true;
    }
}

fn transcript_inner_width(size: Size, app: &AppState, requested_view: ViewMode) -> u16 {
    let mode = view_mode(
        size.width,
        app.active_conversation_id.is_some(),
        requested_view,
    );
    let outer_width = match mode {
        ViewMode::Split => {
            let left_width = (size.width / 3).clamp(24, 40);
            size.width.saturating_sub(left_width)
        }
        ViewMode::Chat => size.width,
        ViewMode::List => 0,
    };

    outer_width.saturating_sub(2).max(1)
}

fn visual_width_u16(s: &str) -> u16 {
    s.width().min(u16::MAX as usize) as u16
}

fn transcript_lines(messages: &[daemon::ChatMessage]) -> Vec<Line<'static>> {
    let mut lines: Vec<Line<'static>> = Vec::new();
    for message in messages {
        let ts = time::OffsetDateTime::from_unix_timestamp(message.date_unix)
            .unwrap_or(time::OffsetDateTime::UNIX_EPOCH)
            .format(&time::format_description::well_known::Rfc3339)
            .unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_string());

        lines.push(Line::from(vec![
            Span::styled(
                message.sender.clone(),
                Style::default().add_modifier(Modifier::BOLD),
            ),
            Span::raw(" "),
            Span::styled(ts, Style::default().fg(Color::DarkGray)),
        ]));

        let mut rendered_any_text = false;
        for text_line in message.text.lines() {
            rendered_any_text = true;
            lines.push(Line::from(Span::raw(text_line.to_string())));
        }
        if !rendered_any_text {
            lines.push(Line::from(Span::styled(
                "<non-text message>",
                Style::default().fg(Color::DarkGray),
            )));
        }
        lines.push(Line::from(Span::raw("")));
    }

    if lines.is_empty() {
        lines.push(Line::from(Span::styled(
            "No messages.",
            Style::default().fg(Color::DarkGray),
        )));
    }

    lines
}

fn transcript_content_visual_lines(messages: &[daemon::ChatMessage], inner_width: u16) -> u16 {
    let paragraph =
        Paragraph::new(Text::from(transcript_lines(messages))).wrap(Wrap { trim: false });
    paragraph.line_count(inner_width).min(u16::MAX as usize) as u16
}

fn transcript_viewport_height(size: Size, app: &AppState, requested_view: ViewMode) -> u16 {
    let mode = view_mode(
        size.width,
        app.active_conversation_id.is_some(),
        requested_view,
    );
    let show_input =
        matches!(mode, ViewMode::Chat | ViewMode::Split) && app.active_conversation_id.is_some();

    let transcript_height = if show_input {
        size.height.saturating_sub(4) // input (3) + status (1)
    } else {
        size.height.saturating_sub(1) // status
    };

    match mode {
        ViewMode::Chat | ViewMode::Split => transcript_height.saturating_sub(2), // borders
        ViewMode::List => 0,
    }
}

fn max_transcript_scroll(app: &AppState, size: Size, requested_view: ViewMode) -> u16 {
    let viewport_height = transcript_viewport_height(size, app, requested_view);
    let inner_width = transcript_inner_width(size, app, requested_view);
    let content = transcript_content_visual_lines(&app.messages, inner_width);
    content.saturating_sub(viewport_height)
}

fn apply_transcript_scroll_policy(app: &mut AppState, size: Size, requested_view: ViewMode) {
    let max_scroll = max_transcript_scroll(app, size, requested_view);
    if app.pinned_to_bottom {
        app.transcript_scroll = max_scroll;
    } else {
        app.transcript_scroll = app.transcript_scroll.min(max_scroll);
    }
}
