//! Rendering for the authoring TUI, replicating `apic read`'s exact layout
//! (see `crate::render::Printer`) with the selection / cell-edit overlaid.
//! Borderless throughout.

use crate::tui::rows::{Cell, CellKind, RowKind, Section, SectionKind, TableRow};
use crate::tui::state::{Mode, UiState};
use ratatui::Frame;
use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Clear, Padding, Paragraph, Row, Table};
use ratatui_textarea::TextArea;

const GAP: usize = 2; // spaces between table columns
const RESP_TITLE_HINT: &str = "title"; // placeholder shown when editing an empty response title

/// Draws the whole UI for the current frame.
pub(crate) fn draw(frame: &mut Frame, state: &UiState) {
    let area = frame.area();
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Min(1), Constraint::Length(1)])
        .margin(2)
        .split(area);

    let (mut lines, sel_start, sel_end, cursor) = build_lines(state);
    // Center the selected row in the viewport (neovim-style). Half a screen of
    // blank padding is appended so even the very bottom row can sit centered.
    // The scroll is clamped at the top so short contracts don't shift.
    let view = chunks[0].height as usize;
    let pad = view / 2;
    lines.extend(std::iter::repeat_with(|| Line::raw("")).take(pad));
    let max_scroll = lines.len().saturating_sub(view);
    let center = (sel_start + sel_end) / 2;
    let scroll = center.saturating_sub(view / 2).min(max_scroll) as u16;
    frame.render_widget(Paragraph::new(lines).scroll((scroll, 0)), chunks[0]);

    // Place a real terminal cursor at the end of the insert buffer, if any.
    if let Some((line, col)) = cursor
        && line >= scroll as usize
    {
        let y = chunks[0].y + (line - scroll as usize) as u16;
        let x = chunks[0].x + col as u16;
        if y < chunks[0].y + chunks[0].height && x < chunks[0].x + chunks[0].width {
            frame.set_cursor_position((x, y));
        }
    }

    draw_status(frame, chunks[1], state);

    match state.mode {
        Mode::Help => draw_help(frame, area),
        Mode::ConfirmQuit => draw_confirm(
            frame,
            area,
            " unsaved changes ",
            "Save before quitting?",
            "y: save & quit    n: discard    Esc: cancel",
        ),
        Mode::ConfirmDelete(_) => draw_confirm(
            frame,
            area,
            " confirm delete ",
            "Delete this row?",
            "y: delete    n/Esc: cancel",
        ),
        Mode::MethodPick(idx) => draw_method_pick(frame, area, idx),
        _ => {}
    }
}

/// The HTTP-method picker: a small list of methods (each in its color) with the
/// highlighted one reversed. `j`/`k` move, Enter selects, Esc cancels.
fn draw_method_pick(frame: &mut Frame, area: Rect, selected: usize) {
    let all = apic_core::json::method_all();
    let w = 30u16.min(area.width);
    let h = (all.len() as u16 + 2).min(area.height); // methods + border
    let popup = Rect {
        x: area.x + area.width.saturating_sub(w) / 2,
        y: area.y + area.height.saturating_sub(h) / 2,
        width: w,
        height: h,
    };
    frame.render_widget(Clear, popup);

    let lines: Vec<Line> = all
        .iter()
        .enumerate()
        .map(|(i, m)| {
            let name = apic_core::json::method_str(m);
            let mut style = Style::default().fg(method_color(&name));
            if i == selected {
                style = style.add_modifier(Modifier::REVERSED | Modifier::BOLD);
            }
            Line::from(Span::styled(format!(" {name}"), style))
        })
        .collect();
    let list = Paragraph::new(lines).block(
        Block::default()
            .title(" method ")
            .title_bottom(" j/k · Enter · Esc ")
            .borders(Borders::ALL)
            .border_style(Style::default().fg(Color::Reset)),
    );
    frame.render_widget(list, popup);
}

/// A style helper: the resting cyan section title.
fn title_style() -> Style {
    Style::default()
        .add_modifier(Modifier::BOLD)
        .fg(Color::Cyan)
}

fn dim() -> Style {
    Style::default().fg(Color::DarkGray)
}

/// The color of an HTTP method label, per `render::Printer::method`.
fn method_color(method: &str) -> Color {
    match method {
        "GET" => Color::Green,
        "POST" => Color::Blue,
        "PUT" => Color::Yellow,
        "PATCH" => Color::Magenta,
        "DELETE" => Color::Red,
        "HEAD" => Color::Cyan,
        _ => Color::White,
    }
}

/// Column widths for a table section: max display width per column across its
/// `Field` rows whose cell count equals the header count.
fn col_widths(section: &Section, ncols: usize) -> Vec<usize> {
    let mut w = vec![0usize; ncols];
    if let Some(h) = &section.headers {
        for (i, head) in h.iter().enumerate() {
            w[i] = head.chars().count();
        }
    }
    for row in &section.rows {
        if row.kind == RowKind::Field && row.cells.len() == ncols {
            for (i, c) in row.cells.iter().enumerate() {
                let extra = if i == 0 {
                    row.prefix.chars().count()
                } else {
                    0
                };
                w[i] = w[i].max(extra + c.value.chars().count());
            }
        }
    }
    w
}

