use ratatui::layout::Constraint;
use ratatui::layout::Direction;
use ratatui::layout::Layout;
use ratatui::prelude::*;
use ratatui::style::Color;
use ratatui::style::Modifier;
use ratatui::style::Style;
use ratatui::style::Stylize;
use ratatui::widgets::Block;
use ratatui::widgets::BorderType;
use ratatui::widgets::Borders;
use ratatui::widgets::Clear;
use ratatui::widgets::List;
use ratatui::widgets::ListItem;
use ratatui::widgets::ListState;
use ratatui::widgets::Padding;
use ratatui::widgets::Paragraph;
use std::sync::OnceLock;
use std::time::Instant;

use crate::app::App;
use crate::app::AttemptView;
use crate::util::format_relative_time_now;
use codex_cloud_tasks_client::AttemptStatus;
use codex_cloud_tasks_client::TaskStatus;
use codex_tui::render_markdown_text;

pub fn draw(frame: &mut Frame, app: &mut App) {
    let area = frame.area();
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Min(1),    // list
            Constraint::Length(2), // two-line footer (help + status)
        ])
        .split(area);
    if app.new_task.is_some() {
        draw_new_task_page(frame, chunks[0], app);
        draw_footer(frame, chunks[1], app);
    } else {
        draw_list(frame, chunks[0], app);
        draw_footer(frame, chunks[1], app);
    }

    if app.diff_overlay.is_some() {
        draw_diff_overlay(frame, area, app);
    }
    if app.env_modal.is_some() {
        draw_env_modal(frame, area, app);
    }
    if app.best_of_modal.is_some() {
        draw_best_of_modal(frame, area, app);
    }
    if app.apply_modal.is_some() {
        draw_apply_modal(frame, area, app);
    }
}

// ===== Overlay helpers (geometry + styling) =====
static ROUNDED: OnceLock<bool> = OnceLock::new();

fn rounded_enabled() -> bool {
    *ROUNDED.get_or_init(|| {
        std::env::var("CODEX_TUI_ROUNDED")
            .ok()
            .map(|v| v == "1")
            .unwrap_or(true)
    })
}

fn overlay_outer(area: Rect) -> Rect {
    let outer_v = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Percentage(10),
            Constraint::Percentage(80),
            Constraint::Percentage(10),
        ])
        .split(area)[1];
    Layout::default()
        .direction(Direction::Horizontal)
        .constraints([
            Constraint::Percentage(10),
            Constraint::Percentage(80),
            Constraint::Percentage(10),
        ])
        .split(outer_v)[1]
}

fn overlay_block() -> Block<'static> {
    let base = Block::default().borders(Borders::ALL);
    let base = if rounded_enabled() {
        base.border_type(BorderType::Rounded)
    } else {
        base
    };
    base.padding(Padding::new(2, 2, 1, 1))
}

fn overlay_content(area: Rect) -> Rect {
    overlay_block().inner(area)
}

pub fn draw_new_task_page(frame: &mut Frame, area: Rect, app: &mut App) {
    let title_spans = {
        let mut spans: Vec<ratatui::text::Span> = vec!["New Task".magenta().bold()];
        if let Some(id) = app
            .new_task
            .as_ref()
            .and_then(|p| p.env_id.as_ref())
            .cloned()
        {
            spans.push("  • ".into());
            // Try to map id to label
            let label = app
                .environments
                .iter()
                .find(|r| r.id == id)
                .and_then(|r| r.label.clone())
                .unwrap_or(id);
            spans.push(label.dim());
        } else {
            spans.push("  • ".into());
            spans.push("Env: none (press ctrl-o to choose)".red());
        }
        if let Some(page) = app.new_task.as_ref() {
            spans.push("  • ".into());
            let attempts = page.best_of_n;
            let label = format!(
                "{} attempt{}",
                attempts,
                if attempts == 1 { "" } else { "s" }
            );
            spans.push(label.cyan());
        }
        spans
    };
    let block = Block::default()
        .borders(Borders::ALL)
        .title(Line::from(title_spans));

    frame.render_widget(Clear, area);
    frame.render_widget(block.clone(), area);
    let content = block.inner(area);

    // Expand composer height up to (terminal height - 6), with a 3-line minimum.
    let max_allowed = frame.area().height.saturating_sub(6).max(3);
    let desired = app
        .new_task
        .as_ref()
        .map(|p| p.composer.desired_height(content.width))
        .unwrap_or(3)
        .clamp(3, max_allowed);

    // Anchor the composer to the bottom-left by allocating a flexible spacer
    // above it and a fixed `desired`-height area for the composer.
    let rows = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Min(1), Constraint::Length(desired)])
        .split(content);
    let composer_area = rows[1];

    if let Some(page) = app.new_task.as_ref() {
        page.composer.render_ref(composer_area, frame.buffer_mut());
        // Composer renders its own footer hints; no extra row here.
    }

    // Place cursor where composer wants it
    if let Some(page) = app.new_task.as_ref()
        && let Some((x, y)) = page.composer.cursor_pos(composer_area)
    {
        frame.set_cursor_position((x, y));
    }
}

