//! Full-screen Ratatui TUI for quicproquo v2, driven by the SDK event system.
//!
//! Layout:
//!   +-- Conversations -+-- Messages ------------------------------+
//!   | > alice (DM)     | [12:34] alice: Hey!                      |
//!   |   teamchat       | [12:35] you: Hello alice                 |
//!   |   devgroup       | [12:36] alice: How's it going?           |
//!   |                  |                                          |
//!   +------------------+------------------------------------------+
//!   | > Type a message... (Enter to send, /cmd for commands)      |
//!   +-------------------------------------------------------------+
//!   | Connected to 127.0.0.1:7000 | alice | 3 conversations      |
//!   +-------------------------------------------------------------+
//!
//! Keyboard:
//!   Enter         -- send message (or execute /command)
//!   Up / Down     -- scroll message history
//!   Tab           -- next conversation
//!   Shift+Tab     -- previous conversation
//!   Ctrl+C / Ctrl+Q -- quit
//!
//! Feature gate: requires both `v2` and `tui` features.

use std::time::Duration;

use crossterm::{
    event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers},
    execute,
    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
    backend::CrosstermBackend,
    layout::{Constraint, Direction, Layout, Rect},
    style::{Color, Modifier, Style},
    text::{Line, Span},
    widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Wrap},
    Frame, Terminal,
};
use tokio::sync::broadcast;

use quicproquo_sdk::client::QpqClient;
use quicproquo_sdk::conversation::ConversationStore;
use quicproquo_sdk::events::ClientEvent;

// ── Data Types ──────────────────────────────────────────────────────────────

/// A conversation entry shown in the sidebar.
struct ConversationItem {
    id: [u8; 16],
    name: String,
    unread: usize,
    last_message: Option<String>,
}

/// A message entry shown in the messages panel.
struct DisplayMessage {
    timestamp: String,
    sender: String,
    body: String,
    is_outgoing: bool,
}

/// Focus region for keyboard navigation.
#[derive(Clone, Copy, PartialEq, Eq)]
enum Focus {
    Input,
    Help,
}

// ── Application State ───────────────────────────────────────────────────────

/// Main application state for the TUI.
pub struct TuiApp {
    conversations: Vec<ConversationItem>,
    selected_conversation: usize,
    messages: Vec<DisplayMessage>,
    input: String,
    input_cursor: usize,
    scroll_offset: usize,
    status_line: String,
    should_quit: bool,
    username: Option<String>,
    server_addr: String,
    focus: Focus,
    /// Notification line (shown briefly, e.g. "Message sent", "Error: ...").
    notification: Option<String>,
    /// Whether the client is currently connected.
    connected: bool,
    /// Current MLS epoch for the active conversation (if available).
    mls_epoch: Option<u64>,
}

impl TuiApp {
    fn new(server_addr: &str) -> Self {
        Self {
            conversations: Vec::new(),
            selected_conversation: 0,
            messages: Vec::new(),
            input: String::new(),
            input_cursor: 0,
            scroll_offset: 0,
            status_line: format!("Connecting to {server_addr}..."),
            should_quit: false,
            username: None,
            server_addr: server_addr.to_string(),
            focus: Focus::Input,
            notification: None,
            connected: false,
            mls_epoch: None,
        }
    }

    fn active_conv_id(&self) -> Option<&[u8; 16]> {
        self.conversations
            .get(self.selected_conversation)
            .map(|c| &c.id)
    }

    fn select_next_conversation(&mut self) {
        if self.conversations.is_empty() {
            return;
        }
        self.selected_conversation =
            (self.selected_conversation + 1) % self.conversations.len();
        self.scroll_offset = 0;
    }

    fn select_prev_conversation(&mut self) {
        if self.conversations.is_empty() {
            return;
        }
        if self.selected_conversation == 0 {
            self.selected_conversation = self.conversations.len() - 1;
        } else {
            self.selected_conversation -= 1;
        }
        self.scroll_offset = 0;
    }

    fn scroll_up(&mut self) {
        self.scroll_offset = self.scroll_offset.saturating_add(1);
    }