/// Builds all display lines, returning them, the FIRST and LAST line index of
/// the selected row's rendered content (for scrolling), and the insert cursor
/// position `(line, col)` where `col` is a char offset from the line start (if
/// editing).
fn build_lines(state: &UiState) -> (Vec<Line<'static>>, usize, usize, Option<(usize, usize)>) {
    let mut lines: Vec<Line<'static>> = Vec::new();
    let mut sel: (usize, usize) = (0, 0);
    let mut cursor: Option<(usize, usize)> = None;

    for (si, section) in state.sections.iter().enumerate() {
        match section.kind {
            SectionKind::Header => {
                push_header(state, si, section, &mut lines, &mut sel, &mut cursor);
            }
            SectionKind::Table | SectionKind::Body => {
                push_section(state, si, section, &mut lines, &mut sel, &mut cursor);
            }
        }
    }
    (lines, sel.0, sel.1, cursor)
}

/// Emits the header block: ` NAME`, ` description` (when non-empty), a blank
/// line, then the inline-editable ` METHOD url` line.
fn push_header(
    state: &UiState,
    si: usize,
    section: &Section,
    lines: &mut Vec<Line<'static>>,
    sel: &mut (usize, usize),
    cursor: &mut Option<(usize, usize)>,
) {
    for (ri, row) in section.rows.iter().enumerate() {
        let selected = si == state.sec && ri == state.row;
        match row.kind {
            RowKind::Name => {
                let (line, col) =
                    header_value_line(state, row, selected, |v| format!(" {}", v.to_uppercase()));
                if selected {
                    *sel = (lines.len(), lines.len());
                }
                record_cursor(cursor, lines.len(), col);
                lines.push(line);
            }
            RowKind::Desc => {
                let value = &row.cells[0].value;
                let editing = selected && state.cell.is_some();
                // Read prints the description line only when non-empty; while
                // editing we still show it so the cursor has somewhere to live.
                if value.is_empty() && !editing {
                    // Still track selection so scroll/navigation stays sane.
                    if selected {
                        *sel = (lines.len(), lines.len());
                    }
                    continue;
                }
                let (line, col) = header_value_line(state, row, selected, |v| format!(" {v}"));
                if selected {
                    *sel = (lines.len(), lines.len());
                }
                record_cursor(cursor, lines.len(), col);
                lines.push(line);
            }
            RowKind::UrlLine => {
                lines.push(Line::raw(""));
                if selected {
                    *sel = (lines.len(), lines.len());
                }
                let (line, col) = url_line(state, row, selected);
                record_cursor(cursor, lines.len(), col);
                lines.push(line);
            }
            RowKind::Field => {
                if selected {
                    *sel = (lines.len(), lines.len());
                }
                let (line, col) = kv_line(state, row, selected);
                record_cursor(cursor, lines.len(), col);
                lines.push(line);
            }
            RowKind::Title | RowKind::Example | RowKind::RespTabs => {}
        }
    }
}

/// Records the insert cursor at `(line, col)` when a line builder reported a
/// cursor column.
fn record_cursor(cursor: &mut Option<(usize, usize)>, line: usize, col: Option<usize>) {
    if let Some(c) = col {
        *cursor = Some((line, c));
    }
}

/// Renders a header text line (` NAME`, ` description`) honoring an in-progress
/// insert buffer (which edits the TRUE value, not the uppercased display).
fn header_value_line(
    state: &UiState,
    row: &TableRow,
    selected: bool,
    fmt: impl Fn(&str) -> String,
) -> (Line<'static>, Option<usize>) {
    let cell = &row.cells[0];
    let focused = selected && state.cell == Some(0);
    let base = sel_style(state, selected);
    if focused && let Mode::Insert(buf) = &state.mode {
        // Insert edits the raw value; show it as plain bold text and place a
        // real terminal cursor at its end (no yellow highlight while typing).
        let line = Line::from(Span::styled(
            format!(" {buf}"),
            base.add_modifier(Modifier::BOLD),
        ));
        // One leading space precedes the buffer.
        let col = 1 + buf.chars().count();
        return (line, Some(col));
    }
    let style = if focused { cell_hl() } else { base };
    (Line::from(Span::styled(fmt(&cell.value), style)), None)
}

