use ratatui::{
    Frame,
    layout::{Alignment, Constraint, Flex, Layout, Rect},
    style::{Color, Style, Stylize},
    text::{Line, Span},
    widgets::{Block, Clear, Paragraph, Wrap},
};
use simd_json::OwnedValue as Value;
use simd_json::StaticNode;
use unicode_width::UnicodeWidthStr;

use crate::app::{App, LoadingState};

const INDENT: &str = "  ";

pub fn render(frame: &mut Frame, app: &mut App) {
    match app.loading_state() {
        LoadingState::Loading => {
            render_loading_screen(frame, "Loading...");
            return;
        }
        LoadingState::Failed(msg) => {
            render_error_screen(frame, msg);
            return;
        }
        LoadingState::Ready => {} // Continue to normal render
    }

    let filter_height = if app.filter_error().is_some() { 2 } else { 1 };
    let chunks = Layout::vertical([
        Constraint::Min(1),
        Constraint::Length(filter_height),
        Constraint::Length(1),
    ])
    .split(frame.area());

    let viewport_height = chunks[0].height as usize;
    let viewport_width = chunks[0].width as usize;
    app.set_viewport(viewport_width, viewport_height);

    // Virtualized rendering: only render visible lines
    let visible_lines = render_viewport(
        app.output_values(),
        &app.render_options(),
        app.scroll_offset(),
        viewport_height,
    );

    let mut json_view = Paragraph::new(visible_lines);
    if app.wrap_lines() {
        json_view = json_view.wrap(Wrap { trim: false });
    } else {
        json_view = json_view.scroll((0, app.horizontal_offset() as u16));
    }
    frame.render_widget(json_view, chunks[0]);

    render_filter_input(frame, app, chunks[1]);

    let total = app.total_lines();
    let start = app.scroll_offset() + 1;
    let end = (app.scroll_offset() + viewport_height).min(total);
    let wrap_indicator = if app.wrap_lines() { " [wrap]" } else { "" };
    let input_indicator = if app.input_count() > 1 {
        format!(" ({} inputs)", app.input_count())
    } else {
        String::new()
    };
    let mode_indicators = {
        let mut modes = Vec::new();
        if app.raw_output() {
            modes.push("raw");
        }
        if app.slurp_mode() {
            modes.push("slurp");
        }
        if app.compact_output() {
            modes.push("compact");
        }
        if modes.is_empty() {
            String::new()
        } else {
            format!(" [{}]", modes.join("+"))
        }
    };
    let eval_indicator = if app.is_evaluating() {
        " [eval...]"
    } else if app.eval_paused() {
        " [paused]"
    } else {
        ""
    };
    let status = if total == 0 {
        format!(
            " (empty){}{}{}{}  ^h=help",
            input_indicator, wrap_indicator, mode_indicators, eval_indicator
        )
    } else if app.horizontal_offset() > 0 && !app.wrap_lines() {
        format!(
            " Lines {}-{} of {} | Col {}{}{}{}{}  ^h=help",
            start,
            end,
            total,
            app.horizontal_offset() + 1,
            input_indicator,
            wrap_indicator,
            mode_indicators,
            eval_indicator
        )
    } else {
        format!(
            " Lines {}-{} of {}{}{}{}{}  ^h=help",
            start, end, total, input_indicator, wrap_indicator, mode_indicators, eval_indicator
        )
    };
    let status_line = if app.is_evaluating() {
        Paragraph::new(status).style(Style::default().fg(Color::Yellow))
    } else {
        Paragraph::new(status).style(Style::default().fg(Color::DarkGray))
    };
    frame.render_widget(status_line, chunks[2]);

    if app.show_help() {
        render_help_popup(frame, app);
    }
}