fn draw_list(frame: &mut Frame, area: Rect, app: &mut App) {
    let items: Vec<ListItem> = app.tasks.iter().map(|t| render_task_item(app, t)).collect();

    // Selection reflects the actual task index (no artificial spacer item).
    let mut state = ListState::default().with_selected(Some(app.selected));
    // Dim task list when a modal/overlay is active to emphasize focus.
    let dim_bg = app.env_modal.is_some()
        || app.apply_modal.is_some()
        || app.best_of_modal.is_some()
        || app.diff_overlay.is_some();
    // Dynamic title includes current environment filter
    let suffix_span = if let Some(ref id) = app.env_filter {
        let label = app
            .environments
            .iter()
            .find(|r| &r.id == id)
            .and_then(|r| r.label.clone())
            .unwrap_or_else(|| "Selected".to_string());
        format!(" • {label}").dim()
    } else {
        " • All".dim()
    };
    // Percent scrolled based on selection position in the list (0% at top, 100% at bottom).
    let percent_span = if app.tasks.len() <= 1 {
        "  • 0%".dim()
    } else {
        let p = ((app.selected as f32) / ((app.tasks.len() - 1) as f32) * 100.0).round() as i32;
        format!("  • {}%", p.clamp(0, 100)).dim()
    };
    let title_line = {
        let base = Line::from(vec!["Cloud Tasks".into(), suffix_span, percent_span]);
        if dim_bg {
            base.style(Style::default().add_modifier(Modifier::DIM))
        } else {
            base
        }
    };
    let block = Block::default().borders(Borders::ALL).title(title_line);
    // Render the outer block first
    frame.render_widget(block.clone(), area);
    // Draw list inside with a persistent top spacer row
    let inner = block.inner(area);
    let rows = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Length(1), Constraint::Min(1)])
        .split(inner);
    let mut list = List::new(items)
        .highlight_symbol("› ")
        .highlight_style(Style::default().bold());
    if dim_bg {
        list = list.style(Style::default().add_modifier(Modifier::DIM));
    }
    frame.render_stateful_widget(list, rows[1], &mut state);

    // In-box spinner during initial/refresh loads
    if app.refresh_inflight {
        draw_centered_spinner(frame, inner, &mut app.spinner_start, "Loading tasks…");
    }
}