    fn scroll_down(&mut self) {
        self.scroll_offset = self.scroll_offset.saturating_sub(1);
    }

    fn update_status(&mut self) {
        let conn_indicator = if self.connected { "Online" } else { "Offline" };
        let user = self
            .username
            .as_deref()
            .unwrap_or("not logged in");
        let conv_count = self.conversations.len();
        let epoch_str = match self.mls_epoch {
            Some(e) => format!("epoch {e}"),
            None => "epoch --".to_string(),
        };
        self.status_line = format!(
            "{conn_indicator} | {} | {} | {} conversation{} | MLS {epoch_str}",
            self.server_addr,
            user,
            conv_count,
            if conv_count == 1 { "" } else { "s" }
        );
    }
}

// ── Terminal Drop Guard ─────────────────────────────────────────────────────

/// Ensures the terminal is restored on drop (including panics).
struct TerminalGuard;

impl Drop for TerminalGuard {
    fn drop(&mut self) {
        let _ = disable_raw_mode();
        let _ = execute!(
            std::io::stdout(),
            LeaveAlternateScreen,
            DisableMouseCapture
        );
    }
}

// ── Entry Point ─────────────────────────────────────────────────────────────

/// Run the full-screen TUI, subscribing to SDK events.
///
/// The caller must have already connected and optionally authenticated the
/// `QpqClient`. The TUI subscribes to events and drives the UI loop.
pub async fn run_v2_tui(client: &mut QpqClient) -> anyhow::Result<()> {
    let server_addr = if client.is_connected() {
        "server"
    } else {
        "disconnected"
    };
    let mut app = TuiApp::new(server_addr);
    app.connected = client.is_connected();

    // Populate initial state from client.
    if let Some(name) = client.username() {
        app.username = Some(String::from(name));
    }

    // Load existing conversations from the conversation store.
    if let Ok(store) = client.conversations() {
        let store: &ConversationStore = store;
        if let Ok(convs) = store.list_conversations() {
            for conv in &convs {
                app.conversations.push(ConversationItem {
                    id: conv.id.0,
                    name: conv.display_name.clone(),
                    unread: conv.unread_count as usize,
                    last_message: None,
                });
            }
        }
    }

    // Load messages for the first conversation.
    load_messages_for_selected(&mut app, client);

    app.update_status();

    // Subscribe to SDK events.
    let mut event_rx = client.subscribe();

    // Terminal setup.
    enable_raw_mode()?;
    let mut stdout = std::io::stdout();
    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
    let backend = CrosstermBackend::new(stdout);
    let mut terminal = Terminal::new(backend)?;
    let _guard = TerminalGuard;

    // Main event loop.
    loop {
        terminal.draw(|frame| ui(frame, &app))?;

        // Poll for crossterm input events (50ms timeout for responsiveness).
        if event::poll(Duration::from_millis(50))? {
            if let Event::Key(key) = event::read()? {
                handle_key(&mut app, client, key).await;
            }
        }

        // Process all pending SDK events (non-blocking).
        loop {
            match event_rx.try_recv() {
                Ok(sdk_event) => handle_sdk_event(&mut app, sdk_event),
                Err(broadcast::error::TryRecvError::Empty) => break,
                Err(broadcast::error::TryRecvError::Lagged(n)) => {
                    app.notification =
                        Some(format!("Warning: dropped {n} events (lagged)"));
                    break;
                }
                Err(broadcast::error::TryRecvError::Closed) => {
                    app.notification =
                        Some("Event channel closed".to_string());
                    break;
                }
            }
        }

        if app.should_quit {
            break;
        }
    }

    Ok(())
}

// ── SDK Event Handling ──────────────────────────────────────────────────────