fn render_filter_input(frame: &mut Frame, app: &App, area: Rect) {
    let prompt = if app.partial_eval_mode() {
        "Filter[→]> "
    } else if app.editing_filter() {
        "Filter> "
    } else {
        "Filter: "
    };
    let prompt_style = if app.partial_eval_mode() {
        Style::default().fg(Color::Magenta)
    } else if app.editing_filter() {
        Style::default().fg(Color::Yellow)
    } else {
        Style::default().fg(Color::DarkGray)
    };

    if let Some(error) = app.filter_error() {
        let filter_area = Rect { height: 1, ..area };
        let error_area = Rect {
            y: area.y + 1,
            height: 1,
            ..area
        };

        let filter_text = app.filter_text();
        let error_pos = match error {
            crate::error::FilterError::Parse(e) => e.position,
            crate::error::FilterError::Eval(e) => e.position(),
        };
        let (valid_part, invalid_part) = filter_text.split_at(error_pos.min(filter_text.len()));

        let filter_line = Line::from(vec![
            Span::styled(prompt, prompt_style),
            Span::raw(valid_part),
            Span::styled(invalid_part, Style::default().fg(Color::Red)),
        ]);
        frame.render_widget(Paragraph::new(filter_line), filter_area);

        let error_line = Line::from(vec![
            Span::styled(" Error: ", Style::default().fg(Color::Red)),
            Span::styled(error.to_string(), Style::default().fg(Color::Red).dim()),
        ]);
        frame.render_widget(Paragraph::new(error_line), error_area);
    } else {
        let filter_line = Line::from(vec![
            Span::styled(prompt, prompt_style),
            Span::raw(app.filter_text()),
        ]);
        frame.render_widget(Paragraph::new(filter_line), area);
    }

    if app.editing_filter() {
        let cursor_x = area.x + prompt.width() as u16 + app.filter_cursor_display_col() as u16;
        frame.set_cursor_position((cursor_x, area.y));
    }
}

fn render_help_popup(frame: &mut Frame, app: &mut App) {
    let lines = if app.help_for_filter_mode() {
        build_filter_help_lines()
    } else {
        build_navigation_help_lines()
    };

    render_scrollable_help(frame, app, &lines);
}

fn help_entry(key: &str, desc: &str) -> Line<'static> {
    Line::from(vec![
        Span::styled(format!("{:<16}", key), Style::default().fg(Color::Cyan)),
        Span::raw(desc.to_string()),
    ])
}

fn build_filter_help_lines() -> Vec<Line<'static>> {
    vec![
        Line::from("SYNTAX".bold()),
        help_entry(".", "Identity (current value)"),
        help_entry(".foo", "Access field"),
        help_entry(".[0]", "Access array index"),
        help_entry(".[]", "Iterate array/object"),
        help_entry(".[2:5]", "Array slice"),
        help_entry("a | b", "Pipe output to next filter"),
        help_entry("[.a, .b]", "Construct array"),
        help_entry("{a: .x}", "Construct object"),
        help_entry("//", "Alternative (fallback)"),
        help_entry("and", "Boolean AND"),
        help_entry("or", "Boolean OR"),
        help_entry("not", "Boolean NOT"),
        help_entry("==", "Equal"),
        help_entry("!=", "Not equal"),
        help_entry("< >", "Less/greater than"),
        help_entry("<= >=", "Less/greater or equal"),
        help_entry("if-then-else", "Conditional expression"),
        Line::from(""),
        Line::from("FUNCTIONS".bold()),
        help_entry("map(f)", "Apply f to each array element"),
        help_entry("select(f)", "Keep values where f is truthy"),
        help_entry("sort_by(f)", "Sort array by extracted key"),
        help_entry("group_by(f)", "Group array elements by key"),
        help_entry("unique_by(f)", "Remove duplicates by key"),
        help_entry("min_by(f)", "Minimum by extracted key"),
        help_entry("max_by(f)", "Maximum by extracted key"),
        help_entry("has(k)", "Check if key exists"),
        help_entry("contains(v)", "Check if value contains v"),
        help_entry("split(s)", "Split string by separator"),
        help_entry("join(s)", "Join array with separator"),
        help_entry("with_entries(f)", "Transform object entries"),
        Line::from(""),
        Line::from("BUILTINS".bold()),
        help_entry("length", "Length of string/array/object"),
        help_entry("keys", "Get object keys as array"),
        help_entry("values", "Get object values as array"),
        help_entry("type", "Get JSON type as string"),
        help_entry("sort", "Sort array"),
        help_entry("reverse", "Reverse array"),
        help_entry("unique", "Remove duplicate values"),
        help_entry("flatten", "Flatten nested arrays"),
        help_entry("first", "First array element"),
        help_entry("last", "Last array element"),
        help_entry("min", "Minimum value"),
        help_entry("max", "Maximum value"),
        help_entry("add", "Sum numbers / concat arrays"),
        help_entry("to_entries", "Object to [{key,value}]"),
        help_entry("from_entries", "[{key,value}] to object"),
        help_entry("empty", "Produce no output"),
        help_entry("not", "Boolean negation"),
        Line::from(""),
        Line::from("KEYS".bold()),
        help_entry("Esc", "Exit filter edit mode"),
        help_entry("Tab", "Partial eval (cache prefix)"),
        help_entry("Ctrl+p", "Pause/resume auto-eval"),
        help_entry("Ctrl+c", "Cancel current evaluation"),
        help_entry("Ctrl+s", "Toggle slurp mode"),
        help_entry("Ctrl+r", "Toggle raw output"),
        help_entry("Ctrl+o", "Toggle compact output"),
        help_entry("Ctrl+h", "Toggle this help"),
    ]
}