fn draw_footer(frame: &mut Frame, area: Rect, app: &mut App) {
    let mut help = vec![
        "↑/↓".dim(),
        ": Move  ".dim(),
        "r".dim(),
        ": Refresh  ".dim(),
        "Enter".dim(),
        ": Open  ".dim(),
    ];
    // Apply hint; show disabled note when overlay is open without a diff.
    if let Some(ov) = app.diff_overlay.as_ref() {
        if !ov.current_can_apply() {
            help.push("a".dim());
            help.push(": Apply (disabled)  ".dim());
        } else {
            help.push("a".dim());
            help.push(": Apply  ".dim());
        }
        if ov.attempt_count() > 1 {
            help.push("Tab".dim());
            help.push(": Next attempt  ".dim());
            help.push("[ ]".dim());
            help.push(": Cycle attempts  ".dim());
        }
    } else {
        help.push("a".dim());
        help.push(": Apply  ".dim());
    }
    help.push("o : Set Env  ".dim());
    if app.new_task.is_some() {
        help.push("Ctrl+N".dim());
        help.push(format!(": Attempts {}x  ", app.best_of_n).dim());
        help.push("(editing new task)  ".dim());
    } else {
        help.push("n : New Task  ".dim());
    }
    help.extend(vec!["q".dim(), ": Quit  ".dim()]);
    // Split footer area into two rows: help+spinner (top) and status (bottom)
    let rows = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Length(1), Constraint::Length(1)])
        .split(area);

    // Top row: help text + spinner at right
    let top = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([Constraint::Fill(1), Constraint::Length(18)])
        .split(rows[0]);
    let para = Paragraph::new(Line::from(help));
    // Draw help text; avoid clearing the whole footer area every frame.
    frame.render_widget(para, top[0]);
    // Right side: spinner or clear the spinner area if idle to prevent stale glyphs.
    if app.refresh_inflight
        || app.details_inflight
        || app.env_loading
        || app.apply_preflight_inflight
        || app.apply_inflight
    {
        draw_inline_spinner(frame, top[1], &mut app.spinner_start, "Loading…");
    } else {
        frame.render_widget(Clear, top[1]);
    }

    // Bottom row: status/log text across full width (single-line; sanitize newlines)
    let mut status_line = app.status.replace('\n', " ");
    if status_line.len() > 2000 {
        // hard cap to avoid TUI noise
        status_line.truncate(2000);
        status_line.push('…');
    }
    // Clear the status row to avoid trailing characters when the message shrinks.
    frame.render_widget(Clear, rows[1]);
    let status = Paragraph::new(status_line);
    frame.render_widget(status, rows[1]);
}

fn draw_diff_overlay(frame: &mut Frame, area: Rect, app: &mut App) {
    let inner = overlay_outer(area);
    if app.diff_overlay.is_none() {
        return;
    }
    let ov_can_apply = app
        .diff_overlay
        .as_ref()
        .map(super::app::DiffOverlay::current_can_apply)
        .unwrap_or(false);
    let is_error = app
        .diff_overlay
        .as_ref()
        .and_then(|o| o.sd.wrapped_lines().first().cloned())
        .map(|s| s.trim_start().starts_with("Task failed:"))
        .unwrap_or(false)
        && !ov_can_apply;
    let title = app
        .diff_overlay
        .as_ref()
        .map(|o| o.title.clone())
        .unwrap_or_default();

    // Title block
    let title_ref = title.as_str();
    let mut title_spans: Vec<ratatui::text::Span> = if is_error {
        vec![
            "Details ".magenta(),
            "[FAILED]".red().bold(),
            " ".into(),
            title_ref.magenta(),
        ]
    } else if ov_can_apply {
        vec!["Diff: ".magenta(), title_ref.magenta()]
    } else {
        vec!["Details: ".magenta(), title_ref.magenta()]
    };
    if let Some(p) = app
        .diff_overlay
        .as_ref()
        .and_then(|o| o.sd.percent_scrolled())
    {
        title_spans.push("  • ".dim());
        title_spans.push(format!("{p}%").dim());
    }
    frame.render_widget(Clear, inner);
    frame.render_widget(
        overlay_block().title(Line::from(title_spans)).clone(),
        inner,
    );

    // Content area and optional status bar
    let content_full = overlay_content(inner);
    let mut content_area = content_full;
    if let Some(ov) = app.diff_overlay.as_mut() {
        let has_text = ov.current_attempt().is_some_and(AttemptView::has_text);
        let has_diff = ov.current_attempt().is_some_and(AttemptView::has_diff) || ov.base_can_apply;
        if has_diff || has_text {
            let rows = Layout::default()
                .direction(Direction::Vertical)
                .constraints([Constraint::Length(1), Constraint::Min(1)])
                .split(content_full);
            // Status bar label
            let mut spans: Vec<ratatui::text::Span> = Vec::new();
            if has_diff && has_text {
                let prompt_lbl = if matches!(ov.current_view, crate::app::DetailView::Prompt) {
                    "[Prompt]".magenta().bold()
                } else {
                    "Prompt".dim()
                };
                let diff_lbl = if matches!(ov.current_view, crate::app::DetailView::Diff) {
                    "[Diff]".magenta().bold()
                } else {
                    "Diff".dim()
                };
                spans.extend(vec![
                    prompt_lbl,
                    "  ".into(),
                    diff_lbl,
                    "  ".into(),
                    "(← → to switch view)".dim(),
                ]);
            } else if has_text {
                spans.push("Conversation".magenta().bold());
            } else {
                spans.push("Diff".magenta().bold());
            }
            if let Some(total) = ov.expected_attempts().or({
                if ov.attempts.is_empty() {
                    None
                } else {
                    Some(ov.attempts.len())
                }
            }) && total > 1
            {
                spans.extend(vec![
                    "  ".into(),
                    format!("Attempt {}/{}", ov.selected_attempt + 1, total)
                        .bold()
                        .dim(),
                    "  ".into(),
                    "(Tab/Shift-Tab or [ ] to cycle attempts)".dim(),
                ]);
            }
            frame.render_widget(Paragraph::new(Line::from(spans)), rows[0]);
            ov.sd.set_width(rows[1].width);
            ov.sd.set_viewport(rows[1].height);
            content_area = rows[1];
        } else {
            ov.sd.set_width(content_full.width);
            ov.sd.set_viewport(content_full.height);
            content_area = content_full;
        }
    }

    // Styled content render
    // Choose styling by the active view, not just presence of a diff
    let is_diff_view = app
        .diff_overlay
        .as_ref()
        .map(|o| matches!(o.current_view, crate::app::DetailView::Diff))
        .unwrap_or(false);
    let styled_lines: Vec<Line<'static>> = if is_diff_view {
        let raw = app.diff_overlay.as_ref().map(|o| o.sd.wrapped_lines());
        raw.unwrap_or(&[])
            .iter()
            .map(|l| style_diff_line(l))
            .collect()
    } else {
        app.diff_overlay
            .as_ref()
            .map(|o| style_conversation_lines(&o.sd, o.current_attempt()))
            .unwrap_or_default()
    };
    let raw_empty = app
        .diff_overlay
        .as_ref()
        .map(|o| o.sd.wrapped_lines().is_empty())
        .unwrap_or(true);
    if app.details_inflight && raw_empty {
        draw_centered_spinner(
            frame,
            content_area,
            &mut app.spinner_start,
            "Loading details…",
        );
    } else {
        let scroll = app
            .diff_overlay
            .as_ref()
            .map(|o| o.sd.state.scroll)
            .unwrap_or(0);
        let content = Paragraph::new(Text::from(styled_lines)).scroll((scroll, 0));
        frame.render_widget(content, content_area);
    }
}