fn handle_sdk_event(app: &mut TuiApp, event: ClientEvent) {
    match event {
        ClientEvent::Connected => {
            app.connected = true;
            app.notification = Some("Connected to server".to_string());
            app.update_status();
        }
        ClientEvent::Disconnected { reason } => {
            app.connected = false;
            app.notification = Some(format!("Disconnected: {reason}"));
            app.update_status();
        }
        ClientEvent::Registered { username } => {
            app.notification = Some(format!("Registered as {username}"));
        }
        ClientEvent::LoggedIn { username } => {
            app.username = Some(username.clone());
            app.notification = Some(format!("Logged in as {username}"));
            app.update_status();
        }
        ClientEvent::LoggedOut { username } => {
            app.username = None;
            app.notification = Some(format!("Logged out ({username})"));
            app.update_status();
        }
        ClientEvent::Authenticated { username } => {
            app.username = Some(username.clone());
            app.update_status();
        }
        ClientEvent::MessageReceived {
            conversation_id,
            sender_name,
            body,
            timestamp_ms,
            ..
        } => {
            let sender = sender_name
                .unwrap_or_else(|| "unknown".to_string());
            let ts = format_timestamp(timestamp_ms);

            // If this is the active conversation, add to the message list.
            let is_active = app
                .active_conv_id()
                .map(|id| *id == conversation_id)
                .unwrap_or(false);

            if is_active {
                app.messages.push(DisplayMessage {
                    timestamp: ts,
                    sender: sender.clone(),
                    body: body.clone(),
                    is_outgoing: false,
                });
            } else {
                // Increment unread count for non-active conversations.
                if let Some(conv) = app
                    .conversations
                    .iter_mut()
                    .find(|c| c.id == conversation_id)
                {
                    conv.unread = conv.unread.saturating_add(1);
                }
            }

            // Update last_message on the conversation item.
            if let Some(conv) = app
                .conversations
                .iter_mut()
                .find(|c| c.id == conversation_id)
            {
                conv.last_message = Some(format!("{sender}: {body}"));
            }
        }
        ClientEvent::MessageSent { .. } => {
            // Nothing to do — the message was already added optimistically.
        }
        ClientEvent::ConversationCreated {
            conversation_id,
            display_name,
        } => {
            // Add to the conversation list if not already present.
            let exists = app
                .conversations
                .iter()
                .any(|c| c.id == conversation_id);
            if !exists {
                app.conversations.push(ConversationItem {
                    id: conversation_id,
                    name: display_name.clone(),
                    unread: 0,
                    last_message: None,
                });
                app.notification =
                    Some(format!("New conversation: {display_name}"));
            }
            app.update_status();
        }
        ClientEvent::MemberAdded {
            conversation_id,
            member_key,
        } => {
            let short_key = hex::encode(&member_key[..member_key.len().min(4)]);
            app.notification =
                Some(format!("Member {short_key} added to conversation"));
            let _ = conversation_id;
        }
        ClientEvent::MemberRemoved {
            conversation_id,
            member_key,
        } => {
            let short_key = hex::encode(&member_key[..member_key.len().min(4)]);
            app.notification =
                Some(format!("Member {short_key} removed from conversation"));
            let _ = conversation_id;
        }
        ClientEvent::PushEvent { event_type, .. } => {
            app.notification =
                Some(format!("Push event (type={event_type})"));
        }
        ClientEvent::Error { message } => {
            app.notification = Some(format!("Error: {message}"));
        }
        // Events that don't need TUI-specific handling.
        _ => {}
    }
}

// ── Keyboard Handling ───────────────────────────────────────────────────────