fn build_navigation_help_lines() -> Vec<Line<'static>> {
    vec![
        Line::from("NAVIGATION".bold()),
        help_entry("j / ↓", "Scroll down"),
        help_entry("k / ↑", "Scroll up"),
        help_entry("h / ←", "Scroll left"),
        help_entry("l / →", "Scroll right"),
        help_entry("g", "Go to top"),
        help_entry("G", "Go to bottom"),
        help_entry("0", "Go to line start"),
        help_entry("Ctrl+d", "Half page down"),
        help_entry("Ctrl+u", "Half page up"),
        help_entry("PgDn", "Full page down"),
        help_entry("PgUp", "Full page up"),
        Line::from(""),
        Line::from("DISPLAY".bold()),
        help_entry("w", "Toggle line wrapping"),
        help_entry("r", "Toggle raw output"),
        help_entry("o", "Toggle compact output"),
        help_entry("s", "Toggle slurp mode"),
        Line::from(""),
        Line::from("FILTER".bold()),
        help_entry("i", "Edit filter"),
        help_entry("/", "Edit filter (alternative)"),
        help_entry("y", "Copy filter as jq command"),
        Line::from(""),
        Line::from("GENERAL".bold()),
        help_entry("Ctrl+h", "Toggle this help"),
        help_entry("q", "Quit"),
        help_entry("Esc", "Quit (alternative)"),
    ]
}

