use ratatui::layout::Rect;
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Paragraph};
use ratatui::Frame;
use unicode_width::UnicodeWidthStr;

use crate::state::AppState;
use crate::theme;

pub fn render(state: &AppState, frame: &mut Frame, area: Rect) {
    let border =
        Block::default()
            .borders(Borders::ALL)
            .border_style(if state.in_flight.is_some() {
                theme::spinner()
            } else {
                theme::dim()
            });

    let inner = border.inner(area);
    frame.render_widget(border, area);

    let lines: Vec<Line> = if state.editor.text.is_empty() {
        vec![Line::from(Span::styled(
            "type your message, Shift+Enter for newline",
            theme::dim(),
        ))]
    } else {
        state
            .editor
            .text
            .split('\n')
            .map(|line| Line::from(Span::styled(line.to_string(), theme::primary())))
            .collect()
    };

    frame.render_widget(Paragraph::new(lines), inner);

    // Cursor: only show when editor is non-empty (otherwise the placeholder
    // would look like text).
    if !state.editor.text.is_empty() && inner.height > 0 {
        let cursor_byte = clamp_to_char_boundary(&state.editor.text, state.editor.cursor_byte);
        let before_cursor = &state.editor.text[..cursor_byte];
        let last_line_start = before_cursor.rfind('\n').map(|i| i + 1).unwrap_or(0);
        let line_count = before_cursor.matches('\n').count() as u16;
        let col = UnicodeWidthStr::width(&before_cursor[last_line_start..]) as u16;
        let x = inner.x + col.min(inner.width.saturating_sub(1));
        let y = inner.y + line_count.min(inner.height.saturating_sub(1));
        frame.set_cursor_position((x, y));
    }
}

// Defensive: the plan's reference impl indexed `text[..cursor_byte.min(text.len())]`
// which panics when `cursor_byte` lands inside a multi-byte char (any non-ASCII
// input). Walking down to the previous boundary is safe because byte 0 is always
// a boundary, so the loop terminates. Do not "simplify" back to direct slicing.
fn clamp_to_char_boundary(s: &str, byte: usize) -> usize {
    let mut byte = byte.min(s.len());
    while !s.is_char_boundary(byte) {
        byte -= 1;
    }
    byte
}

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

    #[test]
    fn renders_placeholder_when_empty() -> Result<(), Box<dyn std::error::Error>> {
        let state = AppState::default();
        let mut term = Terminal::new(TestBackend::new(60, 5))?;
        term.draw(|f| render(&state, f, f.area()))?;
        insta::assert_snapshot!(term.backend());
        Ok(())
    }

    #[test]
    fn renders_multiline_text() -> Result<(), Box<dyn std::error::Error>> {
        let mut state = AppState::default();
        state.editor.text = "line1\nline2".into();
        state.editor.cursor_byte = state.editor.text.len();
        let mut term = Terminal::new(TestBackend::new(60, 5))?;
        term.draw(|f| render(&state, f, f.area()))?;
        insta::assert_snapshot!(term.backend());
        Ok(())
    }

    #[test]
    fn clamps_cursor_to_utf8_boundary() -> Result<(), Box<dyn std::error::Error>> {
        let mut state = AppState::default();
        state.editor.text = "é".into();
        state.editor.cursor_byte = 1;
        let mut term = Terminal::new(TestBackend::new(20, 3))?;
        term.draw(|f| render(&state, f, f.area()))?;
        Ok(())
    }
}