async fn handle_key(app: &mut TuiApp, client: &mut QpqClient, key: event::KeyEvent) {
    // Dismiss help overlay on any key.
    if app.focus == Focus::Help {
        app.focus = Focus::Input;
        return;
    }

    match key.code {
        // Quit.
        KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
            app.should_quit = true;
        }
        KeyCode::Char('q') if key.modifiers.contains(KeyModifiers::CONTROL) => {
            app.should_quit = true;
        }

        // Tab — switch conversations.
        KeyCode::Tab => {
            if key.modifiers.contains(KeyModifiers::SHIFT) {
                app.select_prev_conversation();
            } else {
                app.select_next_conversation();
            }
            load_messages_for_selected(app, client);
        }
        KeyCode::BackTab => {
            app.select_prev_conversation();
            load_messages_for_selected(app, client);
        }

        // Scrolling.
        KeyCode::Up => app.scroll_up(),
        KeyCode::Down => app.scroll_down(),
        KeyCode::PageUp => {
            for _ in 0..10 {
                app.scroll_up();
            }
        }
        KeyCode::PageDown => {
            for _ in 0..10 {
                app.scroll_down();
            }
        }

        // Submit input.
        KeyCode::Enter => {
            let text = app.input.trim().to_string();
            if !text.is_empty() {
                app.input.clear();
                app.input_cursor = 0;
                handle_input(app, client, &text).await;
            }
        }

        // Text editing.
        KeyCode::Char(c) => {
            app.input.insert(app.input_cursor, c);
            app.input_cursor += c.len_utf8();
        }
        KeyCode::Backspace => {
            if app.input_cursor > 0 {
                // Find the byte offset of the previous character.
                let prev = app.input[..app.input_cursor]
                    .char_indices()
                    .next_back()
                    .map(|(i, _)| i)
                    .unwrap_or(0);
                app.input.remove(prev);
                app.input_cursor = prev;
            }
        }
        KeyCode::Delete => {
            if app.input_cursor < app.input.len() {
                app.input.remove(app.input_cursor);
            }
        }
        KeyCode::Left => {
            if app.input_cursor > 0 {
                let prev = app.input[..app.input_cursor]
                    .char_indices()
                    .next_back()
                    .map(|(i, _)| i)
                    .unwrap_or(0);
                app.input_cursor = prev;
            }
        }
        KeyCode::Right => {
            if app.input_cursor < app.input.len() {
                let next = app.input[app.input_cursor..]
                    .char_indices()
                    .nth(1)
                    .map(|(i, _)| app.input_cursor + i)
                    .unwrap_or(app.input.len());
                app.input_cursor = next;
            }
        }
        KeyCode::Home => {
            app.input_cursor = 0;
        }
        KeyCode::End => {
            app.input_cursor = app.input.len();
        }

        _ => {}
    }
}

/// Handle submitted input: either a /command or a chat message.
async fn handle_input(app: &mut TuiApp, client: &mut QpqClient, text: &str) {
    if let Some(cmd) = text.strip_prefix('/') {
        handle_command(app, client, cmd).await;
    } else {
        // Send as chat message to the active conversation.
        if app.active_conv_id().is_none() {
            app.notification = Some("No conversation selected".to_string());
            return;
        }

        // Add optimistically to the display.
        let sender = app
            .username
            .as_deref()
            .unwrap_or("you")
            .to_string();
        let ts = format_timestamp(now_ms());
        app.messages.push(DisplayMessage {
            timestamp: ts,
            sender: sender.clone(),
            body: text.to_string(),
            is_outgoing: true,
        });
        // Snap to bottom.
        app.scroll_offset = 0;

        // TODO: actually send via SDK when the send pipeline is wired up.
        // For now, emit a notification.
        app.notification = Some(format!("Sent: {text}"));
    }
}

/// Handle a /command.
async fn handle_command(app: &mut TuiApp, client: &mut QpqClient, cmd: &str) {
    let parts: Vec<&str> = cmd.splitn(3, ' ').collect();
    let verb = parts.first().copied().unwrap_or("");

    match verb {
        "help" | "h" | "?" => {
            app.focus = Focus::Help;
        }
        "quit" | "exit" | "q" => {
            app.should_quit = true;
        }
        "login" => {
            if parts.len() < 3 {
                app.notification =
                    Some("Usage: /login <username> <password>".to_string());
                return;
            }
            let username = parts[1];
            let password = parts[2];
            match client.login(username, password).await {
                Ok(()) => {
                    app.username = Some(username.to_string());
                    app.notification =
                        Some(format!("Logged in as {username}"));
                    app.update_status();
                }
                Err(e) => {
                    app.notification =
                        Some(format!("Login failed: {e}"));
                }
            }
        }
        "register" => {
            if parts.len() < 3 {
                app.notification =
                    Some("Usage: /register <username> <password>".to_string());
                return;
            }
            let username = parts[1];
            let password = parts[2];
            match client.register(username, password).await {
                Ok(()) => {
                    app.notification =
                        Some(format!("Registered as {username}"));
                }
                Err(e) => {
                    app.notification =
                        Some(format!("Registration failed: {e}"));
                }
            }
        }
        "logout" => {
            match client.logout() {
                Ok(()) => {
                    app.username = None;
                    app.notification = Some("Logged out".to_string());
                    app.update_status();
                }
                Err(e) => {
                    app.notification =
                        Some(format!("Logout failed: {e}"));
                }
            }
        }
        "dm" => {
            if parts.len() < 2 {
                app.notification =
                    Some("Usage: /dm <username>".to_string());
                return;
            }
            app.notification =
                Some(format!("DM with {} (not yet implemented)", parts[1]));
        }
        "group" => {
            if parts.len() < 3 || parts[1] != "create" {
                app.notification =
                    Some("Usage: /group create <name>".to_string());
                return;
            }
            app.notification =
                Some(format!("Create group '{}' (not yet implemented)", parts[2]));
        }
        other => {
            app.notification =
                Some(format!("Unknown command: /{other}. Type /help for help."));
        }
    }
}