fn render_scrollable_help(frame: &mut Frame, app: &mut App, lines: &[Line<'static>]) {
    let area = frame.area();

    // Calculate popup dimensions
    let content_width = 54u16; // Fixed width for consistent layout
    let content_height = lines.len();

    // Leave some margin around the popup
    let max_popup_width = area.width.saturating_sub(4);
    let max_popup_height = area.height.saturating_sub(4);

    let popup_width = content_width.min(max_popup_width);
    // +2 for borders, +1 for title
    let popup_height = (content_height as u16 + 3).min(max_popup_height);
    let inner_height = popup_height.saturating_sub(2) as usize; // subtract borders

    // Calculate max scroll
    let max_scroll = content_height.saturating_sub(inner_height);
    app.clamp_help_scroll(max_scroll);

    let scroll = app.help_scroll();
    let needs_scroll = content_height > inner_height;

    // Center the popup
    let popup_area = centered_rect(popup_width, popup_height, area);

    // Build title with scroll indicator
    let title = if needs_scroll {
        format!(
            " Help [{}-{}/{}] ",
            scroll + 1,
            (scroll + inner_height).min(content_height),
            content_height
        )
    } else {
        " Help ".to_string()
    };

    let block = Block::bordered().title(title);
    let inner = block.inner(popup_area);

    frame.render_widget(Clear, popup_area);
    frame.render_widget(block, popup_area);

    // Render visible lines
    let visible_lines: Vec<Line> = lines
        .iter()
        .skip(scroll)
        .take(inner_height)
        .cloned()
        .collect();

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

    // Draw scroll indicators if needed
    if needs_scroll {
        // Show ▲ at top if scrolled down
        if scroll > 0 {
            let indicator = Span::styled("▲", Style::default().fg(Color::DarkGray));
            let indicator_area = Rect {
                x: popup_area.x + popup_width - 2,
                y: popup_area.y,
                width: 1,
                height: 1,
            };
            frame.render_widget(Paragraph::new(Line::from(indicator)), indicator_area);
        }

        // Show ▼ at bottom if more content below
        if scroll < max_scroll {
            let indicator = Span::styled("▼", Style::default().fg(Color::DarkGray));
            let indicator_area = Rect {
                x: popup_area.x + popup_width - 2,
                y: popup_area.y + popup_height - 1,
                width: 1,
                height: 1,
            };
            frame.render_widget(Paragraph::new(Line::from(indicator)), indicator_area);
        }
    }
}

fn centered_rect(width: u16, height: u16, area: Rect) -> Rect {
    let vertical = Layout::vertical([Constraint::Length(height)]).flex(Flex::Center);
    let horizontal = Layout::horizontal([Constraint::Length(width)]).flex(Flex::Center);
    let [area] = vertical.areas(area);
    let [area] = horizontal.areas(area);
    area
}

fn render_loading_screen(frame: &mut Frame, message: &str) {
    let area = frame.area();
    let text = Paragraph::new(message)
        .style(Style::default().fg(Color::Yellow))
        .alignment(Alignment::Center);
    let centered = centered_rect(30, 3, area);
    frame.render_widget(text, centered);
}

fn render_error_screen(frame: &mut Frame, message: &str) {
    let area = frame.area();
    let lines = vec![
        Line::from(Span::styled(
            "Error loading input",
            Style::default().fg(Color::Red).bold(),
        )),
        Line::from(""),
        Line::from(Span::styled(
            message.to_string(),
            Style::default().fg(Color::Red),
        )),
        Line::from(""),
        Line::from(Span::styled(
            "Press 'q' to quit",
            Style::default().fg(Color::DarkGray),
        )),
    ];
    let text = Paragraph::new(lines).alignment(Alignment::Center);
    let height = 5.min(area.height);
    let width = (message.len() as u16 + 4).min(area.width).max(25);
    let centered = centered_rect(width, height, area);
    frame.render_widget(text, centered);
}

#[derive(Default, Clone)]
pub struct RenderOptions {
    pub raw_output: bool,
    pub compact: bool,
}

// ============================================================================
// Virtualized Rendering - Line Counting
// ============================================================================

/// Count total lines for multiple values without allocating Line objects
pub fn count_total_lines(values: &[Value], options: &RenderOptions) -> usize {
    values.iter().map(|v| count_value_lines(v, options)).sum()
}

/// Count lines for a single value
fn count_value_lines(value: &Value, options: &RenderOptions) -> usize {
    if options.compact {
        count_lines_compact(value, options.raw_output)
    } else {
        count_lines_pretty(value, options.raw_output)
    }
}

fn count_lines_compact(value: &Value, raw: bool) -> usize {
    if raw && let Value::String(s) = value {
        return s.lines().count().max(1);
    }
    1 // Compact is always 1 line (except raw strings with newlines)
}

fn count_lines_pretty(value: &Value, raw: bool) -> usize {
    if raw && let Value::String(s) = value {
        return s.lines().count().max(1);
    }
    match value {
        Value::Static(_) | Value::String(_) => 1,
        Value::Array(arr) if arr.is_empty() => 1,
        Value::Object(obj) if obj.is_empty() => 1,
        Value::Array(arr) => {
            // Opening bracket + items + closing bracket
            2 + arr
                .iter()
                .map(|v| count_lines_pretty(v, false))
                .sum::<usize>()
        }
        Value::Object(obj) => {
            // Opening brace + entries + closing brace
            2 + obj
                .values()
                .map(|v| count_lines_pretty(v, false))
                .sum::<usize>()
        }
    }
}

// ============================================================================
// Virtualized Rendering - Viewport Rendering
// ============================================================================

/// Render only lines in range [skip, skip + take) for multiple values
pub fn render_viewport(
    values: &[Value],
    options: &RenderOptions,
    skip: usize,
    take: usize,
) -> Vec<Line<'static>> {
    let mut lines = Vec::with_capacity(take);
    let mut remaining_skip = skip;

    for value in values {
        if lines.len() >= take {
            break;
        }

        let value_lines = count_value_lines(value, options);

        // Skip entire value if it ends before our viewport starts
        if remaining_skip >= value_lines {
            remaining_skip -= value_lines;
            continue;
        }

        // Render this value with skip/take
        if options.compact {
            render_value_viewport_compact(
                value,
                options.raw_output,
                &mut lines,
                &mut remaining_skip,
                take,
            );
        } else {
            render_value_viewport_pretty(
                value,
                None,
                0,
                &mut lines,
                false,
                &mut remaining_skip,
                take,
                options.raw_output,
            );
        }
    }

    lines
}