pub fn draw_apply_modal(frame: &mut Frame, area: Rect, app: &mut App) {
    use ratatui::widgets::Wrap;
    let inner = overlay_outer(area);
    let title = Line::from("Apply Changes?".magenta().bold());
    let block = overlay_block().title(title);
    frame.render_widget(Clear, inner);
    frame.render_widget(block.clone(), inner);
    let content = overlay_content(inner);

    if let Some(m) = &app.apply_modal {
        // Header
        let header = Paragraph::new(Line::from(
            format!("Apply '{}' ?", m.title).magenta().bold(),
        ))
        .wrap(Wrap { trim: true });
        // Footer instructions
        let footer =
            Paragraph::new(Line::from("Press Y to apply, P to preflight, N to cancel.").dim())
                .wrap(Wrap { trim: true });

        // Split into header/body/footer
        let rows = Layout::default()
            .direction(Direction::Vertical)
            .constraints([
                Constraint::Length(1),
                Constraint::Min(1),
                Constraint::Length(1),
            ])
            .split(content);

        frame.render_widget(header, rows[0]);
        // Body: spinner while preflight/apply runs; otherwise show result message and path lists
        if app.apply_preflight_inflight {
            draw_centered_spinner(frame, rows[1], &mut app.spinner_start, "Checking…");
        } else if app.apply_inflight {
            draw_centered_spinner(frame, rows[1], &mut app.spinner_start, "Applying…");
        } else if m.result_message.is_none() {
            draw_centered_spinner(frame, rows[1], &mut app.spinner_start, "Loading…");
        } else if let Some(msg) = &m.result_message {
            let mut body_lines: Vec<Line> = Vec::new();
            let first = match m.result_level {
                Some(crate::app::ApplyResultLevel::Success) => msg.clone().green(),
                Some(crate::app::ApplyResultLevel::Partial) => msg.clone().magenta(),
                Some(crate::app::ApplyResultLevel::Error) => msg.clone().red(),
                None => msg.clone().into(),
            };
            body_lines.push(Line::from(first));

            // On partial or error, show conflicts/skips if present
            if !matches!(m.result_level, Some(crate::app::ApplyResultLevel::Success)) {
                use ratatui::text::Span;
                if !m.conflict_paths.is_empty() {
                    body_lines.push(Line::from(""));
                    body_lines.push(
                        Line::from(format!("Conflicts ({}):", m.conflict_paths.len()))
                            .red()
                            .bold(),
                    );
                    for p in &m.conflict_paths {
                        body_lines
                            .push(Line::from(vec!["  • ".into(), Span::raw(p.clone()).dim()]));
                    }
                }
                if !m.skipped_paths.is_empty() {
                    body_lines.push(Line::from(""));
                    body_lines.push(
                        Line::from(format!("Skipped ({}):", m.skipped_paths.len()))
                            .magenta()
                            .bold(),
                    );
                    for p in &m.skipped_paths {
                        body_lines
                            .push(Line::from(vec!["  • ".into(), Span::raw(p.clone()).dim()]));
                    }
                }
            }
            let body = Paragraph::new(body_lines).wrap(Wrap { trim: true });
            frame.render_widget(body, rows[1]);
        }
        frame.render_widget(footer, rows[2]);
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ConversationSpeaker {
    User,
    Assistant,
}

fn style_conversation_lines(
    sd: &crate::scrollable_diff::ScrollableDiff,
    attempt: Option<&AttemptView>,
) -> Vec<Line<'static>> {
    use ratatui::text::Span;

    let wrapped = sd.wrapped_lines();
    if wrapped.is_empty() {
        return Vec::new();
    }

    let indices = sd.wrapped_src_indices();
    let mut styled: Vec<Line<'static>> = Vec::new();
    let mut speaker: Option<ConversationSpeaker> = None;
    let mut in_code = false;
    let mut last_src: Option<usize> = None;
    let mut bullet_indent: Option<usize> = None;

    for (display, &src_idx) in wrapped.iter().zip(indices.iter()) {
        let raw = sd.raw_line_at(src_idx);
        let trimmed = raw.trim();
        let is_new_raw = last_src.map(|prev| prev != src_idx).unwrap_or(true);

        if trimmed.eq_ignore_ascii_case("user:") {
            speaker = Some(ConversationSpeaker::User);
            in_code = false;
            bullet_indent = None;
            styled.push(conversation_header_line(ConversationSpeaker::User, None));
            last_src = Some(src_idx);
            continue;
        }
        if trimmed.eq_ignore_ascii_case("assistant:") {
            speaker = Some(ConversationSpeaker::Assistant);
            in_code = false;
            bullet_indent = None;
            styled.push(conversation_header_line(
                ConversationSpeaker::Assistant,
                attempt,
            ));
            last_src = Some(src_idx);
            continue;
        }
        if raw.is_empty() {
            let mut spans: Vec<Span> = Vec::new();
            if let Some(role) = speaker {
                spans.push(conversation_gutter_span(role));
            } else {
                spans.push(Span::raw(String::new()));
            }
            styled.push(Line::from(spans));
            last_src = Some(src_idx);
            bullet_indent = None;
            continue;
        }

        if is_new_raw {
            let trimmed_start = raw.trim_start();
            if trimmed_start.starts_with("```") {
                in_code = !in_code;
                bullet_indent = None;
            } else if !in_code
                && (trimmed_start.starts_with("- ") || trimmed_start.starts_with("* "))
            {
                let indent = raw.chars().take_while(|c| c.is_whitespace()).count();
                bullet_indent = Some(indent);
            } else if !in_code {
                bullet_indent = None;
            }
        }

        let mut spans: Vec<Span> = Vec::new();
        if let Some(role) = speaker {
            spans.push(conversation_gutter_span(role));
        }

        spans.extend(conversation_text_spans(
            display,
            in_code,
            is_new_raw,
            bullet_indent,
        ));

        styled.push(Line::from(spans));
        last_src = Some(src_idx);
    }

    if styled.is_empty() {
        wrapped.iter().map(|l| Line::from(l.to_string())).collect()
    } else {
        styled
    }
}

fn conversation_header_line(
    speaker: ConversationSpeaker,
    attempt: Option<&AttemptView>,
) -> Line<'static> {
    use ratatui::text::Span;

    let mut spans: Vec<Span> = vec!["╭ ".dim()];
    match speaker {
        ConversationSpeaker::User => {
            spans.push("User".cyan().bold());
            spans.push(" prompt".dim());
        }
        ConversationSpeaker::Assistant => {
            spans.push("Assistant".magenta().bold());
            spans.push(" response".dim());
            if let Some(attempt) = attempt
                && let Some(status_span) = attempt_status_span(attempt.status)
            {
                spans.push("  • ".dim());
                spans.push(status_span);
            }
        }
    }
    Line::from(spans)
}