/// The ` METHOD url` line. Both cells are inline-editable: the method cell
/// (cell 0) highlights on focus and cycles on Enter; the url cell (cell 1)
/// highlights on focus and shows the insert buffer + a real cursor while typing.
fn url_line(state: &UiState, row: &TableRow, selected: bool) -> (Line<'static>, Option<usize>) {
    let base = sel_style(state, selected);
    let method = &row.cells[0];
    let url = &row.cells[1];
    let method_focused = selected && state.cell == Some(0);
    let url_focused = selected && state.cell == Some(1);

    let method_style = if method_focused {
        cell_hl()
    } else if selected {
        // On the selected (cursor) line, the method follows the row highlight
        // (white bold) instead of its GET/POST color.
        base
    } else {
        base.fg(method_color(&method.value))
            .add_modifier(Modifier::BOLD)
    };
    let mut spans = vec![
        Span::styled(" ", base),
        Span::styled(method.value.clone(), method_style),
        Span::styled(" ", base),
    ];
    // Column where the url span starts: leading space + method + separator.
    let url_col = 1 + method.value.chars().count() + 1;
    let mut cursor_col = None;
    if url_focused && let Mode::Insert(buf) = &state.mode {
        // Plain text + real cursor while typing (no highlight).
        spans.push(Span::styled(buf.clone(), base.add_modifier(Modifier::BOLD)));
        cursor_col = Some(url_col + buf.chars().count());
    } else {
        let url_style = if url_focused { cell_hl() } else { base };
        spans.push(Span::styled(url.value.clone(), url_style));
    }
    (Line::from(spans), cursor_col)
}

/// The RESPONSE tab strip: ` 200 - ok   400   201 - created`. Cells come in
/// (code, desc) pairs per response; the active tab (`state.resp`) is emphasized,
/// the focused cell highlights, and the ` - ` separator is dropped when a title
/// is empty (unless that title cell is being edited).
fn resp_tabs_line(
    state: &UiState,
    row: &TableRow,
    selected: bool,
) -> (Line<'static>, Option<usize>) {
    let base = sel_style(state, selected);
    let mut spans = vec![Span::styled(" ", base)];
    let mut emitted = 1usize; // leading space
    let mut cursor_col = None;

    let nresp = row.cells.len() / 2;
    for r in 0..nresp {
        let code = &row.cells[r * 2];
        let desc = &row.cells[r * 2 + 1];
        let active = r == state.resp;
        let code_focused = selected && state.cell == Some(r * 2);
        let desc_focused = selected && state.cell == Some(r * 2 + 1);

        if r > 0 {
            spans.push(Span::styled("   ", base));
            emitted += 3;
        }

        // Code.
        if code_focused && let Mode::Insert(buf) = &state.mode {
            cursor_col = Some(emitted + buf.chars().count());
            emitted += buf.chars().count();
            spans.push(Span::styled(buf.clone(), base.add_modifier(Modifier::BOLD)));
        } else {
            emitted += code.value.chars().count();
            spans.push(Span::styled(
                code.value.clone(),
                tab_style(base, code_focused, active, selected),
            ));
        }

        // ` - title`, shown when the title is set or its cell is being edited.
        // A focused-but-empty title shows a dim `title` hint; in preview an empty
        // title drops the ` - ` entirely.
        if !desc.value.is_empty() || desc_focused {
            spans.push(Span::styled(" - ", base));
            emitted += 3;
            if desc_focused && let Mode::Insert(buf) = &state.mode {
                if buf.is_empty() {
                    // Ghost hint; the cursor sits at its start.
                    cursor_col = Some(emitted);
                    emitted += RESP_TITLE_HINT.chars().count();
                    spans.push(Span::styled(RESP_TITLE_HINT, dim()));
                } else {
                    cursor_col = Some(emitted + buf.chars().count());
                    emitted += buf.chars().count();
                    spans.push(Span::styled(buf.clone(), base.add_modifier(Modifier::BOLD)));
                }
            } else if desc.value.is_empty() {
                // Focused (cell-select) but empty: show the hint placeholder.
                emitted += RESP_TITLE_HINT.chars().count();
                spans.push(Span::styled(RESP_TITLE_HINT, dim()));
            } else {
                emitted += desc.value.chars().count();
                spans.push(Span::styled(
                    desc.value.clone(),
                    tab_style(base, desc_focused, active, selected),
                ));
            }
        }
    }
    (Line::from(spans), cursor_col)
}

/// Style for a tab cell: yellow highlight when focused. When the row is selected
/// the text follows the selection's white foreground so it stays readable on the
/// highlight; otherwise the active tab is cyan-bold and the rest are dim.
fn tab_style(base: Style, focused: bool, active: bool, selected: bool) -> Style {
    if focused {
        cell_hl()
    } else if selected {
        base
    } else if active {
        base.fg(Color::Cyan).add_modifier(Modifier::BOLD)
    } else {
        base.fg(Color::DarkGray)
    }
}

