use ratatui::Frame;
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};

use crate::app::{App, ChatMessage, ConnectionStatus, FocusedPanel};
use crate::tui_markdown::assistant_line_to_spans;
use roboticus_core::style::spinner_frame;

/// Render the entire TUI.
pub fn render(f: &mut Frame, app: &App) {
    // Top-level vertical split: main area + status bar
    let outer = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Min(5), Constraint::Length(1)])
        .split(f.area());

    let main_area = outer[0];
    let status_area = outer[1];

    // Horizontal split: left (chat + input) | right (logs)
    let columns = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([Constraint::Percentage(75), Constraint::Percentage(25)])
        .split(main_area);

    let left = columns[0];
    let right = columns[1];

    // Left column: chat panel + input panel
    let left_panels = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Min(3), Constraint::Length(3)])
        .split(left);

    let chat_area = left_panels[0];
    let input_area = left_panels[1];

    render_chat(f, app, chat_area);
    render_input(f, app, input_area);
    render_logs(f, app, right);
    render_status_bar(f, app, status_area);
}

fn render_chat(f: &mut Frame, app: &App, area: Rect) {
    let focused = app.focused == FocusedPanel::Chat;
    let border_style = if focused {
        Style::default().fg(Color::LightMagenta)
    } else {
        Style::default().fg(Color::DarkGray)
    };

    let spinner_char = spinner_frame(app.spinner_tick);

    let title = if let Some(ref activity) = app.agent_activity {
        let label = match activity.as_str() {
            "inference" => "thinking...",
            a if a.starts_with("tool:") => &a[5..],
            a => a,
        };
        format!(" Chat {spinner_char} {label} ")
    } else if app.streaming {
        format!(" Chat {spinner_char} streaming... ")
    } else if focused {
        " Chat \u{25c0} ".to_string()
    } else {
        " Chat ".to_string()
    };

    let block = Block::default()
        .title(title)
        .borders(Borders::ALL)
        .border_style(border_style);

    let inner = block.inner(area);
    f.render_widget(block, area);

    if app.messages.is_empty() {
        let welcome_lines = vec![
            Line::from(""),
            Line::from(Span::styled(
                "  Welcome to Roboticus TUI",
                Style::default()
                    .fg(Color::Magenta)
                    .add_modifier(Modifier::BOLD),
            )),
            Line::from(""),
            Line::from(Span::styled(
                "  Type a message below and press Enter to chat.",
                Style::default().fg(Color::DarkGray),
            )),
            Line::from(""),
            Line::from(vec![
                Span::styled("  Tab", Style::default().fg(Color::Yellow)),
                Span::styled(
                    "  Switch focus (Input → Chat → Logs)",
                    Style::default().fg(Color::DarkGray),
                ),
            ]),
            Line::from(vec![
                Span::styled("  ↑↓ ", Style::default().fg(Color::Yellow)),
                Span::styled(
                    "  Scroll when Chat or Logs focused",
                    Style::default().fg(Color::DarkGray),
                ),
            ]),
            Line::from(vec![
                Span::styled("  Ctrl+C", Style::default().fg(Color::Yellow)),
                Span::styled("  Quit", Style::default().fg(Color::DarkGray)),
            ]),
            Line::from(""),
            Line::from(Span::styled(
                "  The agent can run tools, answer questions,",
                Style::default().fg(Color::DarkGray),
            )),
            Line::from(Span::styled(
                "  and execute tasks in its workspace.",
                Style::default().fg(Color::DarkGray),
            )),
        ];
        let empty = Paragraph::new(welcome_lines);
        f.render_widget(empty, inner);
        return;
    }

    let mut lines = messages_to_lines(&app.messages, inner.width as usize, &app.agent_name);

    // Inline braille spinner when agent is working or we're waiting for a response
    if app.agent_activity.is_some() || app.streaming || app.waiting_for_response {
        let activity_label = if let Some(ref activity) = app.agent_activity {
            match activity.as_str() {
                "inference" => "thinking".to_string(),
                a if a.starts_with("tool:") => a[5..].to_string(),
                a => a.to_string(),
            }
        } else if app.streaming {
            "streaming".to_string()
        } else {
            "thinking".to_string()
        };
        lines.push(Line::from(vec![
            Span::styled(
                format!("[{}] ", app.agent_name),
                Style::default()
                    .fg(Color::Magenta)
                    .add_modifier(Modifier::BOLD),
            ),
            Span::styled(format!("{spinner_char} "), Style::default().fg(Color::Cyan)),
            Span::styled(
                activity_label,
                Style::default()
                    .fg(Color::DarkGray)
                    .add_modifier(Modifier::ITALIC),
            ),
        ]));
        lines.push(Line::from(""));
    }

    // Scroll arithmetic: chat_scroll=0 means "pinned to bottom" (most recent).
    // Paragraph::scroll() takes a logical-line offset from the TOP.
    // We estimate that each logical line wraps to ~2 visual rows on average,
    // then subtract the visible area to find the bottom-pinned offset.
    let total_lines = lines.len() as u16;
    let visible = inner.height;
    // Estimate: assume each line may wrap to 2 rows on average.
    // This is imperfect but much better than 1:1 logical:visual.
    let estimated_total_rows = total_lines.saturating_mul(2);
    let bottom_offset = estimated_total_rows.saturating_sub(visible);
    let offset = if app.chat_scroll == 0 {
        bottom_offset
    } else {
        bottom_offset.saturating_sub(app.chat_scroll)
    };

    let paragraph = Paragraph::new(lines)
        .scroll((offset, 0))
        .wrap(Wrap { trim: false });

    f.render_widget(paragraph, inner);
}