// ── Drawing ─────────────────────────────────────────────────────────────────

fn ui(frame: &mut Frame, app: &TuiApp) {
    let size = frame.area();

    // Help overlay.
    if app.focus == Focus::Help {
        draw_help(frame, size);
        return;
    }

    // Top-level vertical split: [main area | input bar | status bar].
    let v_chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Min(5),    // main area
            Constraint::Length(3), // input bar
            Constraint::Length(1), // status bar
        ])
        .split(size);

    // Main area: sidebar | messages.
    let h_chunks = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([Constraint::Percentage(20), Constraint::Percentage(80)])
        .split(v_chunks[0]);

    draw_sidebar(frame, app, h_chunks[0]);
    draw_messages(frame, app, h_chunks[1]);
    draw_input(frame, app, v_chunks[1]);
    draw_status(frame, app, v_chunks[2]);
}

fn draw_sidebar(frame: &mut Frame, app: &TuiApp, area: Rect) {
    let items: Vec<ListItem> = app
        .conversations
        .iter()
        .enumerate()
        .map(|(i, conv)| {
            let prefix = if i == app.selected_conversation {
                "> "
            } else {
                "  "
            };

            let unread_suffix = if conv.unread > 0 {
                format!(" ({})", conv.unread)
            } else {
                String::new()
            };

            let style = if i == app.selected_conversation {
                Style::default()
                    .fg(Color::Cyan)
                    .add_modifier(Modifier::BOLD)
            } else if conv.unread > 0 {
                Style::default()
                    .fg(Color::White)
                    .add_modifier(Modifier::BOLD)
            } else {
                Style::default().fg(Color::Gray)
            };

            ListItem::new(Line::from(Span::styled(
                format!("{prefix}{}{unread_suffix}", conv.name),
                style,
            )))
        })
        .collect();

    let block = Block::default()
        .title(" Conversations ")
        .borders(Borders::ALL)
        .style(Style::default().fg(Color::DarkGray));

    let mut list_state = ListState::default();
    if !app.conversations.is_empty() {
        list_state.select(Some(app.selected_conversation));
    }

    frame.render_stateful_widget(
        List::new(items).block(block).highlight_style(
            Style::default()
                .fg(Color::Cyan)
                .add_modifier(Modifier::REVERSED),
        ),
        area,
        &mut list_state,
    );
}