fn render_value_viewport_compact(
    value: &Value,
    raw: bool,
    lines: &mut Vec<Line<'static>>,
    remaining_skip: &mut usize,
    take: usize,
) {
    if lines.len() >= take {
        return;
    }

    // Handle raw strings with newlines
    if raw && let Value::String(s) = value {
        for line_str in s.lines() {
            if *remaining_skip > 0 {
                *remaining_skip -= 1;
                continue;
            }
            if lines.len() >= take {
                return;
            }
            lines.push(Line::raw(line_str.to_string()));
        }
        return;
    }

    // Single line for compact
    if *remaining_skip > 0 {
        *remaining_skip -= 1;
        return;
    }

    let mut spans = Vec::new();
    render_value_compact(value, &mut spans);
    lines.push(Line::from(spans));
}

#[allow(clippy::too_many_arguments)]
fn render_value_viewport_pretty(
    value: &Value,
    key: Option<&str>,
    depth: usize,
    lines: &mut Vec<Line<'static>>,
    trailing_comma: bool,
    remaining_skip: &mut usize,
    take: usize,
    raw: bool,
) {
    if lines.len() >= take {
        return;
    }

    // Handle raw strings with newlines at top level (depth 0, no key)
    if raw
        && depth == 0
        && key.is_none()
        && let Value::String(s) = value
    {
        for line_str in s.lines() {
            if *remaining_skip > 0 {
                *remaining_skip -= 1;
                continue;
            }
            if lines.len() >= take {
                return;
            }
            lines.push(Line::raw(line_str.to_string()));
        }
        return;
    }

    let indent = INDENT.repeat(depth);
    let comma_span = if trailing_comma {
        Span::styled(",", Style::default().fg(Color::DarkGray))
    } else {
        Span::raw("")
    };

    match value {
        // Primitive values: 1 line
        Value::Static(_) | Value::String(_) => {
            if *remaining_skip > 0 {
                *remaining_skip -= 1;
                return;
            }
            let mut spans = vec![Span::raw(indent)];
            if let Some(k) = key {
                spans.extend(key_spans(k));
            }
            spans.push(value_styled(value));
            spans.push(comma_span);
            lines.push(Line::from(spans));
        }

        Value::Array(arr) if arr.is_empty() => {
            if *remaining_skip > 0 {
                *remaining_skip -= 1;
                return;
            }
            let mut spans = vec![Span::raw(indent)];
            if let Some(k) = key {
                spans.extend(key_spans(k));
            }
            spans.push(Span::styled("[]", Style::default().fg(Color::DarkGray)));
            spans.push(comma_span);
            lines.push(Line::from(spans));
        }

        Value::Object(obj) if obj.is_empty() => {
            if *remaining_skip > 0 {
                *remaining_skip -= 1;
                return;
            }
            let mut spans = vec![Span::raw(indent)];
            if let Some(k) = key {
                spans.extend(key_spans(k));
            }
            spans.push(Span::styled("{}", Style::default().fg(Color::DarkGray)));
            spans.push(comma_span);
            lines.push(Line::from(spans));
        }

        Value::Array(arr) => {
            // Opening bracket
            if *remaining_skip > 0 {
                *remaining_skip -= 1;
            } else if lines.len() < take {
                let mut spans = vec![Span::raw(indent.clone())];
                if let Some(k) = key {
                    spans.extend(key_spans(k));
                }
                spans.push(Span::styled("[", Style::default().fg(Color::DarkGray)));
                lines.push(Line::from(spans));
            }

            // Array items
            for (i, item) in arr.iter().enumerate() {
                if lines.len() >= take {
                    return;
                }
                let is_last = i == arr.len() - 1;
                render_value_viewport_pretty(
                    item,
                    None,
                    depth + 1,
                    lines,
                    !is_last,
                    remaining_skip,
                    take,
                    false,
                );
            }

            // Closing bracket
            if lines.len() >= take {
                return;
            }
            if *remaining_skip > 0 {
                *remaining_skip -= 1;
            } else {
                lines.push(Line::from(vec![
                    Span::raw(indent),
                    Span::styled("]", Style::default().fg(Color::DarkGray)),
                    comma_span,
                ]));
            }
        }

        Value::Object(obj) => {
            // Opening brace
            if *remaining_skip > 0 {
                *remaining_skip -= 1;
            } else if lines.len() < take {
                let mut spans = vec![Span::raw(indent.clone())];
                if let Some(k) = key {
                    spans.extend(key_spans(k));
                }
                spans.push(Span::styled("{", Style::default().fg(Color::DarkGray)));
                lines.push(Line::from(spans));
            }

            // Object entries
            let entries: Vec<_> = obj.iter().collect();
            for (i, (k, v)) in entries.iter().enumerate() {
                if lines.len() >= take {
                    return;
                }
                let is_last = i == entries.len() - 1;
                render_value_viewport_pretty(
                    v,
                    Some(k),
                    depth + 1,
                    lines,
                    !is_last,
                    remaining_skip,
                    take,
                    false,
                );
            }

            // Closing brace
            if lines.len() >= take {
                return;
            }
            if *remaining_skip > 0 {
                *remaining_skip -= 1;
            } else {
                lines.push(Line::from(vec![
                    Span::raw(indent),
                    Span::styled("}", Style::default().fg(Color::DarkGray)),
                    comma_span,
                ]));
            }
        }
    }
}