/// A ` label  value` key/value line (table / key-value rows). The label is dim;
/// the value cell honors focus and the insert buffer.
fn kv_line(state: &UiState, row: &TableRow, selected: bool) -> (Line<'static>, Option<usize>) {
    let base = sel_style(state, selected);
    let mut spans = vec![Span::styled(" ", base)];
    let mut emitted = 1usize; // leading space
    let mut cursor_col = None;
    for (i, c) in row.cells.iter().enumerate() {
        let focused = selected && state.cell == Some(i);
        if focused && let Mode::Insert(buf) = &state.mode {
            // Plain text + real cursor while typing (no yellow highlight).
            spans.push(Span::styled(buf.clone(), base.add_modifier(Modifier::BOLD)));
            cursor_col = Some(emitted + buf.chars().count());
            emitted += buf.chars().count();
        } else {
            let val = cell_text(state, c, focused);
            let style = if focused {
                cell_hl()
            } else if c.kind == CellKind::Label && !selected {
                // Labels are dim only when the row is not selected; when the
                // cursor is on the row, keep the consistent white-bold highlight.
                base.fg(Color::DarkGray)
            } else {
                base
            };
            emitted += val.chars().count();
            spans.push(Span::styled(val, style));
        }
        if i + 1 < row.cells.len() {
            spans.push(Span::styled("  ", base));
            emitted += 2;
        }
    }
    (Line::from(spans), cursor_col)
}

/// Emits a read-style section: a blank line, the bold title, then either the
/// rows (aligned table / body) or a dim ` (none)` for an empty section.
fn push_section(
    state: &UiState,
    si: usize,
    section: &Section,
    lines: &mut Vec<Line<'static>>,
    sel: &mut (usize, usize),
    cursor: &mut Option<(usize, usize)>,
) {
    lines.push(Line::raw(""));

    // For Body sections the title is carried by a `RowKind::Title` row (so the
    // expand selection lands on it); Table sections print `section.title`.
    let title_row = section
        .rows
        .iter()
        .enumerate()
        .find(|(_, r)| r.kind == RowKind::Title);
    match title_row {
        Some((ri, row)) => {
            let selected = si == state.sec && ri == state.row;
            if selected {
                *sel = (lines.len(), lines.len());
            }
            let style = if selected && state.cell.is_none() {
                title_style().bg(Color::Red).fg(Color::White)
            } else {
                title_style()
            };
            lines.push(Line::from(Span::styled(
                format!(" {}", row.cells[0].value),
                style,
            )));
        }
        None => {
            // An empty-titled, title-rowless section (e.g. the "+ add response"
            // affordance) prints no title line.
            if !section.title.is_empty() {
                lines.push(Line::from(Span::styled(
                    format!(" {}", section.title),
                    title_style(),
                )));
            }
        }
    }

    let field_rows: Vec<&TableRow> = section
        .rows
        .iter()
        .filter(|r| r.kind == RowKind::Field)
        .collect();
    let has_example = section.rows.iter().any(|r| r.kind == RowKind::Example);

    let ncols = section.headers.as_ref().map(|h| h.len()).unwrap_or(0);
    let widths = if ncols > 0 {
        col_widths(section, ncols)
    } else {
        Vec::new()
    };

    // BODY sections (REQUEST/RESPONSE): the lead kv rows (a response's expanded
    // code/description) followed by the inline example row, which always renders
    // (even when empty) so it stays reachable and editable in the TUI.
    if section.kind == SectionKind::Body {
        for (ri, row) in section.rows.iter().enumerate() {
            let selected = si == state.sec && ri == state.row;
            match row.kind {
                RowKind::Field => {
                    if selected {
                        *sel = (lines.len(), lines.len());
                    }
                    let (line, col) = kv_line(state, row, selected);
                    record_cursor(cursor, lines.len(), col);
                    lines.push(line);
                }
                RowKind::RespTabs => {
                    if selected {
                        *sel = (lines.len(), lines.len());
                    }
                    let (line, col) = resp_tabs_line(state, row, selected);
                    record_cursor(cursor, lines.len(), col);
                    lines.push(line);
                }
                RowKind::Example => {
                    push_example_block(state, row, selected, lines, sel);
                }
                _ => {}
            }
        }
        return;
    }

    if field_rows.is_empty() && !has_example {
        lines.push(Line::from(Span::styled(" (none)", dim())));
        return;
    }

    // Only schema field rows (cell count == ncols) feed the column-header line;
    // expanded kv rows (label + value) are rendered ` label  value`.
    let has_schema_rows = field_rows
        .iter()
        .any(|r| ncols > 0 && r.cells.len() == ncols);

    // Column header line (dim), when the section carries headers and schema rows.
    if let (Some(h), true) = (&section.headers, has_schema_rows) {
        let mut spans = vec![Span::raw(" ")];
        for (i, head) in h.iter().enumerate() {
            spans.push(Span::styled(pad(head, widths[i]), dim()));
            if i + 1 < h.len() {
                spans.push(Span::raw(" ".repeat(GAP)));
            }
        }
        lines.push(Line::from(trim_trailing(spans)));
    }

    for (ri, row) in section.rows.iter().enumerate() {
        let selected = si == state.sec && ri == state.row;
        match row.kind {
            RowKind::Field => {
                if selected {
                    *sel = (lines.len(), lines.len());
                }
                // Expanded title rows (label + value) render like URL kv rows.
                let (line, col) = if ncols > 0 && row.cells.len() == ncols {
                    table_line(state, row, &widths, ncols, selected)
                } else {
                    kv_line(state, row, selected)
                };
                record_cursor(cursor, lines.len(), col);
                lines.push(line);
            }
            RowKind::Example => {
                push_example_block(state, row, selected, lines, sel);
            }
            _ => {}
        }
    }
}