fn messages_to_lines(
    messages: &[ChatMessage],
    _width: usize,
    agent_name: &str,
) -> Vec<Line<'static>> {
    let mut lines = Vec::new();
    for msg in messages {
        let (label, color) = match msg.role.as_str() {
            "user" => ("[you]".to_string(), Color::Green),
            "assistant" | "bot" => (format!("[{agent_name}]"), Color::Magenta),
            "system" => ("[sys]".to_string(), Color::Yellow),
            _ => ("[???]".to_string(), Color::DarkGray),
        };

        let use_rich_markdown = matches!(msg.role.as_str(), "assistant" | "bot");

        // First line gets the role label + timestamp, continuation lines are indented
        let indent_width = label.len() + 1; // +1 for the space after label
        let content_lines: Vec<&str> = msg.content.lines().collect();
        let mut in_code_fence = false;

        for (i, line) in content_lines.iter().enumerate() {
            if use_rich_markdown {
                let trimmed = line.trim_start();
                if trimmed.starts_with("```") {
                    in_code_fence = !in_code_fence;
                    let fence_style = Style::default().fg(Color::DarkGray);
                    if i == 0 {
                        let mut spans = vec![Span::styled(
                            format!("{label} "),
                            Style::default().fg(color).add_modifier(Modifier::BOLD),
                        )];
                        if !msg.timestamp.is_empty() {
                            spans.push(Span::styled(
                                format!("{} ", msg.timestamp),
                                Style::default().fg(Color::DarkGray),
                            ));
                        }
                        spans.push(Span::styled(trimmed.to_string(), fence_style));
                        lines.push(Line::from(spans));
                    } else {
                        let pad = " ".repeat(indent_width);
                        lines.push(Line::from(vec![
                            Span::raw(pad),
                            Span::styled(trimmed.to_string(), fence_style),
                        ]));
                    }
                    continue;
                }

                let body_spans = assistant_line_to_spans(line, in_code_fence);
                if i == 0 {
                    let mut spans = vec![Span::styled(
                        format!("{label} "),
                        Style::default().fg(color).add_modifier(Modifier::BOLD),
                    )];
                    if !msg.timestamp.is_empty() {
                        spans.push(Span::styled(
                            format!("{} ", msg.timestamp),
                            Style::default().fg(Color::DarkGray),
                        ));
                    }
                    spans.extend(body_spans);
                    lines.push(Line::from(spans));
                } else {
                    let pad = " ".repeat(indent_width);
                    let mut row = vec![Span::raw(pad)];
                    row.extend(body_spans);
                    lines.push(Line::from(row));
                }
            } else if i == 0 {
                let mut spans = vec![Span::styled(
                    format!("{label} "),
                    Style::default().fg(color).add_modifier(Modifier::BOLD),
                )];
                if !msg.timestamp.is_empty() {
                    spans.push(Span::styled(
                        format!("{} ", msg.timestamp),
                        Style::default().fg(Color::DarkGray),
                    ));
                }
                spans.push(Span::raw(line.to_string()));
                lines.push(Line::from(spans));
            } else {
                let pad = " ".repeat(indent_width);
                lines.push(Line::from(vec![
                    Span::raw(pad),
                    Span::raw(line.to_string()),
                ]));
            }
        }
        // Blank line between messages
        lines.push(Line::from(""));
    }
    lines
}