fn draw_messages(frame: &mut Frame, app: &TuiApp, area: Rect) {
    let channel_title = app
        .conversations
        .get(app.selected_conversation)
        .map(|c| format!(" {} ", c.name))
        .unwrap_or_else(|| " Messages ".to_string());

    let block = Block::default()
        .title(channel_title)
        .borders(Borders::ALL)
        .style(Style::default().fg(Color::DarkGray));

    let inner_height = area.height.saturating_sub(2) as usize;

    // Build lines from messages (newest at bottom).
    let lines: Vec<Line> = app
        .messages
        .iter()
        .map(|m| {
            let ts_span = Span::styled(
                format!("{} ", m.timestamp),
                Style::default().fg(Color::DarkGray),
            );

            let sender_style = if m.is_outgoing {
                Style::default()
                    .fg(Color::Yellow)
                    .add_modifier(Modifier::BOLD)
            } else {
                Style::default()
                    .fg(Color::Green)
                    .add_modifier(Modifier::BOLD)
            };
            let sender_span = Span::styled(format!("{}: ", m.sender), sender_style);
            let body_span = Span::raw(m.body.clone());

            Line::from(vec![ts_span, sender_span, body_span])
        })
        .collect();

    // Apply scroll: scroll_offset=0 means newest at bottom.
    let total = lines.len();
    let (visible_start, visible_end) = if total > inner_height {
        let bottom = total.saturating_sub(app.scroll_offset);
        let start = bottom.saturating_sub(inner_height);
        (start, bottom)
    } else {
        (0, total)
    };

    let visible_lines: Vec<Line> = lines[visible_start..visible_end].to_vec();

    // Show notification at the top if present.
    let mut all_lines = Vec::new();
    if let Some(ref note) = app.notification {
        all_lines.push(Line::from(Span::styled(
            note.clone(),
            Style::default()
                .fg(Color::Magenta)
                .add_modifier(Modifier::ITALIC),
        )));
    }
    all_lines.extend(visible_lines);

    let paragraph = Paragraph::new(all_lines)
        .block(block)
        .wrap(Wrap { trim: false });

    frame.render_widget(paragraph, area);
}

fn draw_input(frame: &mut Frame, app: &TuiApp, area: Rect) {
    let title = if app.conversations.is_empty() {
        " Type /help for commands "
    } else {
        " Enter=send | Tab=switch | /help for commands "
    };

    let block = Block::default()
        .title(title)
        .borders(Borders::ALL)
        .style(Style::default().fg(Color::DarkGray));

    let display_text = if app.input.is_empty() {
        "Type a message..."
    } else {
        &app.input
    };

    let style = if app.input.is_empty() {
        Style::default().fg(Color::DarkGray)
    } else {
        Style::default().fg(Color::White)
    };

    let input_text = Paragraph::new(display_text)
        .block(block)
        .style(style);

    frame.render_widget(input_text, area);

    // Position cursor in the input area.
    if !app.input.is_empty() || true {
        let cursor_x = area.x + 1 + app.input_cursor as u16;
        let cursor_y = area.y + 1;
        if cursor_x < area.x + area.width - 1 {
            frame.set_cursor_position((cursor_x, cursor_y));
        }
    }
}

fn draw_status(frame: &mut Frame, app: &TuiApp, area: Rect) {
    let conn_color = if app.connected {
        Color::Green
    } else {
        Color::Red
    };
    let conn_indicator = if app.connected { " ON " } else { " OFF " };

    let spans = vec![
        Span::styled(
            conn_indicator,
            Style::default().fg(Color::Black).bg(conn_color),
        ),
        Span::styled(
            format!(" {} ", app.status_line),
            Style::default().fg(Color::Black).bg(Color::DarkGray),
        ),
    ];

    let status = Paragraph::new(Line::from(spans));
    frame.render_widget(status, area);
}