fn conversation_gutter_span(speaker: ConversationSpeaker) -> ratatui::text::Span<'static> {
    match speaker {
        ConversationSpeaker::User => "│ ".cyan().dim(),
        ConversationSpeaker::Assistant => "│ ".magenta().dim(),
    }
}

fn conversation_text_spans(
    display: &str,
    in_code: bool,
    is_new_raw: bool,
    bullet_indent: Option<usize>,
) -> Vec<ratatui::text::Span<'static>> {
    use ratatui::text::Span;

    if in_code {
        return vec![Span::styled(
            display.to_string(),
            Style::default().fg(Color::Cyan),
        )];
    }

    let trimmed = display.trim_start();

    if let Some(indent) = bullet_indent {
        if is_new_raw {
            let rest = trimmed.get(2..).unwrap_or("").trim_start();
            let mut spans: Vec<Span> = Vec::new();
            if indent > 0 {
                spans.push(Span::raw(" ".repeat(indent)));
            }
            spans.push("• ".into());
            spans.push(Span::raw(rest.to_string()));
            return spans;
        }
        let mut continuation = String::new();
        continuation.push_str(&" ".repeat(indent + 2));
        continuation.push_str(trimmed);
        return vec![Span::raw(continuation)];
    }

    if is_new_raw
        && (trimmed.starts_with("### ") || trimmed.starts_with("## ") || trimmed.starts_with("# "))
    {
        return vec![Span::styled(
            display.to_string(),
            Style::default()
                .fg(Color::Magenta)
                .add_modifier(Modifier::BOLD),
        )];
    }

    let mut rendered = render_markdown_text(display);
    if rendered.lines.is_empty() {
        return vec![Span::raw(display.to_string())];
    }
    // `render_markdown_text` can yield multiple lines when the input contains
    // explicit breaks. We only expect a single line here; join the spans of the
    // first rendered line for styling.
    rendered.lines.remove(0).spans.into_iter().collect()
}