fn render_value_compact(value: &Value, spans: &mut Vec<Span<'static>>) {
    match value {
        Value::Static(StaticNode::Null) => {
            spans.push(Span::styled("null", Style::default().fg(Color::Magenta)))
        }
        Value::Static(StaticNode::Bool(b)) => spans.push(Span::styled(
            b.to_string(),
            Style::default().fg(Color::Magenta),
        )),
        Value::Static(StaticNode::I64(n)) => spans.push(Span::styled(
            n.to_string(),
            Style::default().fg(Color::Yellow),
        )),
        Value::Static(StaticNode::U64(n)) => spans.push(Span::styled(
            n.to_string(),
            Style::default().fg(Color::Yellow),
        )),
        Value::Static(StaticNode::F64(n)) => spans.push(Span::styled(
            n.to_string(),
            Style::default().fg(Color::Yellow),
        )),
        Value::String(s) => spans.push(Span::styled(
            format!("\"{}\"", escape_string(s)),
            Style::default().fg(Color::Green),
        )),
        Value::Array(arr) => {
            spans.push(Span::styled("[", Style::default().fg(Color::DarkGray)));
            for (i, v) in arr.iter().enumerate() {
                if i > 0 {
                    spans.push(Span::styled(",", Style::default().fg(Color::DarkGray)));
                }
                render_value_compact(v, spans);
            }
            spans.push(Span::styled("]", Style::default().fg(Color::DarkGray)));
        }
        Value::Object(obj) => {
            spans.push(Span::styled("{", Style::default().fg(Color::DarkGray)));
            for (i, (k, v)) in obj.iter().enumerate() {
                if i > 0 {
                    spans.push(Span::styled(",", Style::default().fg(Color::DarkGray)));
                }
                spans.push(Span::styled(
                    format!("\"{}\"", escape_string(k)),
                    Style::default().fg(Color::Cyan),
                ));
                spans.push(Span::styled(":", Style::default().fg(Color::DarkGray)));
                render_value_compact(v, spans);
            }
            spans.push(Span::styled("}", Style::default().fg(Color::DarkGray)));
        }
    }
}

/// Get styled representation of a primitive value.
fn value_styled(value: &Value) -> Span<'static> {
    match value {
        Value::Static(StaticNode::Null) => {
            Span::styled("null", Style::default().fg(Color::Magenta))
        }
        Value::Static(StaticNode::Bool(b)) => {
            Span::styled(b.to_string(), Style::default().fg(Color::Magenta))
        }
        Value::Static(StaticNode::I64(n)) => {
            Span::styled(n.to_string(), Style::default().fg(Color::Yellow))
        }
        Value::Static(StaticNode::U64(n)) => {
            Span::styled(n.to_string(), Style::default().fg(Color::Yellow))
        }
        Value::Static(StaticNode::F64(n)) => {
            Span::styled(n.to_string(), Style::default().fg(Color::Yellow))
        }
        Value::String(s) => Span::styled(
            format!("\"{}\"", escape_string(s)),
            Style::default().fg(Color::Green),
        ),
        _ => unreachable!("value_styled only handles primitives"),
    }
}

/// Get spans for an object key (quoted key + colon).
fn key_spans(key: &str) -> Vec<Span<'static>> {
    vec![
        Span::styled(
            format!("\"{}\"", escape_string(key)),
            Style::default().fg(Color::Cyan),
        ),
        Span::styled(": ", Style::default().fg(Color::DarkGray)),
    ]
}