fn draw_help(frame: &mut Frame, area: Rect) {
    let help_text = vec![
        Line::from(Span::styled(
            "  quicproquo TUI -- Help",
            Style::default()
                .fg(Color::Cyan)
                .add_modifier(Modifier::BOLD),
        )),
        Line::from(""),
        Line::from(Span::styled(
            "  Keyboard shortcuts:",
            Style::default()
                .fg(Color::Yellow)
                .add_modifier(Modifier::BOLD),
        )),
        Line::from("    Enter        Send message / execute command"),
        Line::from("    Up/Down      Scroll messages"),
        Line::from("    PgUp/PgDn    Scroll messages (10 lines)"),
        Line::from("    Tab          Next conversation"),
        Line::from("    Shift+Tab    Previous conversation"),
        Line::from("    Ctrl+C       Quit"),
        Line::from("    Ctrl+Q       Quit"),
        Line::from(""),
        Line::from(Span::styled(
            "  Commands:",
            Style::default()
                .fg(Color::Yellow)
                .add_modifier(Modifier::BOLD),
        )),
        Line::from("    /help              Show this help"),
        Line::from("    /login <u> <p>     Log in"),
        Line::from("    /register <u> <p>  Register a new account"),
        Line::from("    /logout            Log out"),
        Line::from("    /dm <username>     Start a DM"),
        Line::from("    /group create <n>  Create a group"),
        Line::from("    /quit              Exit the TUI"),
        Line::from(""),
        Line::from(Span::styled(
            "  Press any key to dismiss",
            Style::default().fg(Color::DarkGray),
        )),
    ];

    let block = Block::default()
        .title(" Help ")
        .borders(Borders::ALL)
        .style(Style::default().fg(Color::Cyan));

    let help = Paragraph::new(help_text).block(block);
    let help_area = centered_rect(60, 70, area);
    // Clear the background.
    frame.render_widget(
        Block::default().style(Style::default().bg(Color::Black)),
        help_area,
    );
    frame.render_widget(help, help_area);
}

/// Create a centered rectangle of `percent_x`% width and `percent_y`% height.
fn centered_rect(percent_x: u16, percent_y: u16, area: Rect) -> Rect {
    let v = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Percentage((100 - percent_y) / 2),
            Constraint::Percentage(percent_y),
            Constraint::Percentage((100 - percent_y) / 2),
        ])
        .split(area);
    let h = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([
            Constraint::Percentage((100 - percent_x) / 2),
            Constraint::Percentage(percent_x),
            Constraint::Percentage((100 - percent_x) / 2),
        ])
        .split(v[1]);
    h[1]
}

// ── Helpers ─────────────────────────────────────────────────────────────────

/// Load messages from the conversation store into the app for the selected
/// conversation.
fn load_messages_for_selected(app: &mut TuiApp, client: &QpqClient) {
    app.messages.clear();
    app.scroll_offset = 0;

    let conv_id = match app.active_conv_id() {
        Some(id) => *id,
        None => return,
    };

    let store: &ConversationStore = match client.conversations() {
        Ok(s) => s,
        Err(_) => return,
    };

    let sdk_conv_id =
        quicproquo_sdk::conversation::ConversationId::from_slice(&conv_id);
    let sdk_conv_id = match sdk_conv_id {
        Some(id) => id,
        None => return,
    };

    let msgs = match store.load_recent_messages(&sdk_conv_id, 200) {
        Ok(m) => m,
        Err(_) => return,
    };

    for msg in &msgs {
        let sender: String = if msg.is_outgoing {
            app.username.as_deref().unwrap_or("you").to_string()
        } else if let Some(ref name) = msg.sender_name {
            name.clone()
        } else {
            let len = msg.sender_key.len().min(4);
            hex::encode(&msg.sender_key[..len])
        };

        app.messages.push(DisplayMessage {
            timestamp: format_timestamp(msg.timestamp_ms),
            sender,
            body: msg.body.clone(),
            is_outgoing: msg.is_outgoing,
        });
    }

    // Mark unread = 0 for the selected conversation.
    if let Some(conv) = app
        .conversations
        .iter_mut()
        .find(|c| c.id == conv_id)
    {
        conv.unread = 0;
    }
}

fn format_timestamp(ms: u64) -> String {
    let secs = ms / 1000;
    let hours = (secs / 3600) % 24;
    let minutes = (secs / 60) % 60;
    format!("[{hours:02}:{minutes:02}]")
}

fn now_ms() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .as_millis() as u64
}

#[cfg(test)]
mod tests {
    use super::*;
    use ratatui::{backend::TestBackend, Terminal};