fn attempt_status_span(status: AttemptStatus) -> Option<ratatui::text::Span<'static>> {
    match status {
        AttemptStatus::Completed => Some("Completed".green()),
        AttemptStatus::Failed => Some("Failed".red().bold()),
        AttemptStatus::InProgress => Some("In progress".magenta()),
        AttemptStatus::Pending => Some("Pending".cyan()),
        AttemptStatus::Cancelled => Some("Cancelled".dim()),
        AttemptStatus::Unknown => None,
    }
}

fn style_diff_line(raw: &str) -> Line<'static> {
    use ratatui::style::Color;
    use ratatui::style::Modifier;
    use ratatui::style::Style;
    use ratatui::text::Span;

    if raw.starts_with("@@") {
        return Line::from(vec![Span::styled(
            raw.to_string(),
            Style::default()
                .fg(Color::Magenta)
                .add_modifier(Modifier::BOLD),
        )]);
    }
    if raw.starts_with("+++") || raw.starts_with("---") {
        return Line::from(vec![Span::styled(
            raw.to_string(),
            Style::default().add_modifier(Modifier::DIM),
        )]);
    }
    if raw.starts_with('+') {
        return Line::from(vec![Span::styled(
            raw.to_string(),
            Style::default().fg(Color::Green),
        )]);
    }
    if raw.starts_with('-') {
        return Line::from(vec![Span::styled(
            raw.to_string(),
            Style::default().fg(Color::Red),
        )]);
    }
    Line::from(vec![Span::raw(raw.to_string())])
}