fn render_input(f: &mut Frame, app: &App, area: Rect) {
    let focused = app.focused == FocusedPanel::Input;
    let border_style = if app.waiting_for_response {
        Style::default().fg(Color::DarkGray)
    } else if focused {
        Style::default().fg(Color::Magenta)
    } else {
        Style::default().fg(Color::DarkGray)
    };

    let title = if focused && !app.waiting_for_response {
        " Input \u{25c0} "
    } else {
        " Input "
    };
    let block = Block::default()
        .title(title)
        .borders(Borders::ALL)
        .border_style(border_style);

    let inner = block.inner(area);
    f.render_widget(block, area);

    if app.waiting_for_response {
        // Show placeholder in input area while agent is working — the user's eyes
        // are on the chat window where the typing indicator appears, so we just
        // disable the input with a brief status note.
        let placeholder = Paragraph::new("Formulating a response...").style(
            Style::default()
                .fg(Color::DarkGray)
                .add_modifier(Modifier::ITALIC),
        );
        f.render_widget(placeholder, inner);
    } else {
        // Horizontal scroll: keep cursor visible when input exceeds width
        let display_col = app.input[..app.cursor_pos].chars().count();
        let width = inner.width as usize;
        let h_scroll = if display_col >= width {
            display_col - width + 1
        } else {
            0
        };
        let input_text = Paragraph::new(app.input.as_str())
            .style(Style::default().fg(Color::White))
            .scroll((0, h_scroll as u16));
        f.render_widget(input_text, inner);

        // Show cursor in input field when focused
        if focused {
            let cursor_x = inner.x + (display_col - h_scroll) as u16;
            let cursor_y = inner.y;
            f.set_cursor_position((cursor_x.min(inner.x + inner.width - 1), cursor_y));
        }
    }
}

fn render_logs(f: &mut Frame, app: &App, area: Rect) {
    let focused = app.focused == FocusedPanel::Logs;
    let border_style = if focused {
        Style::default().fg(Color::LightMagenta)
    } else {
        Style::default().fg(Color::DarkGray)
    };

    let title = if focused { " Logs \u{25c0} " } else { " Logs " };
    let block = Block::default()
        .title(title)
        .borders(Borders::ALL)
        .border_style(border_style);

    let inner = block.inner(area);
    f.render_widget(block, area);

    if app.logs.is_empty() {
        return;
    }

    let lines: Vec<Line<'static>> = app
        .logs
        .iter()
        .map(|l| {
            Line::from(Span::styled(
                l.clone(),
                Style::default().fg(Color::DarkGray),
            ))
        })
        .collect();

    let total_lines = lines.len() as u16;
    let visible = inner.height;
    let max_scroll = total_lines.saturating_sub(visible);
    let effective_scroll = app.log_scroll.min(max_scroll);
    let offset = max_scroll.saturating_sub(effective_scroll);

    let paragraph = Paragraph::new(lines)
        .scroll((offset, 0))
        .wrap(Wrap { trim: false });

    f.render_widget(paragraph, inner);
}