    fn make_app() -> TuiApp {
        let mut app = TuiApp::new("127.0.0.1:7000");
        app.connected = true;
        app.username = Some("alice".to_string());
        app.conversations.push(ConversationItem {
            id: [1u8; 16],
            name: "general".to_string(),
            unread: 0,
            last_message: None,
        });
        app.conversations.push(ConversationItem {
            id: [2u8; 16],
            name: "random".to_string(),
            unread: 3,
            last_message: Some("bob: hello".to_string()),
        });
        app.messages.push(DisplayMessage {
            timestamp: "[12:34]".to_string(),
            sender: "alice".to_string(),
            body: "Hello world".to_string(),
            is_outgoing: true,
        });
        app.messages.push(DisplayMessage {
            timestamp: "[12:35]".to_string(),
            sender: "bob".to_string(),
            body: "Hi alice!".to_string(),
            is_outgoing: false,
        });
        app.update_status();
        app
    }

    #[test]
    fn render_does_not_panic() {
        let app = make_app();
        let backend = TestBackend::new(80, 24);
        let mut terminal = Terminal::new(backend).unwrap();
        terminal.draw(|f| ui(f, &app)).unwrap();
    }

    #[test]
    fn render_help_overlay() {
        let mut app = make_app();
        app.focus = Focus::Help;
        let backend = TestBackend::new(80, 24);
        let mut terminal = Terminal::new(backend).unwrap();
        terminal.draw(|f| ui(f, &app)).unwrap();
    }

    #[test]
    fn status_bar_shows_online() {
        let mut app = TuiApp::new("127.0.0.1:7000");
        app.connected = true;
        app.username = Some("alice".to_string());
        app.update_status();
        assert!(app.status_line.contains("Online"));
        assert!(app.status_line.contains("alice"));
        assert!(app.status_line.contains("MLS epoch --"));
    }

    #[test]
    fn status_bar_shows_offline() {
        let mut app = TuiApp::new("127.0.0.1:7000");
        app.connected = false;
        app.update_status();
        assert!(app.status_line.contains("Offline"));
    }

    #[test]
    fn status_bar_shows_epoch() {
        let mut app = TuiApp::new("127.0.0.1:7000");
        app.connected = true;
        app.mls_epoch = Some(42);
        app.update_status();
        assert!(app.status_line.contains("MLS epoch 42"));
    }

    #[test]
    fn conversation_navigation() {
        let mut app = make_app();
        assert_eq!(app.selected_conversation, 0);
        app.select_next_conversation();
        assert_eq!(app.selected_conversation, 1);
        app.select_next_conversation();
        assert_eq!(app.selected_conversation, 0); // wraps around
        app.select_prev_conversation();
        assert_eq!(app.selected_conversation, 1); // wraps back
    }

    #[test]
    fn scroll_bounds() {
        let mut app = make_app();
        assert_eq!(app.scroll_offset, 0);
        app.scroll_down(); // already at 0
        assert_eq!(app.scroll_offset, 0);
        app.scroll_up();
        assert_eq!(app.scroll_offset, 1);
        app.scroll_up();
        assert_eq!(app.scroll_offset, 2);
        app.scroll_down();
        assert_eq!(app.scroll_offset, 1);
    }

    #[test]
    fn render_empty_state() {
        let app = TuiApp::new("127.0.0.1:7000");
        let backend = TestBackend::new(80, 24);
        let mut terminal = Terminal::new(backend).unwrap();
        terminal.draw(|f| ui(f, &app)).unwrap();
    }

    #[test]
    fn unread_count_display() {
        let app = make_app();
        // The second conversation has 3 unread.
        assert_eq!(app.conversations[1].unread, 3);

        // Render and check the sidebar contains the unread indicator.
        let backend = TestBackend::new(80, 24);
        let mut terminal = Terminal::new(backend).unwrap();
        terminal.draw(|f| ui(f, &app)).unwrap();

        // Read the buffer and check the sidebar area contains "random (3)".
        let buf = terminal.backend().buffer().clone();
        let mut sidebar_text = String::new();
        // Sidebar is roughly the left 20% = 16 columns.
        for y in 0..buf.area.height {
            for x in 0..16.min(buf.area.width) {
                let cell = &buf[(x, y)];
                sidebar_text.push_str(cell.symbol());
            }
        }
        assert!(
            sidebar_text.contains("random (3)"),
            "sidebar should show unread count; got: {sidebar_text}"
        );
    }
}