/// Renders the example payload (or ` (no example provided)`), tracking the row's
/// selection span over the whole block.
fn push_example_block(
    state: &UiState,
    row: &TableRow,
    selected: bool,
    lines: &mut Vec<Line<'static>>,
    sel: &mut (usize, usize),
) {
    lines.push(Line::raw(""));
    let start = lines.len();
    push_example(state, row, selected, lines);
    if selected {
        *sel = (start, lines.len().saturating_sub(1));
    }
}

/// Emits the inline example payload (` <line>` per line of the raw buffer). An
/// empty example renders a single blank line so the row stays selectable/editable
/// without any placeholder text.
fn push_example(state: &UiState, row: &TableRow, selected: bool, lines: &mut Vec<Line<'static>>) {
    let base = sel_style(state, selected);
    if row.raw.trim().is_empty() {
        lines.push(Line::from(Span::styled(" ", base)));
        return;
    }
    for raw_line in row.raw.lines() {
        lines.push(Line::from(Span::styled(format!(" {raw_line}"), base)));
    }
}

/// An aligned table row: columns left-padded to `widths`, joined by two spaces,
/// trailing whitespace trimmed — identical to `render::Printer::table`.
fn table_line(
    state: &UiState,
    row: &TableRow,
    widths: &[usize],
    ncols: usize,
    selected: bool,
) -> (Line<'static>, Option<usize>) {
    let base = sel_style(state, selected);
    let editing_here = selected && state.cell.is_some();
    let mut spans = vec![Span::styled(" ", base)];
    let mut emitted = 1usize; // leading space
    let mut cursor_col = None;

    if row.cells.len() == ncols && ncols > 0 {
        let last = row.cells.len() - 1;
        for (i, c) in row.cells.iter().enumerate() {
            let focused = editing_here && state.cell == Some(i);
            // Column 0 carries the tree prefix at display time only.
            let prefix = if i == 0 { row.prefix.as_str() } else { "" };
            if focused && let Mode::Insert(buf) = &state.mode {
                // Plain text + real cursor (no yellow highlight). The prefix is
                // shown but the cursor sits after the buffer.
                let shown = format!("{prefix}{buf}");
                let cell_str = if i == last {
                    shown
                } else {
                    pad(&shown, widths[i])
                };
                cursor_col = Some(emitted + prefix.chars().count() + buf.chars().count());
                emitted += cell_str.chars().count();
                spans.push(Span::styled(cell_str, base.add_modifier(Modifier::BOLD)));
            } else {
                let mut val = cell_text(state, c, focused);
                if i == 0 && !prefix.is_empty() {
                    val = format!("{prefix}{val}");
                }
                // A focused but empty last cell renders a single space so its
                // highlight is visible (e.g. editing an empty description).
                if i == last && focused && val.is_empty() {
                    val = " ".to_string();
                }
                // Last column is not padded (its trailing space would trim).
                let cell_str = if i == last { val } else { pad(&val, widths[i]) };
                let style = if focused { cell_hl() } else { base };
                emitted += cell_str.chars().count();
                spans.push(Span::styled(cell_str, style));
            }
            if i + 1 < row.cells.len() {
                spans.push(Span::styled(" ".repeat(GAP), base));
                emitted += GAP;
            }
        }
    } else {
        // Header-less (HEADERS) or mismatched: space-joined cells.
        let last = row.cells.len().saturating_sub(1);
        let hdr_widths = !widths.is_empty();
        for (i, c) in row.cells.iter().enumerate() {
            let focused = editing_here && state.cell == Some(i);
            if focused && let Mode::Insert(buf) = &state.mode {
                let cell_str = if hdr_widths && i < widths.len() && i != last {
                    pad(buf, widths[i])
                } else {
                    buf.clone()
                };
                cursor_col = Some(emitted + buf.chars().count());
                emitted += cell_str.chars().count();
                spans.push(Span::styled(cell_str, base.add_modifier(Modifier::BOLD)));
            } else {
                let val = cell_text(state, c, focused);
                let cell_str = if hdr_widths && i < widths.len() && i != last {
                    pad(&val, widths[i])
                } else {
                    val
                };
                let style = if focused {
                    cell_hl()
                } else if c.kind == CellKind::Label {
                    base.fg(Color::DarkGray)
                } else {
                    base
                };
                emitted += cell_str.chars().count();
                spans.push(Span::styled(cell_str, style));
            }
            if i + 1 < row.cells.len() {
                spans.push(Span::styled(" ".repeat(GAP), base));
                emitted += GAP;
            }
        }
    }
    (Line::from(trim_trailing(spans)), cursor_col)
}