fn render_status_bar(f: &mut Frame, app: &App, area: Rect) {
    let status_icon = match &app.status {
        ConnectionStatus::Connected => {
            Span::styled(" Connected ", Style::default().fg(Color::Green))
        }
        ConnectionStatus::Connecting => {
            Span::styled(" Connecting... ", Style::default().fg(Color::Yellow))
        }
        ConnectionStatus::Disconnected(reason) => {
            let msg = format!(" Disconnected: {} ", truncate(reason, 30));
            Span::styled(msg, Style::default().fg(Color::Red))
        }
    };

    let session_span = if let Some(ref name) = app.session_name {
        Span::raw(format!(" Session: {} ", truncate(name, 24)))
    } else {
        match &app.session_id {
            Some(id) => Span::raw(format!(" Session: {} ", truncate(id, 12))),
            None => Span::styled(" No session ", Style::default().fg(Color::DarkGray)),
        }
    };

    let model_span = match &app.model {
        Some(m) => Span::raw(format!(" Model: {m} ")),
        None => Span::styled(" Model: — ", Style::default().fg(Color::DarkGray)),
    };

    let help = Span::styled(
        " [Tab] Switch  [Ctrl+C] Quit  [Enter] Send ",
        Style::default().fg(Color::DarkGray),
    );

    let line = Line::from(vec![
        Span::styled(
            " Roboticus ",
            Style::default()
                .fg(Color::Magenta)
                .add_modifier(Modifier::BOLD),
        ),
        Span::raw("|"),
        status_icon,
        Span::raw("|"),
        session_span,
        model_span,
        Span::raw("  "),
        help,
    ]);

    let bar = Paragraph::new(line).style(Style::default().bg(Color::DarkGray).fg(Color::White));
    f.render_widget(bar, area);
}