fn render_task_item(_app: &App, t: &codex_cloud_tasks_client::TaskSummary) -> ListItem<'static> {
    let status = match t.status {
        TaskStatus::Ready => "READY".green(),
        TaskStatus::Pending => "PENDING".magenta(),
        TaskStatus::Applied => "APPLIED".blue(),
        TaskStatus::Error => "ERROR".red(),
    };

    // Title line: [STATUS] Title
    let title = Line::from(vec![
        "[".into(),
        status,
        "] ".into(),
        t.title.clone().into(),
    ]);

    // Meta line: environment label and relative time (dim)
    let mut meta: Vec<ratatui::text::Span> = Vec::new();
    if let Some(lbl) = t.environment_label.as_ref().filter(|s| !s.is_empty()) {
        meta.push(lbl.clone().dim());
    }
    let when = format_relative_time_now(t.updated_at).dim();
    if !meta.is_empty() {
        meta.push("  ".into());
        meta.push("•".dim());
        meta.push("  ".into());
    }
    meta.push(when);
    let meta_line = Line::from(meta);

    // Subline: summary when present; otherwise show "no diff"
    let sub = if t.summary.files_changed > 0
        || t.summary.lines_added > 0
        || t.summary.lines_removed > 0
    {
        let adds = t.summary.lines_added;
        let dels = t.summary.lines_removed;
        let files = t.summary.files_changed;
        Line::from(vec![
            format!("+{adds}").green(),
            "/".into(),
            format!("−{dels}").red(),
            " ".into(),
            "•".dim(),
            " ".into(),
            format!("{files}").into(),
            " ".into(),
            "files".dim(),
        ])
    } else {
        Line::from("no diff".to_string().dim())
    };

    // Insert a blank spacer line after the summary to separate tasks
    let spacer = Line::from("");
    ListItem::new(vec![title, meta_line, sub, spacer])
}

fn draw_inline_spinner(
    frame: &mut Frame,
    area: Rect,
    spinner_start: &mut Option<Instant>,
    label: &str,
) {
    use ratatui::widgets::Paragraph;
    let start = spinner_start.get_or_insert_with(Instant::now);
    let blink_on = (start.elapsed().as_millis() / 600).is_multiple_of(2);
    let dot = if blink_on {
        "• ".into()
    } else {
        "◦ ".dim()
    };
    let label = label.cyan();
    let line = Line::from(vec![dot, label]);
    frame.render_widget(Paragraph::new(line), area);
}

fn draw_centered_spinner(
    frame: &mut Frame,
    area: Rect,
    spinner_start: &mut Option<Instant>,
    label: &str,
) {
    // Center a 1xN spinner within the given rect
    let rows = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Percentage(50),
            Constraint::Length(1),
            Constraint::Percentage(49),
        ])
        .split(area);
    let cols = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([
            Constraint::Percentage(50),
            Constraint::Length(18),
            Constraint::Percentage(50),
        ])
        .split(rows[1]);
    draw_inline_spinner(frame, cols[1], spinner_start, label);
}

// Styling helpers for diff rendering live inline where used.