fn escape_string(s: &str) -> String {
    let mut result = String::with_capacity(s.len());
    for c in s.chars() {
        match c {
            '"' => result.push_str("\\\""),
            '\\' => result.push_str("\\\\"),
            '\n' => result.push_str("\\n"),
            '\r' => result.push_str("\\r"),
            '\t' => result.push_str("\\t"),
            c if c.is_control() => {
                result.push_str(&format!("\\u{:04x}", c as u32));
            }
            c => result.push(c),
        }
    }
    result
}

#[cfg(test)]
mod tests {
    use super::*;
    use simd_json::json;

    fn render_all(values: &[Value], options: &RenderOptions) -> Vec<Line<'static>> {
        let total = count_total_lines(values, options);
        render_viewport(values, options, 0, total)
    }

    #[test]
    fn test_raw_string_output() {
        let value = json!("hello world");
        let options = RenderOptions {
            raw_output: true,
            compact: false,
        };
        let lines = render_all(&[value], &options);
        assert_eq!(lines.len(), 1);
        assert_eq!(lines[0].to_string(), "hello world");
    }

    #[test]
    fn test_raw_non_string_unchanged() {
        let value = json!(42);
        let options = RenderOptions {
            raw_output: true,
            compact: false,
        };
        let lines = render_all(&[value], &options);
        assert_eq!(lines.len(), 1);
        assert_eq!(lines[0].to_string(), "42");
    }

    #[test]
    fn test_raw_null_unchanged() {
        let value = json!(null);
        let options = RenderOptions {
            raw_output: true,
            compact: false,
        };
        let lines = render_all(&[value], &options);
        assert_eq!(lines.len(), 1);
        assert_eq!(lines[0].to_string(), "null");
    }

    #[test]
    fn test_raw_string_with_newline() {
        let value = json!("line1\nline2");
        let options = RenderOptions {
            raw_output: true,
            compact: false,
        };
        let lines = render_all(&[value], &options);
        assert_eq!(lines.len(), 2);
        assert_eq!(lines[0].to_string(), "line1");
        assert_eq!(lines[1].to_string(), "line2");
    }

    #[test]
    fn test_compact_output() {
        let value = json!({"a": 1, "b": 2});
        let options = RenderOptions {
            raw_output: false,
            compact: true,
        };
        let lines = render_all(&[value], &options);
        assert_eq!(lines.len(), 1);
        let text = lines[0].to_string();
        assert!(text.contains("\"a\":1") || text.contains("\"a\": 1"));
    }

    #[test]
    fn test_compact_array() {
        let value = json!([1, 2, 3]);
        let options = RenderOptions {
            raw_output: false,
            compact: true,
        };
        let lines = render_all(&[value], &options);
        assert_eq!(lines.len(), 1);
        assert_eq!(lines[0].to_string(), "[1,2,3]");
    }

    #[test]
    fn test_compact_raw_string() {
        let value = json!("test");
        let options = RenderOptions {
            raw_output: true,
            compact: true,
        };
        let lines = render_all(&[value], &options);
        assert_eq!(lines.len(), 1);
        assert_eq!(lines[0].to_string(), "test");
    }

    #[test]
    fn test_compact_raw_non_string() {
        let value = json!({"x": 1});
        let options = RenderOptions {
            raw_output: true,
            compact: true,
        };
        let lines = render_all(&[value], &options);
        assert_eq!(lines.len(), 1);
        // Raw doesn't affect non-strings
        assert!(lines[0].to_string().contains("\"x\""));
    }

    #[test]
    fn test_line_count_simple() {
        let value = json!({"name": "alice"});
        let options = RenderOptions::default();
        // { + "name": ... + } = 3 lines
        assert_eq!(count_total_lines(&[value], &options), 3);
    }

    #[test]
    fn test_viewport_skip_and_take() {
        let value = json!([1, 2, 3, 4, 5]);
        let options = RenderOptions::default();
        // [ + 5 items + ] = 7 lines
        assert_eq!(count_total_lines(std::slice::from_ref(&value), &options), 7);

        // Skip 2 ([ and first item), take 2 (second and third items)
        let lines = render_viewport(&[value], &options, 2, 2);
        assert_eq!(lines.len(), 2);
        assert!(lines[0].to_string().contains("2"));
        assert!(lines[1].to_string().contains("3"));
    }
}