fn truncate(s: &str, max: usize) -> &str {
    if s.len() <= max {
        s
    } else {
        // Find the last char boundary at or before `max` to avoid panicking
        let mut end = max;
        while end > 0 && !s.is_char_boundary(end) {
            end -= 1;
        }
        &s[..end]
    }
}

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

    fn line_text(line: &Line<'_>) -> String {
        line.spans
            .iter()
            .map(|span| span.content.as_ref())
            .collect::<String>()
    }

    fn buffer_text(backend: &TestBackend) -> String {
        let buffer = backend.buffer();
        let mut out = String::new();
        for y in 0..buffer.area.height {
            for x in 0..buffer.area.width {
                out.push_str(buffer[(x, y)].symbol());
            }
            out.push('\n');
        }
        out
    }

    #[test]
    fn truncate_ascii_within_limit() {
        assert_eq!(truncate("hello", 10), "hello");
    }

    #[test]
    fn truncate_ascii_exact_boundary() {
        assert_eq!(truncate("hello", 5), "hello");
    }

    #[test]
    fn truncate_ascii_shorter() {
        assert_eq!(truncate("hello world", 5), "hello");
    }

    #[test]
    fn truncate_empty_string() {
        assert_eq!(truncate("", 5), "");
        assert_eq!(truncate("", 0), "");
    }

    #[test]
    fn truncate_zero_max() {
        assert_eq!(truncate("hello", 0), "");
    }

    #[test]
    fn truncate_multibyte_emoji_not_split() {
        // "😀" is 4 bytes. max=2 would land inside it, should back up to 0.
        let s = "😀hello";
        assert_eq!(truncate(s, 2), "");
        // max=4 should include the whole emoji
        assert_eq!(truncate(s, 4), "😀");
    }

    #[test]
    fn truncate_cjk_boundary() {
        // '中' = 3 bytes, '文' = 3 bytes
        let s = "中文字";
        assert_eq!(truncate(s, 3), "中");
        assert_eq!(truncate(s, 6), "中文");
        assert_eq!(truncate(s, 5), "中"); // 5 is inside '文', backs up to 3
    }

    #[test]
    fn truncate_mixed_ascii_and_multibyte() {
        let s = "ab😀cd";
        // 'a'=1, 'b'=2, '😀'=2..6, 'c'=6, 'd'=7
        assert_eq!(truncate(s, 2), "ab");
        assert_eq!(truncate(s, 3), "ab"); // 3 is inside emoji, backs up to 2
        assert_eq!(truncate(s, 6), "ab😀");
        assert_eq!(truncate(s, 7), "ab😀c");
    }

    #[test]
    fn messages_to_lines_formats_user_and_system_messages() {
        let messages = vec![
            ChatMessage {
                role: "user".into(),
                content: "hello\nworld".into(),
                timestamp: "10:00".into(),
            },
            ChatMessage {
                role: "system".into(),
                content: "notice".into(),
                timestamp: String::new(),
            },
        ];

        let lines = messages_to_lines(&messages, 80, "Roboticus");
        assert_eq!(line_text(&lines[0]), "[you] 10:00 hello");
        assert_eq!(line_text(&lines[1]), "      world");
        assert_eq!(line_text(&lines[2]), "");
        assert_eq!(line_text(&lines[3]), "[sys] notice");
        assert_eq!(line_text(&lines[4]), "");
    }

    #[test]
    fn messages_to_lines_formats_assistant_markdown_and_code_fences() {
        let messages = vec![ChatMessage {
            role: "assistant".into(),
            content: "Here is code:\n```rust\nlet x = 1;\n```".into(),
            timestamp: "10:01".into(),
        }];

        let lines = messages_to_lines(&messages, 80, "Roboticus");
        assert_eq!(line_text(&lines[0]), "[Roboticus] 10:01 Here is code:");
        assert_eq!(line_text(&lines[1]), "            ```rust");
        assert_eq!(line_text(&lines[2]), "            let x = 1;");
        assert_eq!(line_text(&lines[3]), "            ```");
        assert_eq!(line_text(&lines[4]), "");
    }

    #[test]
    fn messages_to_lines_uses_fallback_label_for_unknown_roles() {
        let messages = vec![ChatMessage {
            role: "mystery".into(),
            content: "???".into(),
            timestamp: String::new(),
        }];

        let lines = messages_to_lines(&messages, 80, "Roboticus");
        assert_eq!(line_text(&lines[0]), "[???] ???");
        assert_eq!(line_text(&lines[1]), "");
    }

    #[test]
    fn render_shows_welcome_and_status_bar_for_empty_app() {
        let backend = TestBackend::new(80, 24);
        let mut terminal = Terminal::new(backend).unwrap();
        let app = App {
            focused: FocusedPanel::Input,
            input: String::new(),
            cursor_pos: 0,
            messages: Vec::new(),
            logs: Vec::new(),
            session_id: None,
            session_name: None,
            status: ConnectionStatus::Connecting,
            model: None,
            agent_name: "Roboticus".into(),
            agent_activity: None,
            streaming: false,
            spinner_tick: 0,
            chat_scroll: 0,
            log_scroll: 0,
            should_quit: false,
            waiting_for_response: false,
        };

        terminal.draw(|f| render(f, &app)).unwrap();
        let text = buffer_text(terminal.backend());
        assert!(text.contains("Welcome to Roboticus TUI"));
        assert!(text.contains("Input"));
        assert!(text.contains("Logs"));
        assert!(text.contains("Connecting"));
        assert!(text.contains("No session"));
    }

    #[test]
    fn render_shows_messages_logs_and_waiting_placeholder() {
        let backend = TestBackend::new(100, 24);
        let mut terminal = Terminal::new(backend).unwrap();
        let app = App {
            focused: FocusedPanel::Logs,
            input: "draft".into(),
            cursor_pos: 5,
            messages: vec![
                ChatMessage {
                    role: "user".into(),
                    content: "hello".into(),
                    timestamp: "10:00".into(),
                },
                ChatMessage {
                    role: "assistant".into(),
                    content: "working on it".into(),
                    timestamp: "10:01".into(),
                },
            ],
            logs: vec!["boot ok".into(), "tool call".into()],
            session_id: Some("sess-1234567890".into()),
            session_name: Some("Smoke Session".into()),
            status: ConnectionStatus::Disconnected(
                "something went wrong in a very long way".into(),
            ),
            model: Some("ollama/qwen3:8b".into()),
            agent_name: "Roboticus".into(),
            agent_activity: Some("tool:search".into()),
            streaming: false,
            spinner_tick: 1,
            chat_scroll: 0,
            log_scroll: 0,
            should_quit: false,
            waiting_for_response: true,
        };

        terminal.draw(|f| render(f, &app)).unwrap();
        let text = buffer_text(terminal.backend());
        assert!(text.contains("[you] 10:00 hello"));
        assert!(text.contains("[Roboticus] 10:01 working on it"));
        assert!(text.contains("tool call"));
        assert!(text.contains("Formulating"));
        assert!(text.contains("Smoke Session"));
        assert!(text.contains("Model:"));
        assert!(text.contains("ollama/"));
        assert!(text.contains("Disconnected"));
    }
}