pub fn draw_env_modal(frame: &mut Frame, area: Rect, app: &mut App) {
    use ratatui::widgets::Wrap;

    // Use shared overlay geometry and padding.
    let inner = overlay_outer(area);

    // Title: primary only; move long hints to a subheader inside content.
    let title = Line::from(vec!["Select Environment".magenta().bold()]);
    let block = overlay_block().title(title);

    frame.render_widget(Clear, inner);
    frame.render_widget(block.clone(), inner);
    let content = overlay_content(inner);

    if app.env_loading {
        draw_centered_spinner(
            frame,
            content,
            &mut app.spinner_start,
            "Loading environments…",
        );
        return;
    }

    // Layout: subheader + search + results list
    let rows = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Length(1), // subheader
            Constraint::Length(1), // search
            Constraint::Min(1),    // list
        ])
        .split(content);

    // Subheader with usage hints (dim cyan)
    let subheader = Paragraph::new(Line::from(
        "Type to search, Enter select, Esc cancel".cyan().dim(),
    ))
    .wrap(Wrap { trim: true });
    frame.render_widget(subheader, rows[0]);

    let query = app
        .env_modal
        .as_ref()
        .map(|m| m.query.clone())
        .unwrap_or_default();
    let ql = query.to_lowercase();
    let search = Paragraph::new(format!("Search: {query}")).wrap(Wrap { trim: true });
    frame.render_widget(search, rows[1]);

    // Filter environments by query (case-insensitive substring over label/id/hints)
    let envs: Vec<&crate::app::EnvironmentRow> = app
        .environments
        .iter()
        .filter(|e| {
            if ql.is_empty() {
                return true;
            }
            let mut hay = String::new();
            if let Some(l) = &e.label {
                hay.push_str(&l.to_lowercase());
                hay.push(' ');
            }
            hay.push_str(&e.id.to_lowercase());
            if let Some(h) = &e.repo_hints {
                hay.push(' ');
                hay.push_str(&h.to_lowercase());
            }
            hay.contains(&ql)
        })
        .collect();

    let mut items: Vec<ListItem> = Vec::new();
    items.push(ListItem::new(Line::from("All Environments (Global)")));
    for env in envs.iter() {
        let primary = env.label.clone().unwrap_or_else(|| "<unnamed>".to_string());
        let mut spans: Vec<ratatui::text::Span> = vec![primary.into()];
        if env.is_pinned {
            spans.push("  ".into());
            spans.push("PINNED".magenta().bold());
        }
        spans.push("  ".into());
        spans.push(env.id.clone().dim());
        if let Some(hint) = &env.repo_hints {
            spans.push("  ".into());
            spans.push(hint.clone().dim());
        }
        items.push(ListItem::new(Line::from(spans)));
    }

    let sel_desired = app.env_modal.as_ref().map(|m| m.selected).unwrap_or(0);
    let sel = sel_desired.min(envs.len());
    let mut list_state = ListState::default().with_selected(Some(sel));
    let list = List::new(items)
        .highlight_symbol("› ")
        .highlight_style(Style::default().bold())
        .block(Block::default().borders(Borders::NONE));
    frame.render_stateful_widget(list, rows[2], &mut list_state);
}

pub fn draw_best_of_modal(frame: &mut Frame, area: Rect, app: &mut App) {
    use ratatui::widgets::Wrap;

    let inner = overlay_outer(area);
    const MAX_WIDTH: u16 = 40;
    const MIN_WIDTH: u16 = 20;
    const MAX_HEIGHT: u16 = 12;
    const MIN_HEIGHT: u16 = 6;
    let modal_width = inner.width.min(MAX_WIDTH).max(inner.width.min(MIN_WIDTH));
    let modal_height = inner
        .height
        .min(MAX_HEIGHT)
        .max(inner.height.min(MIN_HEIGHT));
    let modal_x = inner.x + (inner.width.saturating_sub(modal_width)) / 2;
    let modal_y = inner.y + (inner.height.saturating_sub(modal_height)) / 2;
    let modal_area = Rect::new(modal_x, modal_y, modal_width, modal_height);
    let title = Line::from(vec!["Parallel Attempts".magenta().bold()]);
    let block = overlay_block().title(title);

    frame.render_widget(Clear, modal_area);
    frame.render_widget(block.clone(), modal_area);
    let content = overlay_content(modal_area);

    let rows = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Length(2), Constraint::Min(1)])
        .split(content);

    let hint = Paragraph::new(Line::from("Use ↑/↓ to choose, 1-4 jump".cyan().dim()))
        .wrap(Wrap { trim: true });
    frame.render_widget(hint, rows[0]);

    let selected = app.best_of_modal.as_ref().map(|m| m.selected).unwrap_or(0);
    let options = [1usize, 2, 3, 4];
    let mut items: Vec<ListItem> = Vec::new();
    for &attempts in &options {
        let noun = if attempts == 1 { "attempt" } else { "attempts" };
        let mut spans: Vec<ratatui::text::Span> = vec![format!("{attempts} {noun:<8}").into()];
        spans.push("  ".into());
        spans.push(format!("{attempts}x parallel").dim());
        if attempts == app.best_of_n {
            spans.push("  ".into());
            spans.push("Current".magenta().bold());
        }
        items.push(ListItem::new(Line::from(spans)));
    }
    let sel = selected.min(options.len().saturating_sub(1));
    let mut list_state = ListState::default().with_selected(Some(sel));
    let list = List::new(items)
        .highlight_symbol("› ")
        .highlight_style(Style::default().bold())
        .block(Block::default().borders(Borders::NONE));
    frame.render_stateful_widget(list, rows[1], &mut list_state);
}