/// The base style for a row: a yellow highlight when selected in row-select mode
/// (no cell focused). In cell-edit mode the row carries no base — only the
/// focused cell is highlighted in red (see `cell_hl`).
fn sel_style(state: &UiState, selected: bool) -> Style {
    if selected && state.cell.is_none() {
        Style::default()
            .bg(Color::Red)
            .fg(Color::White)
            .add_modifier(Modifier::BOLD)
    } else if selected && state.cell.is_some() {
        Style::default()
            .fg(Color::White)
            .add_modifier(Modifier::BOLD)
    } else {
        Style::default()
    }
}

/// The highlight for the focused cell in cell-edit mode: red, regardless of the
/// row base.
fn cell_hl() -> Style {
    Style::default()
        .bg(Color::Yellow)
        .fg(Color::White)
        .add_modifier(Modifier::BOLD)
}

/// The text to show for a cell, accounting for an in-progress insert buffer.
fn cell_text(state: &UiState, cell: &Cell, focused: bool) -> String {
    if focused && let Mode::Insert(buf) = &state.mode {
        return format!("{buf}_");
    }
    cell.value.clone()
}

fn pad(s: &str, w: usize) -> String {
    let len = s.chars().count();
    if len >= w {
        s.to_string()
    } else {
        format!("{s}{}", " ".repeat(w - len))
    }
}

/// Drops a trailing whitespace-only span so lines `trim_end` like read's output.
fn trim_trailing(mut spans: Vec<Span<'static>>) -> Vec<Span<'static>> {
    while let Some(last) = spans.last() {
        // Keep a highlighted (background-styled) trailing span so a focused
        // empty cell's cursor stays visible; only trim default whitespace.
        if last.content.trim().is_empty() && last.style.bg.is_none() && spans.len() > 1 {
            spans.pop();
        } else {
            break;
        }
    }
    spans
}

fn draw_status(frame: &mut Frame, area: Rect, state: &UiState) {
    let marker = if state.dirty { "● unsaved  " } else { "" };
    let line = Line::from(vec![
        Span::styled(
            marker,
            Style::default()
                .fg(Color::Yellow)
                .add_modifier(Modifier::BOLD),
        ),
        Span::styled(state.status.clone(), Style::default().fg(Color::DarkGray)),
    ]);
    frame.render_widget(Paragraph::new(line), area);
}

/// A bordered, centered confirmation popup showing a prompt and the key legend.
fn draw_confirm(frame: &mut Frame, area: Rect, title: &str, prompt: &str, keys: &str) {
    let popup = centered(area, 50, 20);

    frame.render_widget(Clear, popup);

    let content = vec![
        Line::from(""),
        Line::from(Span::styled(
            prompt,
            Style::default().add_modifier(Modifier::BOLD),
        )),
        Line::from(""),
        Line::from(keys),
    ];

    let dialog = Paragraph::new(content).alignment(Alignment::Center).block(
        Block::default()
            .title(title)
            .borders(Borders::ALL)
            .padding(Padding::new(2, 2, 1, 1)),
    );

    frame.render_widget(dialog, popup);
}

/// A bordered help popup with an aligned `KEY  ACTION` two-column table.
fn draw_help(frame: &mut Frame, area: Rect) {
    let rows = vec![
        Row::new(vec!["↑/↓  j/k", "Select a row"]),
        Row::new(vec![
            "←/→  h/l",
            "Move between cells · switch response tabs",
        ]),
        Row::new(vec!["Enter", "Edit cell · open example"]),
        Row::new(vec!["i", "Insert, edit the focused text cell"]),
        Row::new(vec!["Esc", "Back · cancel"]),
        Row::new(vec![
            "a",
            "Add row · REQUEST writes JSON · RESPONSE new-response form",
        ]),
        Row::new(vec![
            "e",
            "Edit, response tab: status/title · body: JSON example",
        ]),
        Row::new(vec!["d", "Delete the selected row"]),
        Row::new(vec!["Ctrl-S", "Save"]),
        Row::new(vec!["q", "Quit"]),
        Row::new(vec!["?", "Toggle this help"]),
    ];

    let popup = centered(area, 70, 50);

    let table = Table::new(rows, [Constraint::Length(12), Constraint::Min(10)])
        .block(
            Block::default()
                .title(" help ")
                .borders(Borders::ALL)
                .padding(Padding::uniform(1)),
        )
        .column_spacing(2);

    frame.render_widget(Clear, popup);
    frame.render_widget(table, popup);
}

/// Renders the borderless example-editor modal.
pub(crate) fn draw_example_modal(frame: &mut Frame, textarea: &TextArea) {
    let area = centered(frame.area(), 80, 70);
    frame.render_widget(Clear, area);
    frame.render_widget(textarea, area);
}

/// The response dialog, laid out as a two-column table (`Status` and `Short
/// Description`) with the values editable underneath. The focused column carries
/// a real terminal cursor at the end of its text. `editing` picks the title.
pub(crate) fn draw_response_form(
    frame: &mut Frame,
    status: &str,
    description: &str,
    on_description: bool,
    editing: bool,
) {
    const STATUS_W: usize = 12; // status column width
    const DESC_W: usize = 20; // description column width (input box)
    const SEP: &str = " │ ";
    // A compact, content-sized popup centered in the terminal (border + 1-cell
    // top/bottom padding around the 5 content rows, so 9 tall).
    let area = frame.area();
    let w = 46.min(area.width);
    let h = 9.min(area.height);
    let popup = Rect {
        x: area.x + area.width.saturating_sub(w) / 2,
        y: area.y + area.height.saturating_sub(h) / 2,
        width: w,
        height: h,
    };
    frame.render_widget(Clear, popup);

    // Border and text follow the terminal's default foreground (not a hardcoded
    // white); the focused field is marked by a background highlight only.
    let fg = Style::default().fg(Color::Reset);
    let header = fg.add_modifier(Modifier::BOLD);
    let focused = fg.bg(Color::Blue).add_modifier(Modifier::BOLD);
    let blurred = fg.bg(Color::DarkGray);

    let content = vec![
        Line::from(""),
        Line::from(vec![
            Span::styled(format!("{:<STATUS_W$}", "Status"), header),
            Span::styled(SEP, fg),
            Span::styled(format!("{:<DESC_W$}", "Short Description"), header),
        ]),
        Line::from(vec![
            Span::styled(
                format!("{status:<STATUS_W$}"),
                if on_description { blurred } else { focused },
            ),
            Span::styled(SEP, fg),
            Span::styled(
                format!("{description:<DESC_W$}"),
                if on_description { focused } else { blurred },
            ),
        ]),
        Line::from(""),
        Line::from(Span::styled(
            "Enter: Yes    Esc: No    Tab: switch field",
            fg,
        )),
    ];
    let title = if editing {
        " edit response "
    } else {
        " new response "
    };
    let dialog = Paragraph::new(content).block(
        Block::default()
            .title(title)
            .borders(Borders::ALL)
            .border_style(fg)
            .padding(Padding::new(2, 1, 1, 1)),
    );
    frame.render_widget(dialog, popup);

    // Cursor on the values row (content row 2). Content starts inside the border
    // (1) + left/top padding (2/1); the Status column starts at content_x, the
    // Short Description column after the status width + separator.
    let content_x = popup.x + 1 + 2;
    let values_y = popup.y + 1 + 1 + 2;
    let x = if on_description {
        content_x + (STATUS_W + SEP.chars().count() + description.chars().count()) as u16
    } else {
        content_x + status.chars().count() as u16
    };
    if x < popup.x + popup.width && values_y < popup.y + popup.height {
        frame.set_cursor_position((x, values_y));
    }
}

/// A centered rect `pct_x`% × `pct_y`% of `area`.
fn centered(area: Rect, pct_x: u16, pct_y: u16) -> Rect {
    let v = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Percentage((100 - pct_y) / 2),
            Constraint::Percentage(pct_y),
            Constraint::Percentage((100 - pct_y) / 2),
        ])
        .split(area);
    Layout::default()
        .direction(Direction::Horizontal)
        .constraints([
            Constraint::Percentage((100 - pct_x) / 2),
            Constraint::Percentage(pct_x),
            Constraint::Percentage((100 - pct_x) / 2),
        ])
        .split(v[1])[1]
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::tui::model::EditModel;
    use apic_core::json::json_get;
    use ratatui::Terminal;
    use ratatui::backend::TestBackend;

    #[test]
    fn renders_like_apic_read() {
        let c = json_get(
            r#"{ "name":"user","description":"User management","method":"GET",
                 "url":"https://api.example.com/user",
                 "headers":[{"name":"Content-Type","value":"application/json"}],
                 "responses":[{"code":200,"description":"ok",
                    "schema":{"access_token":"abc"}}] }"#,
            None,
        )
        .unwrap();
        let m = EditModel::from_contract(c);
        let state = UiState::new(&m);
        let backend = TestBackend::new(100, 50);
        let mut terminal = Terminal::new(backend).unwrap();
        terminal.draw(|f| draw(f, &state)).unwrap();
        let buf = terminal.backend().buffer().clone();
        let text: String = buf.content().iter().map(|c| c.symbol()).collect();
        assert!(text.contains("USER")); // uppercased name
        assert!(text.contains("User management")); // description
        assert!(text.contains("https://api.example.com/user")); // url string
        assert!(text.contains("HEADERS"));
        assert!(text.contains("RESPONSE")); // section title
        assert!(text.contains("200 - ok")); // response tab: `code - title`
        // The body renders its raw JSON example.
        assert!(text.contains("access_token"));
        // No box borders.
        for g in ['│', '┌', '┐', '┘', '┤', '┬', '┴', '┼'] {
            assert!(!text.contains(g), "found border glyph {g:?}");
        }
    }

    #[test]
    fn empty_request_body_keeps_a_reachable_example_row() {
        use crate::tui::rows::{BodyLoc, Field};
        let c = json_get(
            r#"{ "name":"t","method":"POST",
                 "url":"https://h/x",
                 "headers":[],
                 "request":{"foo":"bar"},
                 "responses":[] }"#,
            None,
        )
        .unwrap();
        let m = EditModel::from_contract(c);
        let state = UiState::new(&m);
        // An empty body still exposes an example row (no placeholder text) so it
        // stays reachable and editable.
        let has_example = state.sections.iter().flat_map(|s| &s.rows).any(|r| {
            r.kind == RowKind::Example
                && matches!(&r.cells[0].field, Field::BodyExample(BodyLoc::Request))
        });
        assert!(
            has_example,
            "empty body should keep an editable example row"
        );
    }

    #[test]
    fn no_request_body_renders_none() {
        let c = json_get(
            r#"{ "name":"t","method":"GET","url":"https://h/x","headers":[],"responses":[] }"#,
            None,
        )
        .unwrap();
        let m = EditModel::from_contract(c);
        let state = UiState::new(&m);
        let backend = TestBackend::new(100, 50);
        let mut terminal = Terminal::new(backend).unwrap();
        terminal.draw(|f| draw(f, &state)).unwrap();
        let buf = terminal.backend().buffer().clone();
        let text: String = buf.content().iter().map(|c| c.symbol()).collect();
        // A REQUEST with no body renders ` (none)`, like an empty RESPONSE.
        assert!(text.contains("REQUEST"));
        assert!(text.contains("(none)"));
    }

    #[test]
    fn cell_edit_highlights_only_focused_cell() {
        let c = json_get(
            r#"{ "name":"t","method":"GET",
                 "url":"https://h/x",
                 "query":[{"name":"page","value":"1","description":"d"}],
                 "headers":[],"responses":[] }"#,
            None,
        )
        .unwrap();
        let m = EditModel::from_contract(c);
        let mut state = UiState::new(&m);
        // focus the QUERY name cell in cell-edit mode
        let (si, ri) = state
            .sections
            .iter()
            .enumerate()
            .find_map(|(si, s)| {
                s.rows
                    .iter()
                    .position(|r| {
                        r.cells
                            .iter()
                            .any(|c| matches!(c.field, crate::tui::rows::Field::QueryName(_)))
                    })
                    .map(|ri| (si, ri))
            })
            .unwrap();
        state.sec = si;
        state.row = ri;
        state.cell = Some(0);
        let backend = TestBackend::new(100, 40);
        let mut terminal = Terminal::new(backend).unwrap();
        terminal.draw(|f| draw(f, &state)).unwrap();
        let buf = terminal.backend().buffer().clone();
        // Some cell in the buffer carries the focused-cell highlight background.
        let any_hl = buf
            .content()
            .iter()
            .any(|cell| cell.style().bg == Some(Color::Yellow));
        assert!(any_hl, "expected a highlighted focused cell");
    }

    #[test]
    fn scroll_follows_cursor_to_example_at_bottom() {
        // Many headers push the response example well below a short viewport.
        let mut headers = String::new();
        for i in 0..20 {
            if i > 0 {
                headers.push(',');
            }
            headers.push_str(&format!(r#"{{"name":"H{i}","value":"v{i}"}}"#));
        }
        let json = format!(
            r#"{{ "name":"t","method":"GET",
                 "url":"https://h/x",
                 "headers":[{headers}],
                 "responses":[{{"code":200,"description":"ok",
                    "schema":{{"unique_marker_xyz":1}} }}] }}"#
        );
        let c = json_get(&json, None).unwrap();
        let m = EditModel::from_contract(c);
        let mut state = UiState::new(&m);
        // place the cursor on the response example row
        let (si, ri) = state
            .sections
            .iter()
            .enumerate()
            .find_map(|(si, s)| {
                s.rows
                    .iter()
                    .position(|r| r.kind == crate::tui::rows::RowKind::Example)
                    .map(|ri| (si, ri))
            })
            .unwrap();
        state.sec = si;
        state.row = ri;
        state.cell = None;
        let backend = TestBackend::new(80, 12); // short viewport
        let mut terminal = Terminal::new(backend).unwrap();
        terminal.draw(|f| draw(f, &state)).unwrap();
        let buf = terminal.backend().buffer().clone();
        let text: String = buf.content().iter().map(|c| c.symbol()).collect();
        assert!(
            text.contains("unique_marker_xyz"),
            "example should be scrolled into view"
        );
    }
}
