//! Renderização das telas da TUI (T-08, T-11, T-12, T-15, T-20, T-21, T-22).
//! Funções puras de desenho a partir do `App`; sem mutação de estado de domínio.

use ratatui::layout::{Constraint, Direction, Layout, Margin, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span, Text};
use ratatui::widgets::{
    Block, Borders, Clear, List, ListItem, ListState, Paragraph, Row, Table, Wrap,
};
use ratatui::Frame;

use crate::domain::providers::{ProviderAuthStatus, ProviderCatalogEntry, ProviderUsageLimit};

use super::app::{slash_visible_indices, App, Overlay, Screen, TaskDetailView, SLASH_COMMANDS};
use super::components::{
    banner, gauge, input as input_widget, modal, status_bar, trace as trace_widget,
};
use super::execution::TaskRunStatus;
use super::orchestration::RecordedStageState;
use super::palette::COMMANDS;
use super::parallel::AgentSlotStatus;
use super::runner::{StageContextSample, TokenUsageSample};
use super::stage::Stage;
use super::theme;

pub fn render(frame: &mut Frame, app: &App) {
    let area = frame.area();
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Length(2), // header/context
            Constraint::Min(3),    // body
            Constraint::Length(2), // shortcuts/provider route
        ])
        .split(area);

    render_header(frame, chunks[0], app);
    match app.screen {
        Screen::Welcome => render_welcome(frame, chunks[1], app),
        Screen::ProviderSetup => render_provider_setup(frame, chunks[1], app),
        Screen::OrchestrationList => render_list(frame, chunks[1], app),
        Screen::NewOrchestration => render_new(frame, chunks[1], app),
        Screen::StageBoard => render_board(frame, chunks[1], app),
        Screen::Running => render_running(frame, chunks[1], app),
        Screen::TaskExecution => render_task_execution(frame, chunks[1], app, false),
        Screen::TaskDecision => render_task_execution(frame, chunks[1], app, true),
        Screen::ParallelRunning => render_parallel_running(frame, chunks[1], app),
        Screen::ParallelDecision => render_parallel_decision(frame, chunks[1], app),
        Screen::Viewer => render_viewer(frame, chunks[1], app),
        Screen::Checkpoint => render_checkpoint(frame, chunks[1], app),
        Screen::RefinementPrompt => render_refinement_prompt(frame, chunks[1], app),
        Screen::ErrorPanel => render_error(frame, chunks[1], app),
        Screen::Help => render_help(frame, chunks[1], app),
        Screen::Summary => render_summary(frame, chunks[1], app),
    }
    render_footer(frame, chunks[2], app);

    // Overlays flutuantes desenhados por cima da tela ativa (estilo OpenCode).
    match app.overlay {
        Overlay::CommandPalette => render_command_palette(frame, area, app),
        Overlay::SlashMenu => render_slash_menu(frame, area, app),
        Overlay::ProviderDialog => render_provider_dialog(frame, area, app),
        Overlay::Telemetry => render_telemetry(frame, area, app),
        Overlay::Confirm => render_confirm(frame, area, app),
        Overlay::None => {}
    }
}

fn render_welcome(frame: &mut Frame, area: Rect, app: &App) {
    let th = current_theme();
    let layout = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Min(6),    // banner
            Constraint::Length(5), // comandos
            Constraint::Length(3), // input
        ])
        .split(area);
    let version = concat!("v", env!("CARGO_PKG_VERSION"));
    banner::render(frame, layout[0], version, "", &th);

    // Comandos da home (estilo OpenCode): só os que funcionam no input vivo.
    let cmds: [(&str, &str); 3] = [
        ("Enter", "criar orquestração com o texto digitado"),
        ("Esc", "ver orquestrações existentes"),
        ("/", "menu TUI · etapas, provider, modelo, effort"),
    ];
    let mut cmd_lines = vec![Line::from("")];
    for (key, desc) in cmds {
        cmd_lines.push(Line::from(vec![
            Span::styled(
                format!("  {key:<8}"),
                Style::default().fg(th.primary).add_modifier(Modifier::BOLD),
            ),
            Span::styled(desc, Style::default().fg(th.text_muted)),
        ]));
    }
    frame.render_widget(Paragraph::new(Text::from(cmd_lines)), layout[1]);

    let props = input_widget::InputProps {
        value: &app.input,
        placeholder: "Ask anything…  descreva a feature em linguagem natural",
        mode_line: "",
        focused: true,
    };
    input_widget::render(frame, layout[2], &props, &th);
}

fn render_confirm(frame: &mut Frame, area: Rect, app: &App) {
    let th = current_theme();
    let buttons = [
        modal::ModalButton {
            label: "Cancelar",
            focused: app.confirm_focus == 0,
        },
        modal::ModalButton {
            label: "Sair",
            focused: app.confirm_focus == 1,
        },
    ];
    modal::render(
        frame,
        area,
        "Sair do SDD",
        &[
            "Deseja realmente encerrar a sessão?",
            "Tab/setas alternam · Enter confirma · Esc cancela.",
        ],
        &buttons,
        &th,
    );
}

/// Tema resolvido a partir da capacidade do terminal corrente.
fn current_theme() -> theme::Theme {
    let colorterm = std::env::var("COLORTERM").ok();
    let term = std::env::var("TERM").ok();
    theme::theme(theme::detect(colorterm.as_deref(), term.as_deref()))
}

fn render_command_palette(frame: &mut Frame, area: Rect, app: &App) {
    let th = current_theme();
    th.overlay_dim(frame, area);
    let modal = th.centered_rect(64, 70, area);
    frame.render_widget(Clear, modal);

    let inner_width = modal.width.saturating_sub(4) as usize;
    let visible = app.palette.visible();
    let selected = app.palette.selected.min(visible.len().saturating_sub(1));

    let mut lines = vec![
        Line::from(vec![
            Span::styled(
                " Comandos ",
                Style::default()
                    .fg(th.on_primary)
                    .bg(th.primary)
                    .add_modifier(Modifier::BOLD),
            ),
            Span::raw("  "),
            Span::styled("esc fecha", Style::default().fg(th.text_muted)),
        ]),
        Line::from(vec![
            Span::styled("> ", Style::default().fg(th.primary)),
            Span::styled(
                app.palette.query.clone(),
                Style::default().fg(th.text_strong),
            ),
            Span::styled("▏", Style::default().fg(th.primary)),
        ]),
        Line::from(""),
    ];

    if visible.is_empty() {
        lines.push(Line::from(vec![Span::styled(
            "  nenhum comando corresponde",
            Style::default().fg(th.text_muted),
        )]));
    }
    for (row, idx) in visible.iter().enumerate() {
        let command = &COMMANDS[*idx];
        let is_selected = row == selected;
        let label = command.label;
        let shortcut = command.shortcut;
        let pad = inner_width
            .saturating_sub(label.chars().count() + shortcut.chars().count() + 2)
            .max(1);
        let text = format!(" {label}{}{shortcut} ", " ".repeat(pad));
        let style = if is_selected {
            th.selection_style()
        } else {
            Style::default().fg(th.text_strong)
        };
        lines.push(Line::from(Span::styled(text, style)));
    }

    let block = Block::default()
        .borders(Borders::ALL)
        .border_style(Style::default().fg(th.primary))
        .title(" SDD ");
    frame.render_widget(
        Paragraph::new(Text::from(lines))
            .wrap(Wrap { trim: false })
            .block(block),
        modal,
    );
}

fn render_slash_menu(frame: &mut Frame, area: Rect, app: &App) {
    let th = current_theme();
    if area.width == 0 || area.height == 0 {
        return;
    }
    let visible = slash_visible_indices(&app.slash_query);
    let selected = app.slash_index.min(visible.len().saturating_sub(1));
    let max_rows = 9usize;
    let rows_to_show = visible.len().min(max_rows).max(1);
    let height = (rows_to_show + 5) as u16;
    let modal = slash_menu_rect(area, height);
    frame.render_widget(Clear, modal);

    let inner_width = modal.width.saturating_sub(4) as usize;
    let start = if selected >= max_rows {
        selected + 1 - max_rows
    } else {
        0
    };
    let end = (start + max_rows).min(visible.len());
    let mut lines = Vec::new();

    if visible.is_empty() {
        lines.push(Line::from(vec![Span::styled(
            "  nenhum comando corresponde",
            Style::default().fg(th.text_muted),
        )]));
    } else {
        for (row, idx) in visible[start..end].iter().enumerate() {
            let command = &SLASH_COMMANDS[*idx];
            let is_selected = start + row == selected;
            let name_width = 18usize;
            let desc_width = inner_width.saturating_sub(name_width + 3).max(8);
            let name = format!(" {:<name_width$}", command.name);
            let desc = truncate(command.description, desc_width);
            let style = if is_selected {
                th.selection_style()
            } else {
                Style::default().fg(th.text_strong)
            };
            lines.push(Line::from(vec![
                Span::styled(name, style),
                Span::styled(
                    desc,
                    if is_selected {
                        style
                    } else {
                        Style::default().fg(th.text_muted)
                    },
                ),
            ]));
        }
    }

    lines.push(Line::from(""));
    lines.push(Line::from(vec![
        Span::styled(" /", Style::default().fg(th.text_strong)),
        Span::styled(
            app.slash_query.clone(),
            Style::default()
                .fg(th.text_strong)
                .add_modifier(Modifier::BOLD),
        ),
        Span::styled("▏", Style::default().fg(th.primary)),
    ]));
    lines.push(Line::from(vec![
        Span::styled(
            slash_scope_label(app),
            Style::default().fg(th.primary).add_modifier(Modifier::BOLD),
        ),
        Span::styled(" · ", Style::default().fg(th.text_muted)),
        Span::styled(
            slash_provider_route(app),
            Style::default()
                .fg(th.text_strong)
                .add_modifier(Modifier::BOLD),
        ),
        Span::styled(" · Tab provider", Style::default().fg(th.text_muted)),
        Span::styled(" · ←/→ model", Style::default().fg(th.text_muted)),
        Span::styled(" · [/] effort", Style::default().fg(th.text_muted)),
    ]));

    let block = Block::default()
        .borders(Borders::LEFT | Borders::RIGHT)
        .border_style(Style::default().fg(th.primary))
        .style(Style::default().bg(th.overlay_dim));
    frame.render_widget(
        Paragraph::new(Text::from(lines))
            .wrap(Wrap { trim: false })
            .block(block),
        modal,
    );
}

fn slash_menu_rect(area: Rect, requested_height: u16) -> Rect {
    let height = requested_height.min(area.height);
    let y = area
        .y
        .saturating_add(area.height.saturating_sub(height.saturating_add(4)));
    Rect::new(area.x, y, area.width, height)
}

fn render_telemetry(frame: &mut Frame, area: Rect, app: &App) {
    let th = current_theme();
    th.overlay_dim(frame, area);
    let modal = th.centered_rect(72, 70, area);
    frame.render_widget(Clear, modal);

    let mut lines = vec![
        Line::from(vec![
            Span::styled(
                " Telemetria ",
                Style::default()
                    .fg(th.on_primary)
                    .bg(th.primary)
                    .add_modifier(Modifier::BOLD),
            ),
            Span::raw("  "),
            Span::styled("t/esc fecha", Style::default().fg(th.text_muted)),
        ]),
        Line::from(""),
        th.label_value("Uso da sessão", &session_usage_label(app)),
        th.label_value("Última geração", &last_usage_label(app)),
    ];
    let usage = &app.session_usage;
    let max = (usage.total_tokens_estimate.max(1)) as u64;
    let rows = vec![
        ("Entrada".to_string(), usage.input_tokens_estimate as u64),
        ("Saída".to_string(), usage.output_tokens_estimate as u64),
        ("Total".to_string(), usage.total_tokens_estimate as u64),
    ];
    let bar_w = modal.width.saturating_sub(28).clamp(8, 40);
    lines.push(Line::from(""));
    lines.extend(gauge::dashboard("Uso de tokens", &rows, max, bar_w, &th));
    if let Some(provider) = app.provider_selection.as_ref() {
        let budget = provider
            .token_budget
            .map(|value| format!("{value} tokens"))
            .unwrap_or_else(|| "padrão".to_string());
        lines.push(th.label_value("Budget", &budget));
        if let Some(token_budget) = provider.token_budget {
            lines.push(th.label_value(
                "Saldo local",
                &token_budget_remaining_label(
                    token_budget,
                    app.session_usage.total_tokens_estimate,
                ),
            ));
        }
        lines.push(th.label_value(
            "Limites",
            &provider_usage_limits_label(&provider.id, &provider.usage_limits),
        ));
    }
    lines.push(Line::from(""));
    lines.push(th.section_header("Roteamento por etapa"));
    if let Some(provider) = app.provider_selection.as_ref() {
        for stage in Stage::ALL {
            let model = provider
                .stage_models
                .get(stage.key())
                .cloned()
                .unwrap_or_else(|| provider.model.clone());
            lines.push(th.label_value(stage.label(), &truncate(&model, 40)));
        }
    } else {
        lines.push(Line::from(Span::styled(
            "  provider não selecionado",
            Style::default().fg(th.text_muted),
        )));
    }

    let block = Block::default()
        .borders(Borders::ALL)
        .border_style(Style::default().fg(th.primary))
        .title(" SDD ");
    frame.render_widget(
        Paragraph::new(Text::from(lines))
            .wrap(Wrap { trim: false })
            .block(block),
        modal,
    );
}

fn render_provider_dialog(frame: &mut Frame, area: Rect, app: &App) {
    let th = current_theme();
    th.overlay_dim(frame, area);
    let width = area.width.saturating_sub(8).clamp(62, 118);
    let height = area.height.saturating_sub(4).clamp(18, 34);
    let x = area.x + area.width.saturating_sub(width) / 2;
    let y = area.y + area.height.saturating_sub(height) / 2;
    let modal = Rect::new(x, y, width.min(area.width), height.min(area.height));
    frame.render_widget(Clear, modal);
    frame.render_widget(
        Block::default()
            .borders(Borders::ALL)
            .border_style(Style::default().fg(th.primary))
            .title(" Provider "),
        modal,
    );

    let inner = modal.inner(Margin {
        vertical: 1,
        horizontal: 2,
    });
    if app.providers.is_empty() {
        frame.render_widget(
            Paragraph::new(Text::from(vec![
                Line::from("Nenhum provider carregado."),
                Line::from("Rode `sdd providers doctor` ou edite `sdd.config.yaml`."),
            ]))
            .wrap(Wrap { trim: false }),
            inner,
        );
        return;
    }

    let layout = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([Constraint::Length(30), Constraint::Min(36)])
        .split(inner);
    render_provider_list(frame, layout[0], app);
    let provider = &app.providers[app.provider_index.min(app.providers.len() - 1)];
    let right = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Min(10), Constraint::Length(6)])
        .split(layout[1]);
    render_provider_picker(frame, right[0], app, provider);

    let hints = Text::from(vec![
        Line::from(vec![
            Span::styled(
                " Enter aplicar ",
                Style::default().fg(th.on_primary).bg(th.primary),
            ),
            Span::raw("  "),
            Span::styled("Esc fechar", Style::default().fg(th.text_strong)),
            Span::raw("  "),
            Span::styled("↑/↓ provider", Style::default().fg(th.text_muted)),
            Span::raw("  "),
            Span::styled("←/→ modelo", Style::default().fg(th.text_muted)),
        ]),
        Line::from(vec![
            Span::styled("[/] effort", Style::default().fg(th.text_muted)),
            Span::raw("  "),
            Span::styled("r recarregar", Style::default().fg(th.text_muted)),
            Span::raw("  "),
            Span::styled("e editar config", Style::default().fg(th.text_muted)),
        ]),
    ]);
    frame.render_widget(Paragraph::new(hints).wrap(Wrap { trim: false }), right[1]);
}

fn render_header(frame: &mut Frame, area: Rect, app: &App) {
    let th = current_theme();
    let title = screen_title(app.screen);
    let location = app
        .active
        .as_ref()
        .map(|active| format!("{} · {}", active.name, active.slug))
        .unwrap_or_else(|| app.root.display().to_string());
    let lines = Text::from(vec![
        Line::from(vec![
            Span::styled(
                " SDD ",
                Style::default()
                    .fg(th.on_primary)
                    .bg(th.primary)
                    .add_modifier(Modifier::BOLD),
            ),
            Span::styled(
                " cockpit ",
                Style::default().fg(th.primary).add_modifier(Modifier::BOLD),
            ),
            Span::styled(format!("· {title}"), Style::default().fg(th.text_strong)),
        ]),
        Line::from(vec![
            Span::raw(" "),
            Span::styled(truncate(&location, 96), Style::default().fg(th.text_muted)),
        ]),
    ]);
    frame.render_widget(Paragraph::new(lines), area);
}

const TIPS: &[&str] = &[
    "/ abre o menu TUI contextual; Ctrl+K mantém o command palette.",
    "Tab troca provider; ←/→ troca modelo; [/] troca effort.",
    "t alterna a telemetria; o dashboard mostra uso de tokens.",
    "Cada etapa tem um checkpoint humano antes de avançar.",
    "Refinement é opcional — pule em features triviais.",
    "Esc volta; q encerra a sessão com segurança.",
];

fn render_footer(frame: &mut Frame, area: Rect, app: &App) {
    let th = current_theme();
    // Mensagem transitória tem prioridade visual (estilo alerta).
    if let Some(msg) = &app.footer {
        let hint = Line::from(Span::styled(
            format!(" {msg}"),
            Style::default()
                .fg(th.on_primary)
                .bg(th.warning)
                .add_modifier(Modifier::BOLD),
        ));
        let route = Line::from(vec![
            Span::styled(" rota ", Style::default().fg(th.on_primary).bg(th.primary)),
            Span::raw(" "),
            Span::styled(
                active_provider_summary(app),
                Style::default().fg(th.text_strong),
            ),
        ]);
        frame.render_widget(Paragraph::new(Text::from(vec![hint, route])), area);
        return;
    }

    let route = active_provider_summary(app);
    let shortcuts = shortcuts_hint(app.screen);
    let tokens_abs = format_usize(app.session_usage.total_tokens_estimate);
    let tokens_pct = app
        .provider_selection
        .as_ref()
        .and_then(|p| p.token_budget)
        .filter(|budget| *budget > 0)
        .map(|budget| {
            let used = app.session_usage.total_tokens_estimate.min(budget as usize);
            ((used as f64 / budget as f64) * 100.0).round() as u8
        });
    let tip = TIPS[app.session_usage.generation_count % TIPS.len()];
    let props = status_bar::StatusBarProps {
        route: &route,
        tokens_pct,
        tokens_abs: Some(&tokens_abs),
        cost: None,
        shortcuts: &shortcuts,
        tip: Some(tip),
    };
    status_bar::render(frame, area, &props, &th);
}

fn shortcuts_hint(screen: Screen) -> String {
    let base = match screen {
        Screen::ProviderSetup => {
            "↑/↓ provider · ←/→ modelo · [/] effort · Enter escolher · e config · q sair"
        }
        Screen::Welcome => {
            "digite a ideia · / menu · Tab provider dialog · Enter criar · Esc orquestrações"
        }
        Screen::OrchestrationList => {
            "↑/↓ navegar · Enter abrir · n nova · / menu · Tab provider dialog · q sair"
        }
        Screen::NewOrchestration => {
            "digite a ideia · / menu · Tab provider dialog · Enter confirmar · Esc cancelar"
        }
        Screen::StageBoard => {
            "↑/↓ etapa · Enter abrir/executar · / menu · Tab provider dialog · q sair"
        }
        Screen::Running => "executando… · q sair",
        Screen::TaskExecution => {
            "↑/↓ task · Tab log/diff/report · r retentar falha · Esc voltar se livre · q sair"
        }
        Screen::TaskDecision => {
            "↑/↓ task · Tab log/diff/report · r retentar · a aceitar parcial · Esc cancelar"
        }
        Screen::ParallelRunning => "Agent Teams ativos · q sair · ? ajuda",
        Screen::ParallelDecision => "r retentar falhos · a aceitar parcial · Esc cancelar",
        Screen::Viewer => {
            "↑/↓ rolar · a checkpoint · r regenerar · Tab provider dialog · Esc voltar"
        }
        Screen::Checkpoint => {
            "a aprovar · r regenerar · Tab provider dialog · e editar · Esc voltar"
        }
        Screen::RefinementPrompt => "r executar · s pular · Tab provider dialog · Esc voltar",
        Screen::ErrorPanel => "r tentar de novo · e editar config · Esc voltar",
        Screen::Help => "Esc fechar ajuda",
        Screen::Summary => "q encerrar",
    };
    format!(" {base}  ·  / menu · Ctrl+K comandos · t telemetria · ? ajuda")
}

fn render_provider_setup(frame: &mut Frame, area: Rect, app: &App) {
    let area = if area.height >= 18 {
        let chunks = Layout::default()
            .direction(Direction::Vertical)
            .constraints([Constraint::Length(4), Constraint::Min(3)])
            .split(area);
        render_sdd_signature(frame, chunks[0], app);
        chunks[1]
    } else {
        area
    };

    if app.providers.is_empty() {
        let th = current_theme();
        let mut lines = vec![
            Line::from(""),
            Line::from(Span::styled(
                "  Nenhum provider carregado.",
                Style::default().fg(th.text_strong),
            )),
            Line::from(""),
        ];
        if let Some(error) = &app.provider_error {
            lines.push(Line::from(vec![
                Span::styled("  Erro: ", Style::default().fg(th.error)),
                Span::styled(error.clone(), Style::default().fg(th.text_strong)),
            ]));
            lines.push(Line::from(""));
        }
        lines.push(Line::from(vec![
            Span::styled(
                " e editar config ",
                Style::default()
                    .fg(th.on_primary)
                    .bg(th.primary)
                    .add_modifier(Modifier::BOLD),
            ),
            Span::raw("  "),
            Span::styled(" r recarregar ", Style::default().fg(th.text_strong)),
        ]));
        frame.render_widget(
            Paragraph::new(Text::from(lines))
                .wrap(Wrap { trim: false })
                .block(Block::default().title(" Provider ")),
            area,
        );
        return;
    }

    let chunks = if area.width < 96 {
        Layout::default()
            .direction(Direction::Vertical)
            .constraints([Constraint::Length(8), Constraint::Min(8)])
            .split(area)
    } else {
        Layout::default()
            .direction(Direction::Horizontal)
            .constraints([Constraint::Percentage(36), Constraint::Min(44)])
            .split(area)
    };

    render_provider_list(frame, chunks[0], app);

    let provider = &app.providers[app.provider_index.min(app.providers.len() - 1)];
    let detail_chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Length(13), Constraint::Min(8)])
        .split(chunks[1]);
    render_provider_picker(frame, detail_chunks[0], app, provider);
    render_provider_routes(frame, detail_chunks[1], app, provider);
}

fn render_sdd_signature(frame: &mut Frame, area: Rect, app: &App) {
    let th = current_theme();
    let route = active_provider_summary(app);
    let rule = "─".repeat(area.width.saturating_sub(2) as usize);
    let lines = Text::from(vec![
        Line::from(vec![
            Span::raw(" "),
            Span::styled(
                " SDD ",
                Style::default()
                    .fg(th.on_primary)
                    .bg(th.primary)
                    .add_modifier(Modifier::BOLD),
            ),
            Span::styled(
                " Spec-Driven Development ",
                Style::default()
                    .fg(th.text_strong)
                    .add_modifier(Modifier::BOLD),
            ),
            Span::styled("TUI", Style::default().fg(th.text_muted)),
        ]),
        Line::from(vec![
            Span::raw(" "),
            Span::styled("provider", Style::default().fg(th.primary)),
            Span::styled(" · ", Style::default().fg(th.text_muted)),
            Span::styled("model", Style::default().fg(th.primary)),
            Span::styled(" · ", Style::default().fg(th.text_muted)),
            Span::styled("effort", Style::default().fg(th.primary)),
            Span::styled(
                " · checkpoints · artifacts · memory",
                Style::default().fg(th.text_muted),
            ),
        ]),
        Line::from(vec![
            Span::raw(" "),
            Span::styled(truncate(&route, 88), Style::default().fg(th.text_strong)),
        ]),
        Line::from(Span::styled(
            format!(" {rule}"),
            Style::default().fg(th.text_muted),
        )),
    ]);
    frame.render_widget(Paragraph::new(lines), area);
}

fn render_provider_list(frame: &mut Frame, area: Rect, app: &App) {
    let th = current_theme();
    let items: Vec<ListItem> = app
        .providers
        .iter()
        .map(|provider| {
            let auth_mark = match provider.auth_present {
                Some(true) => "●",
                Some(false) => "!",
                None => "·",
            };
            let enabled = if provider.enabled {
                "enabled"
            } else {
                "disabled"
            };
            let strong = if provider.enabled {
                th.text_strong
            } else {
                th.text_muted
            };
            let model = provider.model.as_deref().unwrap_or("default");
            let lines = Text::from(vec![
                Line::from(vec![
                    Span::styled(
                        format!(" {auth_mark} "),
                        Style::default().fg(auth_color(provider.auth_present, &th)),
                    ),
                    Span::styled(
                        format!("{:<12}", truncate(&provider.id, 12)),
                        Style::default().fg(strong).add_modifier(Modifier::BOLD),
                    ),
                    Span::styled(
                        format!("  {enabled}"),
                        Style::default().fg(if provider.enabled {
                            th.success
                        } else {
                            th.text_muted
                        }),
                    ),
                ]),
                Line::from(vec![
                    Span::raw("   "),
                    Span::styled(
                        truncate(&provider.kind, 18),
                        Style::default().fg(th.primary),
                    ),
                    Span::styled(" · ", Style::default().fg(th.text_muted)),
                    Span::styled(truncate(model, 24), Style::default().fg(th.text_muted)),
                ]),
            ]);
            ListItem::new(lines)
        })
        .collect();
    let list = List::new(items)
        .block(Block::default().title(" Providers "))
        .highlight_style(th.selection_style())
        .highlight_symbol("  ");
    let mut state = ListState::default();
    state.select(Some(app.provider_index.min(app.providers.len() - 1)));
    frame.render_stateful_widget(list, area, &mut state);
}

fn render_provider_picker(
    frame: &mut Frame,
    area: Rect,
    app: &App,
    provider: &ProviderCatalogEntry,
) {
    let th = current_theme();
    let model = selected_model_label(provider, app.provider_model_index);
    let effort = selected_effort_label(provider, app.provider_effort_index);
    let selected = app
        .provider_selection
        .as_ref()
        .map(|selection| selection.id.as_str())
        .unwrap_or("auto");
    let auth_env = provider.auth_env.as_deref().unwrap_or("não requerido");
    let auth_methods = auth_methods_label(&provider.auth_methods);
    let auth_login = auth_login_hint(&provider.auth_methods);
    let capabilities = if provider.capabilities.is_empty() {
        "nenhuma declarada".to_string()
    } else {
        provider.capabilities.join(", ")
    };
    let token_budget = provider
        .token_budget
        .map(|value| format!("{value} tokens"))
        .unwrap_or_else(|| "padrão".to_string());
    let usage_limits = provider_usage_limits_label(&provider.id, &provider.usage_limits);
    let model_count = provider.models.len().max(1);
    let mut lines = vec![
        Line::from(vec![
            Span::styled(
                " Provider ",
                Style::default().fg(th.on_primary).bg(th.primary),
            ),
            Span::raw(" "),
            Span::styled(
                provider.id.clone(),
                Style::default()
                    .fg(th.text_strong)
                    .add_modifier(Modifier::BOLD),
            ),
            Span::styled(
                format!(" · {}", provider.kind),
                Style::default().fg(th.text_muted),
            ),
        ]),
        Line::from(vec![
            Span::styled("  Modelo ", Style::default().fg(th.primary)),
            Span::styled("◀ ", Style::default().fg(th.text_muted)),
            Span::styled(
                truncate(&model, 54),
                Style::default()
                    .fg(th.on_primary)
                    .bg(th.primary)
                    .add_modifier(Modifier::BOLD),
            ),
            Span::styled(
                format!(" ▶  {}/{}", app.provider_model_index + 1, model_count),
                Style::default().fg(th.text_muted),
            ),
        ]),
        effort_segments(provider, app.provider_effort_index, &effort),
        Line::from(vec![
            Span::styled("  Auth ", Style::default().fg(th.primary)),
            Span::styled(
                auth_label(provider.auth_present),
                Style::default()
                    .fg(th.on_primary)
                    .bg(auth_color(provider.auth_present, &th)),
            ),
            Span::styled(format!(" · {auth_env}"), Style::default().fg(th.text_muted)),
        ]),
        Line::from(vec![
            Span::styled("  Métodos ", Style::default().fg(th.primary)),
            Span::styled(
                truncate(&auth_methods, 76),
                Style::default().fg(th.text_strong),
            ),
        ]),
        Line::from(vec![
            Span::styled("  Login ", Style::default().fg(th.primary)),
            Span::styled(
                truncate(&auth_login, 76),
                Style::default().fg(th.text_strong),
            ),
        ]),
        Line::from(vec![
            Span::styled("  Budget tokens ", Style::default().fg(th.primary)),
            Span::styled(token_budget, Style::default().fg(th.text_strong)),
            Span::styled(" · seleção ativa ", Style::default().fg(th.text_muted)),
            Span::styled(selected.to_string(), Style::default().fg(th.text_strong)),
        ]),
        Line::from(vec![
            Span::styled("  Limites ", Style::default().fg(th.primary)),
            Span::styled(
                truncate(&usage_limits, 84),
                Style::default().fg(th.text_strong),
            ),
        ]),
        Line::from(vec![
            Span::styled("  Capacidades ", Style::default().fg(th.primary)),
            Span::styled(
                truncate(&capabilities, 76),
                Style::default().fg(th.text_strong),
            ),
        ]),
        Line::from(vec![
            Span::styled(
                " Enter aplicar ",
                Style::default()
                    .fg(th.on_primary)
                    .bg(th.primary)
                    .add_modifier(Modifier::BOLD),
            ),
            Span::raw("  "),
            Span::styled(" e config ", Style::default().fg(th.text_strong)),
            Span::raw("  "),
            Span::styled(" r recarregar ", Style::default().fg(th.text_muted)),
        ]),
    ];
    if let Some(error) = &app.provider_error {
        lines.push(Line::from(vec![
            Span::styled("  Erro ", Style::default().fg(th.error)),
            Span::styled(truncate(error, 84), Style::default().fg(th.text_strong)),
        ]));
    }
    frame.render_widget(
        Paragraph::new(Text::from(lines))
            .wrap(Wrap { trim: false })
            .block(
                Block::default()
                    .borders(Borders::ALL)
                    .border_style(Style::default().fg(th.primary))
                    .title(" Seletor "),
            ),
        area,
    );
}

fn render_provider_routes(
    frame: &mut Frame,
    area: Rect,
    app: &App,
    provider: &ProviderCatalogEntry,
) {
    let th = current_theme();
    let fallback_model = selected_model_id(provider, app.provider_model_index);
    let fallback_effort = selected_effort_label(provider, app.provider_effort_index);
    let header = Row::new(vec!["Etapa", "Modelo efetivo", "Effort"])
        .style(Style::default().fg(th.primary).add_modifier(Modifier::BOLD));
    let rows: Vec<Row> = Stage::ALL
        .iter()
        .map(|stage| {
            let model = provider
                .stage_models
                .get(stage.key())
                .cloned()
                .unwrap_or_else(|| fallback_model.clone());
            let effort = provider
                .stage_efforts
                .get(stage.key())
                .cloned()
                .unwrap_or_else(|| fallback_effort.clone());
            let is_override = provider.stage_models.contains_key(stage.key())
                || provider.stage_efforts.contains_key(stage.key());
            Row::new(vec![
                stage.label().to_string(),
                truncate(&model, 34),
                effort,
            ])
            .style(Style::default().fg(if is_override {
                th.text_strong
            } else {
                th.text_muted
            }))
        })
        .collect();
    let table = Table::new(
        rows,
        [
            Constraint::Length(13),
            Constraint::Min(18),
            Constraint::Length(9),
        ],
    )
    .header(header)
    .block(Block::default().title(" Roteamento por etapa "));
    frame.render_widget(table, area);
}

fn render_list(frame: &mut Frame, area: Rect, app: &App) {
    let th = current_theme();
    if app.orchestrations.is_empty() {
        let chunks = Layout::default()
            .direction(Direction::Vertical)
            .constraints([Constraint::Min(7), Constraint::Length(3)])
            .split(area);
        let version = concat!("v", env!("CARGO_PKG_VERSION"));
        banner::render(frame, chunks[0], version, "", &th);
        let hints = Text::from(vec![
            Line::from(Span::styled(
                "  Nenhuma orquestração encontrada em docs/.",
                Style::default().fg(th.text_muted),
            )),
            Line::from(vec![
                Span::styled("  n", Style::default().fg(th.primary)),
                Span::styled(" cria uma nova    ", Style::default().fg(th.text_muted)),
                Span::styled("Ctrl+K", Style::default().fg(th.primary)),
                Span::styled(" comandos", Style::default().fg(th.text_muted)),
            ]),
        ]);
        frame.render_widget(Paragraph::new(hints).wrap(Wrap { trim: false }), chunks[1]);
        return;
    }
    let items: Vec<ListItem> = app
        .orchestrations
        .iter()
        .map(|o| {
            let stage = o.current_stage().map(|s| s.label()).unwrap_or("concluída");
            let line = Line::from(vec![
                Span::styled(
                    format!("{:<28}", truncate(&o.name, 28)),
                    Style::default().fg(th.text_strong),
                ),
                Span::styled(format!(" [{}]", o.state), Style::default().fg(th.primary)),
                Span::styled(format!("  → {stage}"), Style::default().fg(th.text_muted)),
            ]);
            ListItem::new(line)
        })
        .collect();
    let list = List::new(items)
        .highlight_style(th.selection_style())
        .highlight_symbol("  ");
    let mut state = ListState::default();
    state.select(Some(
        app.list_index
            .min(app.orchestrations.len().saturating_sub(1)),
    ));
    frame.render_stateful_widget(list, area, &mut state);
}

fn render_new(frame: &mut Frame, area: Rect, app: &App) {
    let th = current_theme();
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Length(2), Constraint::Min(4)])
        .split(area);
    let target_label = app
        .new_target_stage
        .map(|stage| format!("Gere {} a partir do contexto abaixo:", stage.label()))
        .unwrap_or_else(|| "Descreva a feature em linguagem natural:".to_string());
    frame.render_widget(
        Paragraph::new(Line::from(Span::styled(
            format!("  {target_label}"),
            Style::default().fg(th.text_muted),
        ))),
        chunks[0],
    );
    let props = input_widget::InputProps {
        value: &app.input,
        placeholder: app
            .new_target_stage
            .map(|stage| match stage {
                Stage::Idea => "Contexto para a Idea…",
                Stage::Prd => "Contexto para gerar o PRD…",
                Stage::Techspec => "Contexto para gerar a Tech Spec…",
                Stage::Tasks => "Contexto para quebrar em tasks…",
                Stage::Refinement => "Contexto para refinement técnico…",
                Stage::Execution => "Contexto para execution…",
                Stage::Adr => "Contexto para ADR pós-execução…",
                Stage::Review => "Contexto para review…",
                Stage::Memory => "Contexto para memory…",
            })
            .unwrap_or("Ask anything… \"reformular o layout do TUI\""),
        mode_line: "",
        focused: true,
    };
    input_widget::render(frame, chunks[1], &props, &th);
}

fn render_board(frame: &mut Frame, area: Rect, app: &App) {
    let Some(active) = &app.active else {
        return;
    };
    let chunks = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([Constraint::Percentage(38), Constraint::Min(40)])
        .split(area);
    let th = current_theme();
    let mut items: Vec<ListItem> = Vec::new();
    items.push(ListItem::new(Line::from(vec![Span::styled(
        " Pré-etapas recorded ",
        Style::default().fg(th.primary).add_modifier(Modifier::BOLD),
    )])));
    for stage in &active.pre_stages {
        items.push(render_recorded_lane_item(stage, &th));
    }
    items.push(ListItem::new(Line::from("")));
    items.push(ListItem::new(Line::from(vec![Span::styled(
        " Guia de execução ",
        Style::default().fg(th.primary).add_modifier(Modifier::BOLD),
    )])));
    let guided_selection_offset = items.len();
    items.extend(active.stages.iter().map(|st| {
        let color = match st.status {
            super::stage::StageStatus::Approved => th.success,
            super::stage::StageStatus::Skipped => th.text_muted,
            super::stage::StageStatus::Recorded => th.primary,
            super::stage::StageStatus::InProgress => th.warning,
            super::stage::StageStatus::Optional => th.primary,
            _ => th.text_strong,
        };
        let approved = st
            .approved_at
            .as_deref()
            .map(|t| format!("  ({t})"))
            .unwrap_or_default();
        ListItem::new(Line::from(vec![
            Span::styled(
                format!(" {} ", st.status.icon()),
                Style::default().fg(color),
            ),
            Span::styled(
                format!("{:<12}", st.stage.label()),
                Style::default().fg(color),
            ),
            Span::styled(approved, Style::default().fg(th.text_muted)),
        ]))
    }));
    items.push(ListItem::new(Line::from("")));
    items.push(ListItem::new(Line::from(vec![Span::styled(
        " Pós-execução recorded ",
        Style::default().fg(th.primary).add_modifier(Modifier::BOLD),
    )])));
    for stage in &active.post_stages {
        items.push(render_recorded_lane_item(stage, &th));
    }
    let list = List::new(items)
        .block(Block::default().title(format!(" {} ", truncate(&active.name, 40))))
        .highlight_style(th.selection_style())
        .highlight_symbol("  ");
    let mut state = ListState::default();
    state.select(Some(
        guided_selection_offset + app.board_index.min(active.stages.len().saturating_sub(1)),
    ));
    frame.render_stateful_widget(list, chunks[0], &mut state);

    let selected = active
        .stages
        .get(app.board_index.min(active.stages.len().saturating_sub(1)));
    let stage = selected.map(|st| st.stage).unwrap_or(Stage::Idea);
    let file = active.path.join(stage.filename());
    let status = selected
        .map(|st| format!("{:?}", st.status))
        .unwrap_or_else(|| "Unknown".to_string());
    let artifact_exists = file.exists();
    let provider = app
        .provider_selection
        .as_ref()
        .map(|p| {
            let model = p.stage_models.get(stage.key()).unwrap_or(&p.model);
            let effort = p.stage_efforts.get(stage.key()).unwrap_or(&p.effort);
            format!("{} / {} / {}", p.id, model, effort)
        })
        .unwrap_or_else(|| "auto / default".to_string());
    let action = if artifact_exists {
        "Enter abre o artefato; r no viewer regenera via provider e salva com --force."
    } else {
        "Enter gera o artefato inicial via provider quando houver adapter disponível."
    };
    let checkpoint = if stage == Stage::Refinement {
        "Executar, aprovar ou pular com rastreabilidade explícita."
    } else {
        "Depois do artefato, aprovar, editar ou regenerar antes de avançar."
    };
    let lines = Text::from(vec![
        Line::from(""),
        th.label_value("Etapa", stage.label()),
        th.label_value("Estado", &status),
        th.label_value("Artefato", stage.filename()),
        th.label_value("Existe", if artifact_exists { "sim" } else { "não" }),
        Line::from(""),
        th.label_value("Provider", &provider),
        th.label_value(
            "Pré-lanes",
            &recorded_lane_summary(&active.pre_stages, "Project Discovery e Risk"),
        ),
        th.label_value(
            "Pós-lane",
            &recorded_lane_summary(&active.post_stages, "ADR pós-execução"),
        ),
        Line::from(""),
        Line::from(Span::styled(
            format!("  {action}"),
            Style::default().fg(th.text_strong),
        )),
        Line::from(Span::styled(
            format!("  {checkpoint}"),
            Style::default().fg(th.text_muted),
        )),
        Line::from(Span::styled(
            "  Discovery, Risk e ADR aparecem no board como lanes recorded para consulta visual.",
            Style::default().fg(th.text_muted),
        )),
        Line::from(""),
        Line::from(vec![
            Span::styled("  t", Style::default().fg(th.primary)),
            Span::styled(" telemetria · ", Style::default().fg(th.text_muted)),
            Span::styled("Ctrl+K", Style::default().fg(th.primary)),
            Span::styled(" comandos", Style::default().fg(th.text_muted)),
        ]),
        Line::from(""),
        th.label_value("Caminho", &file.display().to_string()),
    ]);
    frame.render_widget(
        Paragraph::new(lines)
            .wrap(Wrap { trim: false })
            .block(Block::default().title(" Detalhes ")),
        chunks[1],
    );
}

fn render_recorded_lane_item(stage: &RecordedStageState, th: &theme::Theme) -> ListItem<'static> {
    let color = match stage.status {
        super::stage::StageStatus::Approved => th.success,
        super::stage::StageStatus::Recorded => th.primary,
        super::stage::StageStatus::Skipped => th.text_muted,
        super::stage::StageStatus::InProgress => th.warning,
        super::stage::StageStatus::Optional => th.primary,
        _ => th.text_muted,
    };
    let approved = stage
        .approved_at
        .as_deref()
        .map(|t| format!("  ({t})"))
        .unwrap_or_default();
    ListItem::new(Line::from(vec![
        Span::styled(
            format!(" {} ", stage.status.icon()),
            Style::default().fg(color),
        ),
        Span::styled(format!("{:<18}", stage.label), Style::default().fg(color)),
        Span::styled(
            format!(" {}", stage.file),
            Style::default().fg(th.text_muted),
        ),
        Span::styled(approved, Style::default().fg(th.text_muted)),
    ]))
}

fn recorded_lane_summary(stages: &[RecordedStageState], fallback: &str) -> String {
    if stages.is_empty() {
        return fallback.to_string();
    }
    stages
        .iter()
        .map(|stage| format!("{} {} [{}]", stage.status.icon(), stage.label, stage.key))
        .collect::<Vec<_>>()
        .join(" · ")
}

fn render_running(frame: &mut Frame, area: Rect, app: &App) {
    if area.width >= 112 {
        let chunks = Layout::default()
            .direction(Direction::Horizontal)
            .constraints([Constraint::Length(52), Constraint::Min(48)])
            .split(area);
        render_running_overview(frame, chunks[0], app);
        render_running_timeline(frame, chunks[1], app);
    } else {
        let chunks = Layout::default()
            .direction(Direction::Vertical)
            .constraints([Constraint::Length(16), Constraint::Min(8)])
            .split(area);
        render_running_overview(frame, chunks[0], app);
        render_running_timeline(frame, chunks[1], app);
    }
}

fn render_task_execution(frame: &mut Frame, area: Rect, app: &App, decision: bool) {
    if area.width >= 118 {
        let chunks = Layout::default()
            .direction(Direction::Horizontal)
            .constraints([
                Constraint::Length(42),
                Constraint::Length(44),
                Constraint::Min(42),
            ])
            .split(area);
        render_task_progress(frame, chunks[0], app, decision);
        render_task_list(frame, chunks[1], app);
        render_task_detail(frame, chunks[2], app);
    } else {
        let chunks = Layout::default()
            .direction(Direction::Vertical)
            .constraints([
                Constraint::Length(if decision { 13 } else { 11 }),
                Constraint::Length(9),
                Constraint::Min(8),
            ])
            .split(area);
        render_task_progress(frame, chunks[0], app, decision);
        render_task_list(frame, chunks[1], app);
        render_task_detail(frame, chunks[2], app);
    }
}

fn render_task_progress(frame: &mut Frame, area: Rect, app: &App, decision: bool) {
    let total = app.execution_total_count();
    let done = app.execution_done_count();
    let errors = app.execution_error_count();
    let pending = app.execution_pending_count();
    let running = app
        .execution
        .as_ref()
        .and_then(|session| session.current_index)
        .and_then(|index| {
            app.execution
                .as_ref()
                .and_then(|session| session.tasks.get(index))
        });
    let current = running
        .map(|task| format!("{} — {}", task.task.id, task.task.title))
        .or_else(|| {
            app.selected_execution_task()
                .map(|task| format!("{} — {}", task.task.id, task.task.title))
        })
        .unwrap_or_else(|| "nenhuma task selecionada".to_string());
    let progress = progress_bar(done, total, area.width.saturating_sub(8) as usize);
    let status = if decision {
        "decisão pendente"
    } else if running.is_some() {
        "task em execução"
    } else {
        "preparando próxima task"
    };

    let th = current_theme();
    let mut lines = vec![
        Line::from(""),
        Line::from(vec![
            Span::styled(
                format!("  {} ", app.loader.current_frame()),
                Style::default().fg(th.primary),
            ),
            Span::styled(
                if decision {
                    "execução pausada por falha"
                } else {
                    "executando tasks reais"
                },
                Style::default()
                    .fg(if decision { th.warning } else { th.text_strong })
                    .add_modifier(Modifier::BOLD),
            ),
        ]),
        Line::from(""),
        metric_line("Progresso", &format!("{done}/{total} {progress}")),
        metric_line("Status", status),
        metric_line(
            "Atual",
            &truncate(&current, area.width.saturating_sub(14) as usize),
        ),
        metric_line("Tempo", &app.loader.elapsed_label()),
        metric_line("Pendentes", &pending.to_string()),
        metric_line("Erros", &errors.to_string()),
        metric_line("Uso", &execution_usage_label(app)),
    ];
    if decision {
        lines.push(Line::from(""));
        lines.push(Line::from(vec![
            Span::styled(
                " r Retentar com erro ",
                Style::default()
                    .fg(th.on_primary)
                    .bg(th.primary)
                    .add_modifier(Modifier::BOLD),
            ),
            Span::raw("  "),
            Span::styled(" a Aceitar parcial ", Style::default().fg(th.text_strong)),
        ]));
    }

    frame.render_widget(
        Paragraph::new(Text::from(lines))
            .wrap(Wrap { trim: false })
            .block(Block::default().title(" Execution ")),
        area,
    );
}

fn render_task_list(frame: &mut Frame, area: Rect, app: &App) {
    let Some(session) = app.execution.as_ref() else {
        frame.render_widget(
            Paragraph::new(" sem sessão de execução ")
                .block(Block::default().borders(Borders::ALL).title("Tasks")),
            area,
        );
        return;
    };
    let items = session
        .tasks
        .iter()
        .map(|run| {
            let style = task_status_style(run.status);
            let line = Line::from(vec![
                Span::styled(
                    format!(" {} ", run.status.marker()),
                    style.add_modifier(Modifier::BOLD),
                ),
                Span::styled(format!("{:<8}", truncate(&run.task.id, 8)), style),
                Span::raw(" "),
                Span::styled(truncate(&run.task.title, 28), style),
            ]);
            ListItem::new(line)
        })
        .collect::<Vec<_>>();
    let th = current_theme();
    let list = List::new(items)
        .block(Block::default().title(" Tasks "))
        .highlight_style(th.selection_style())
        .highlight_symbol("  ");
    let mut state = ListState::default();
    state.select(Some(
        session
            .selected_index
            .min(session.tasks.len().saturating_sub(1)),
    ));
    frame.render_stateful_widget(list, area, &mut state);
}

fn render_task_detail(frame: &mut Frame, area: Rect, app: &App) {
    let Some(session) = app.execution.as_ref() else {
        return;
    };
    let selected = app.selected_execution_task();
    let title = selected
        .map(|task| format!(" {} · {} ", task.task.id, session.detail_view.label()))
        .unwrap_or_else(|| " Detalhe ".to_string());
    let lines = match (selected, session.detail_view) {
        (Some(task), TaskDetailView::Log) => task_log_lines(task, area.height),
        (Some(task), TaskDetailView::Diff) => task_diff_lines(task, area.height),
        (Some(task), TaskDetailView::Report) => task_report_lines(task, area.height),
        (None, _) => vec![Line::from("  nenhuma task selecionada")],
    };
    frame.render_widget(
        Paragraph::new(Text::from(lines))
            .wrap(Wrap { trim: false })
            .block(Block::default().borders(Borders::ALL).title(title)),
        area,
    );
}

fn task_log_lines(task: &super::app::TaskRun, height: u16) -> Vec<Line<'static>> {
    let th = current_theme();
    let mut lines = vec![
        th.label_value("Status", task.status.label()),
        th.label_value("Task", &truncate(&task.task.title, 96)),
        Line::from(""),
    ];
    let limit = height.saturating_sub(5).max(1) as usize;
    if task.trace.is_empty() {
        lines.push(Line::from("  aguardando eventos da task…"));
    } else {
        let start = task.trace.len().saturating_sub(limit);
        for item in task.trace.iter().skip(start) {
            lines.push(timeline_line(item, 120));
        }
    }
    lines
}

fn task_diff_lines(task: &super::app::TaskRun, height: u16) -> Vec<Line<'static>> {
    let Some(diff) = task.diff.as_ref() else {
        return vec![Line::from("  diff ainda não disponível para esta task")];
    };
    if diff.is_empty() {
        return vec![Line::from("  sem alterações detectadas para esta task")];
    }
    let th = current_theme();
    let mut lines = vec![
        th.label_value("Arquivos", &diff.changed_files.join(", ")),
        Line::from(""),
    ];
    let text = if diff.patch.trim().is_empty() {
        diff.summary.as_str()
    } else {
        diff.patch.as_str()
    };
    let limit = height.saturating_sub(4).max(1) as usize;
    for line in text.lines().take(limit) {
        let style = if line.starts_with('+') && !line.starts_with("+++") {
            Style::default().fg(th.success)
        } else if line.starts_with('-') && !line.starts_with("---") {
            Style::default().fg(th.error)
        } else if line.starts_with("@@") {
            Style::default().fg(th.primary)
        } else {
            Style::default().fg(th.text_strong)
        };
        lines.push(Line::from(Span::styled(format!("  {line}"), style)));
    }
    lines
}

fn task_report_lines(task: &super::app::TaskRun, height: u16) -> Vec<Line<'static>> {
    if task.report.trim().is_empty() {
        return vec![Line::from(
            "  relatório ainda não disponível para esta task",
        )];
    }
    task.report
        .lines()
        .take(height.saturating_sub(2).max(1) as usize)
        .map(|line| Line::from(format!("  {line}")))
        .collect()
}

fn task_status_style(status: TaskRunStatus) -> Style {
    let th = current_theme();
    match status {
        TaskRunStatus::Pending => Style::default().fg(th.text_muted),
        TaskRunStatus::Running => Style::default().fg(th.primary),
        TaskRunStatus::Succeeded => Style::default().fg(th.success),
        TaskRunStatus::Failed => Style::default().fg(th.error),
        TaskRunStatus::Skipped => Style::default().fg(th.warning),
    }
}

fn progress_bar(done: usize, total: usize, width: usize) -> String {
    let width = width.clamp(8, 28);
    if total == 0 {
        return format!("[{}]", " ".repeat(width));
    }
    let filled = width.saturating_mul(done).checked_div(total).unwrap_or(0);
    format!(
        "[{}{}]",
        "█".repeat(filled),
        " ".repeat(width.saturating_sub(filled))
    )
}

fn render_parallel_running(frame: &mut Frame, area: Rect, app: &App) {
    if area.width >= 112 {
        let chunks = Layout::default()
            .direction(Direction::Horizontal)
            .constraints([Constraint::Length(50), Constraint::Min(52)])
            .split(area);
        render_parallel_overview(frame, chunks[0], app, false);
        render_parallel_slots(frame, chunks[1], app);
    } else {
        let chunks = Layout::default()
            .direction(Direction::Vertical)
            .constraints([Constraint::Length(13), Constraint::Min(8)])
            .split(area);
        render_parallel_overview(frame, chunks[0], app, false);
        render_parallel_slots(frame, chunks[1], app);
    }
}

fn render_parallel_decision(frame: &mut Frame, area: Rect, app: &App) {
    if area.width >= 112 {
        let chunks = Layout::default()
            .direction(Direction::Horizontal)
            .constraints([Constraint::Length(50), Constraint::Min(52)])
            .split(area);
        render_parallel_overview(frame, chunks[0], app, true);
        render_parallel_slots(frame, chunks[1], app);
    } else {
        let chunks = Layout::default()
            .direction(Direction::Vertical)
            .constraints([Constraint::Length(17), Constraint::Min(8)])
            .split(area);
        render_parallel_overview(frame, chunks[0], app, true);
        render_parallel_slots(frame, chunks[1], app);
    }
}

fn render_parallel_overview(frame: &mut Frame, area: Rect, app: &App, decision: bool) {
    let stage = app
        .parallel_stage
        .map(|stage| stage.key().to_string())
        .unwrap_or_else(|| "desconhecida".to_string());
    let total = app.parallel.len();
    let done = app.parallel_done_count();
    let errors = app.parallel_error_count();
    let running = app
        .parallel
        .iter()
        .filter(|slot| slot.status == AgentSlotStatus::EmExecucao)
        .count();
    let pending = app
        .parallel
        .iter()
        .filter(|slot| slot.status == AgentSlotStatus::Aguardando)
        .count();

    let th = current_theme();
    let mut lines = vec![
        Line::from(""),
        Line::from(vec![
            Span::styled(
                format!("  {} ", app.loader.current_frame()),
                Style::default().fg(th.primary),
            ),
            Span::styled(
                if decision {
                    "barreira concluída com falhas"
                } else {
                    "Agent Teams em execução"
                },
                Style::default()
                    .fg(if decision { th.warning } else { th.text_strong })
                    .add_modifier(Modifier::BOLD),
            ),
        ]),
        Line::from(""),
        metric_line("Etapa", &stage),
        metric_line("Slots", &format!("{done} de {total} concluídos")),
        metric_line("Rodando", &running.to_string()),
        metric_line("Fila", &pending.to_string()),
        metric_line("Erros", &errors.to_string()),
        metric_line("Uso", &parallel_usage_label(app)),
    ];
    if decision {
        lines.push(Line::from(""));
        lines.push(Line::from(vec![
            Span::styled(
                " r Retentar falhos ",
                Style::default()
                    .fg(th.on_primary)
                    .bg(th.primary)
                    .add_modifier(Modifier::BOLD),
            ),
            Span::raw("  "),
            Span::styled(" a Aceitar parcial ", Style::default().fg(th.text_strong)),
            Span::raw("  "),
            Span::styled(" Esc Cancelar ", Style::default().fg(th.text_muted)),
        ]));
    }

    frame.render_widget(
        Paragraph::new(Text::from(lines))
            .wrap(Wrap { trim: false })
            .block(Block::default().title(if decision {
                " Decisão "
            } else {
                " Execução paralela "
            })),
        area,
    );
}

fn render_parallel_slots(frame: &mut Frame, area: Rect, app: &App) {
    let visible = area.height.saturating_sub(3).max(1) as usize;
    let rows = app.parallel.iter().take(visible).map(|slot| {
        let last_trace = slot
            .trace
            .last()
            .map(|value| truncate(value, 54))
            .unwrap_or_else(|| "aguardando evento".to_string());
        Row::new(vec![
            slot.task_id.clone(),
            truncate(&slot.task_label, 30),
            slot_status_label(slot.status, app),
            last_trace,
        ])
        .style(slot_status_style(slot.status))
    });
    let th = current_theme();
    let header = Row::new(vec!["Task", "AgentSlot", "Status", "Último evento"])
        .style(Style::default().fg(th.primary).add_modifier(Modifier::BOLD));
    let table = Table::new(
        rows,
        [
            Constraint::Length(10),
            Constraint::Length(30),
            Constraint::Length(16),
            Constraint::Min(20),
        ],
    )
    .header(header)
    .block(Block::default().title(" AgentSlots "));
    frame.render_widget(table, area);
}

fn slot_status_label(status: AgentSlotStatus, app: &App) -> String {
    match status {
        AgentSlotStatus::Aguardando => "○ Aguardando".to_string(),
        AgentSlotStatus::EmExecucao => format!("{} Em execução", app.loader.current_frame()),
        AgentSlotStatus::Concluido => "✓ Concluído".to_string(),
        AgentSlotStatus::Erro => "✗ Erro".to_string(),
    }
}

fn slot_status_style(status: AgentSlotStatus) -> Style {
    let th = current_theme();
    match status {
        AgentSlotStatus::Aguardando => Style::default().fg(th.text_muted),
        AgentSlotStatus::EmExecucao => Style::default().fg(th.primary),
        AgentSlotStatus::Concluido => Style::default().fg(th.success),
        AgentSlotStatus::Erro => Style::default().fg(th.error),
    }
}

fn render_running_overview(frame: &mut Frame, area: Rect, app: &App) {
    let mut lines = vec![Line::from("")];
    let elapsed = app.loader.elapsed_label();
    let stage = app
        .viewer_stage
        .map(|stage| stage.key().to_string())
        .unwrap_or_else(|| "desconhecida".to_string());
    let th = current_theme();
    if app.loader.is_visible() {
        lines.push(Line::from(vec![
            Span::styled(
                format!("  {} ", app.loader.current_frame()),
                Style::default().fg(th.primary),
            ),
            Span::styled(
                app.loader.label.clone(),
                Style::default().fg(th.text_strong),
            ),
        ]));
    } else {
        lines.push(Line::from("  preparando…"));
    }
    lines.push(Line::from(""));
    lines.push(metric_line("Etapa", &stage));
    lines.push(metric_line("Tempo", &elapsed));
    lines.push(metric_line(
        "Status",
        if app.pending.is_some() {
            "provider em execução"
        } else {
            "sem subprocesso pendente"
        },
    ));
    lines.push(Line::from(""));
    lines.push(section_label("Uso e custo"));
    lines.push(metric_line("Atual", &current_or_last_usage_label(app)));
    lines.push(metric_line("Custo", &current_or_last_cost_label(app)));
    lines.push(metric_line("Quota", &current_provider_quota_label(app)));
    lines.push(metric_line("Sessão", &session_usage_label(app)));
    lines.push(Line::from(""));
    lines.push(section_label("Contexto enviado"));
    lines.extend(context_lines(
        app.current_context.as_ref().or(app.last_context.as_ref()),
    ));

    frame.render_widget(
        Paragraph::new(Text::from(lines))
            .wrap(Wrap { trim: false })
            .block(Block::default().borders(Borders::ALL).title("Execução")),
        area,
    );
}

fn render_running_timeline(frame: &mut Frame, area: Rect, app: &App) {
    let mut lines = Vec::new();
    if !app.provider_trace.is_empty() {
        let trace_limit = area.height.saturating_sub(2).clamp(6, 40) as usize;
        let start = app.provider_trace.len().saturating_sub(trace_limit);
        for item in app.provider_trace.iter().skip(start) {
            lines.push(timeline_line(item, area.width));
        }
    } else {
        lines.push(Line::from(""));
        lines.push(Line::from("  aguardando primeiro evento do provider…"));
    }
    frame.render_widget(
        Paragraph::new(Text::from(lines))
            .wrap(Wrap { trim: false })
            .block(
                Block::default()
                    .borders(Borders::ALL)
                    .title("Timeline do agente"),
            ),
        area,
    );
}

fn timeline_line(item: &str, width: u16) -> Line<'static> {
    let th = current_theme();
    let state = if item.starts_with("erro") {
        trace_widget::TraceState::Error
    } else if item.starts_with("aviso") {
        trace_widget::TraceState::Running
    } else if item.starts_with("tokens")
        || item.starts_with("prompt")
        || item.starts_with("markdown")
        || item.starts_with("artifact")
        || item.starts_with("skills Codex")
        || item.starts_with("skill compat")
    {
        trace_widget::TraceState::Done
    } else {
        trace_widget::TraceState::Pending
    };
    let text = truncate(item, width.saturating_sub(8) as usize);
    trace_widget::trace_line(state, &text, false, &th)
}

fn render_viewer(frame: &mut Frame, area: Rect, app: &App) {
    if (app.last_usage.is_some() || app.last_context.is_some()) && area.height >= 16 {
        let chunks = Layout::default()
            .direction(Direction::Vertical)
            .constraints([Constraint::Length(5), Constraint::Min(8)])
            .split(area);
        render_run_receipt(frame, chunks[0], app);
        render_viewer_body(frame, chunks[1], app);
    } else {
        render_viewer_body(frame, area, app);
    }
}

fn render_run_receipt(frame: &mut Frame, area: Rect, app: &App) {
    let usage = last_usage_label(app);
    let cost = current_or_last_cost_label(app);
    let context = app
        .last_context
        .as_ref()
        .map(|context| {
            format!(
                "contexto ~{} tokens · {} artefato(s){}",
                format_usize(context.prompt_tokens_estimate),
                context.artifact_count,
                if context.truncated {
                    " · truncado"
                } else {
                    ""
                }
            )
        })
        .unwrap_or_else(|| "contexto não registrado".to_string());
    let th = current_theme();
    let lines = Text::from(vec![
        th.label_value(
            "Uso",
            &truncate(&usage, area.width.saturating_sub(8) as usize),
        ),
        th.label_value(
            "Custo",
            &truncate(&cost, area.width.saturating_sub(10) as usize),
        ),
        th.label_value(
            "Contexto",
            &truncate(&context, area.width.saturating_sub(13) as usize),
        ),
    ]);
    frame.render_widget(
        Paragraph::new(lines)
            .wrap(Wrap { trim: false })
            .block(Block::default().title(" Recibo da geração ")),
        area,
    );
}

fn render_viewer_body(frame: &mut Frame, area: Rect, app: &App) {
    let text = app
        .viewer_text
        .clone()
        .unwrap_or_else(|| Text::from("(vazio)"));
    let title = app
        .viewer_stage
        .map(|s| format!(" {} ", s.label()))
        .unwrap_or_else(|| " Artefato ".to_string());
    let p = Paragraph::new(text)
        .wrap(Wrap { trim: false })
        .scroll((app.viewer_scroll, 0))
        .block(Block::default().borders(Borders::ALL).title(title));
    frame.render_widget(p, area);
}

fn render_checkpoint(frame: &mut Frame, area: Rect, app: &App) {
    let stage = app.viewer_stage.unwrap_or(Stage::Idea);
    let stage_label = stage.label();
    let artifact = app
        .active
        .as_ref()
        .map(|active| active.path.join(stage.filename()).display().to_string())
        .unwrap_or_else(|| stage.filename().to_string());
    let provider = app
        .provider_selection
        .as_ref()
        .map(|p| {
            let model = p.stage_models.get(stage.key()).unwrap_or(&p.model);
            let effort = p.stage_efforts.get(stage.key()).unwrap_or(&p.effort);
            format!("{} / {} / {}", p.id, model, effort)
        })
        .unwrap_or_else(|| "auto / default".to_string());
    let th = current_theme();
    let contexto = app
        .last_context
        .as_ref()
        .map(|context| {
            format!(
                "~{} tokens · {} artefato(s){}",
                format_usize(context.prompt_tokens_estimate),
                context.artifact_count,
                if context.truncated {
                    " · truncado"
                } else {
                    ""
                }
            )
        })
        .unwrap_or_else(|| "não registrado".to_string());
    let body = Text::from(vec![
        Line::from(""),
        th.label_value(
            "Decisão",
            &format!("aprovar, ajustar ou regenerar {stage_label}"),
        ),
        th.label_value("Artefato", &artifact),
        th.label_value("Provider/modelo/effort", &provider),
        th.label_value("Uso artefato", &last_usage_label(app)),
        th.label_value("Custo estimado", &current_or_last_cost_label(app)),
        th.label_value("Contexto", &contexto),
        th.label_value("Sessão atual", &session_usage_label(app)),
        th.label_value(
            "Critério",
            "seções obrigatórias, rastreabilidade e evidência suficientes",
        ),
        Line::from(""),
        Line::from(vec![
            Span::styled(
                " a Aprovar ",
                Style::default()
                    .fg(th.on_primary)
                    .bg(th.primary)
                    .add_modifier(Modifier::BOLD),
            ),
            Span::raw("  "),
            Span::styled(" r Regenerar ", Style::default().fg(th.text_strong)),
            Span::raw("  "),
            Span::styled(" e Editar ", Style::default().fg(th.text_strong)),
            Span::raw("  "),
            Span::styled(" Esc Voltar ", Style::default().fg(th.text_muted)),
        ]),
    ]);
    let p = Paragraph::new(body)
        .wrap(Wrap { trim: false })
        .block(Block::default().title(" Checkpoint de aprovação "));
    frame.render_widget(p, area);
}

fn render_refinement_prompt(frame: &mut Frame, area: Rect, _app: &App) {
    let th = current_theme();
    let body = Text::from(vec![
        Line::from(""),
        Line::from(Span::styled(
            "  O Refinement é opcional. Deseja executá-lo?",
            Style::default().fg(th.text_strong),
        )),
        Line::from(""),
        Line::from(vec![
            Span::styled(
                " r Executar ",
                Style::default()
                    .fg(th.on_primary)
                    .bg(th.primary)
                    .add_modifier(Modifier::BOLD),
            ),
            Span::raw("  "),
            Span::styled(" s Pular ", Style::default().fg(th.text_strong)),
            Span::raw("  "),
            Span::styled(" Esc Voltar ", Style::default().fg(th.text_muted)),
        ]),
    ]);
    frame.render_widget(
        Paragraph::new(body)
            .wrap(Wrap { trim: false })
            .block(Block::default().title(" Refinement ")),
        area,
    );
}

fn render_error(frame: &mut Frame, area: Rect, app: &App) {
    let th = current_theme();
    let raw = app
        .error
        .clone()
        .unwrap_or_else(|| "erro desconhecido".to_string());
    let overwrite_conflict = raw.contains("refusing to overwrite existing artifact");
    let mut lines = vec![Line::from(Span::styled(
        "  Falha ao executar a etapa:",
        Style::default().fg(th.error).add_modifier(Modifier::BOLD),
    ))];
    if overwrite_conflict {
        lines.push(Line::from(""));
        lines.push(Line::from(Span::styled(
            "  Um artefato local já existe. Para proteger checkpoints, o SDD não sobrescreve sem decisão explícita.",
            Style::default().fg(th.text_muted),
        )));
    }
    if !app.provider_trace.is_empty() {
        lines.push(Line::from(""));
        lines.push(Line::from(Span::styled(
            "  Trace do provider:",
            Style::default()
                .fg(th.section_header)
                .add_modifier(Modifier::BOLD),
        )));
        for item in app.provider_trace.iter().take(12) {
            lines.push(trace_widget::trace_line(
                trace_widget::TraceState::Pending,
                item,
                false,
                &th,
            ));
        }
        lines.push(Line::from(""));
    }
    for l in raw.lines().take(20) {
        lines.push(Line::from(Span::styled(
            format!("  {l}"),
            Style::default().fg(th.text_strong),
        )));
    }
    lines.push(Line::from(""));
    if overwrite_conflict {
        lines.push(Line::from(vec![
            Span::styled(
                " r regenerar sobrescrevendo ",
                Style::default()
                    .fg(th.on_primary)
                    .bg(th.primary)
                    .add_modifier(Modifier::BOLD),
            ),
            Span::raw("  "),
            Span::styled(" Esc Voltar ", Style::default().fg(th.text_muted)),
        ]));
    } else {
        lines.push(Line::from(vec![
            Span::styled(
                " r tentar novamente ",
                Style::default()
                    .fg(th.on_primary)
                    .bg(th.primary)
                    .add_modifier(Modifier::BOLD),
            ),
            Span::raw("  "),
            Span::styled(" e editar config ", Style::default().fg(th.text_strong)),
            Span::raw("  "),
            Span::styled(" Esc Voltar ", Style::default().fg(th.text_muted)),
        ]));
    }
    let p = Paragraph::new(Text::from(lines))
        .wrap(Wrap { trim: false })
        .block(
            Block::default()
                .borders(Borders::ALL)
                .border_style(Style::default().fg(th.error))
                .title(" Erro "),
        );
    frame.render_widget(p, area);
}

fn render_help(frame: &mut Frame, area: Rect, app: &App) {
    let th = current_theme();
    let rows = help_rows(app.help_return);
    let mut lines = vec![Line::from("")];
    for (key, desc) in rows {
        lines.push(Line::from(vec![
            Span::styled(
                format!("  {key:<10}"),
                Style::default().fg(th.primary).add_modifier(Modifier::BOLD),
            ),
            Span::styled(desc, Style::default().fg(th.text_strong)),
        ]));
    }
    frame.render_widget(
        Paragraph::new(Text::from(lines)).block(Block::default().title(" Ajuda ")),
        area,
    );
}

fn help_rows(screen: Screen) -> Vec<(&'static str, &'static str)> {
    match screen {
        Screen::ProviderSetup => vec![
            ("↑/↓, Tab", "selecionar provider"),
            ("←/→", "selecionar modelo inicial"),
            ("Ctrl+N", "avançar modelo inicial"),
            ("[/]", "alternar effort inicial"),
            ("Enter", "usar provider e abrir orquestrações"),
            ("r", "recarregar sdd.config.yaml"),
            ("e", "editar sdd.config.yaml"),
            ("/", "abrir menu TUI"),
            ("Esc", "voltar sem trocar"),
            ("q / Ctrl+C", "sair"),
        ],
        Screen::OrchestrationList => vec![
            ("↑/↓, j/k", "navegar na lista"),
            ("Enter", "abrir orquestração"),
            ("n", "criar nova orquestração"),
            ("p", "trocar provider/modelo"),
            ("Tab", "abrir provider dialog"),
            ("/", "abrir menu TUI"),
            ("q / Ctrl+C", "sair"),
        ],
        Screen::StageBoard => vec![
            ("↑/↓, j/k", "selecionar etapa"),
            ("Enter", "executar ou reler etapa"),
            ("p", "trocar provider/modelo"),
            ("Tab", "abrir provider dialog da etapa"),
            ("←/→", "trocar modelo da etapa"),
            ("[/]", "trocar effort da etapa"),
            ("/", "abrir menu TUI"),
            ("Esc", "voltar à lista"),
            ("q", "sair"),
        ],
        Screen::Viewer => vec![
            ("↑/↓, j/k", "rolar o artefato"),
            ("a / Enter", "ir ao checkpoint"),
            ("r", "regenerar artefato via provider"),
            ("Tab", "abrir provider dialog da etapa"),
            ("←/→", "trocar modelo da etapa"),
            ("[/]", "trocar effort da etapa"),
            ("/", "abrir menu TUI"),
            ("Esc", "voltar ao fluxo"),
        ],
        Screen::TaskExecution => vec![
            ("↑/↓, j/k", "selecionar task"),
            ("Tab", "alternar detalhe entre Log, Diff e Report"),
            ("Shift+Tab", "voltar detalhe"),
            ("r", "retentar task selecionada se ela falhou"),
            ("Esc", "voltar ao fluxo quando nenhuma task estiver rodando"),
            ("q / Ctrl+C", "sair"),
        ],
        Screen::TaskDecision => vec![
            ("↑/↓, j/k", "selecionar task"),
            ("Tab", "alternar detalhe entre Log, Diff e Report"),
            ("r", "retentar task com erro"),
            ("a / Enter", "aceitar resultado parcial e gerar artefato"),
            ("Esc", "cancelar execução e voltar ao fluxo"),
        ],
        Screen::ParallelRunning => vec![
            ("q / Ctrl+C", "sair mantendo o terminal limpo"),
            ("?", "abrir ajuda"),
        ],
        Screen::ParallelDecision => vec![
            ("r", "retentar apenas AgentSlots em erro"),
            ("a / Enter", "aceitar resultado parcial e compor artefato"),
            ("Esc", "cancelar etapa e voltar ao fluxo"),
        ],
        Screen::Checkpoint => vec![
            ("a", "aprovar e avançar"),
            ("r", "regenerar a etapa via provider"),
            ("e", "editar no $EDITOR"),
            ("Tab", "abrir provider dialog da etapa"),
            ("←/→", "trocar modelo da etapa"),
            ("[/]", "trocar effort da etapa"),
            ("/", "abrir menu TUI"),
            ("Esc", "voltar ao artefato"),
        ],
        _ => vec![("Esc", "voltar"), ("q", "sair"), ("?", "ajuda")],
    }
}

fn render_summary(frame: &mut Frame, area: Rect, app: &App) {
    let Some(active) = &app.active else {
        return;
    };
    let th = current_theme();
    let header = Row::new(vec!["Etapa", "Arquivo", "Estado", "Aprovado em"])
        .style(Style::default().fg(th.primary).add_modifier(Modifier::BOLD));
    let rows: Vec<Row> = active
        .stages
        .iter()
        .map(|st| {
            Row::new(vec![
                st.stage.label().to_string(),
                if st.file.is_empty() {
                    st.stage.filename().to_string()
                } else {
                    st.file.clone()
                },
                format!("{:?}", st.status),
                st.approved_at.clone().unwrap_or_default(),
            ])
            .style(Style::default().fg(th.text_strong))
        })
        .collect();
    let widths = [
        Constraint::Length(12),
        Constraint::Length(22),
        Constraint::Length(12),
        Constraint::Min(10),
    ];
    let table = Table::new(rows, widths)
        .header(header)
        .block(Block::default().title(format!(" Resumo · {} ", truncate(&active.name, 36))));
    frame.render_widget(table, area);
}

fn screen_title(screen: Screen) -> &'static str {
    match screen {
        Screen::Welcome => "Bem-vindo",
        Screen::ProviderSetup => "Provider",
        Screen::OrchestrationList => "Orquestrações",
        Screen::NewOrchestration => "Nova orquestração",
        Screen::StageBoard => "Fluxo",
        Screen::Running => "Executando",
        Screen::TaskExecution => "Execution",
        Screen::TaskDecision => "Decisão de task",
        Screen::ParallelRunning => "Agent Teams",
        Screen::ParallelDecision => "Decisão paralela",
        Screen::Viewer => "Artefato",
        Screen::Checkpoint => "Checkpoint",
        Screen::RefinementPrompt => "Refinement",
        Screen::ErrorPanel => "Erro",
        Screen::Help => "Ajuda",
        Screen::Summary => "Resumo",
    }
}

fn slash_scope_label(app: &App) -> String {
    match app.screen {
        Screen::StageBoard => format!(
            "Step {}",
            Stage::GUIDED[app.board_index.min(Stage::GUIDED.len() - 1)].label()
        ),
        Screen::Running | Screen::Viewer | Screen::Checkpoint => app
            .viewer_stage
            .map(|stage| format!("Step {}", stage.label()))
            .unwrap_or_else(|| "Build".to_string()),
        Screen::RefinementPrompt => "Step Refinement".to_string(),
        Screen::TaskExecution | Screen::TaskDecision => "Step Execution".to_string(),
        Screen::ParallelRunning | Screen::ParallelDecision => app
            .parallel_stage
            .map(|stage| format!("Step {}", stage.label()))
            .unwrap_or_else(|| "Agent Team".to_string()),
        Screen::ProviderSetup => "Provider".to_string(),
        _ => "Build".to_string(),
    }
}

fn slash_provider_route(app: &App) -> String {
    app.providers
        .get(app.provider_index)
        .map(|provider| {
            format!(
                "{} · {} · {}",
                provider.id,
                selected_model_id(provider, app.provider_model_index),
                selected_effort_label(provider, app.provider_effort_index)
            )
        })
        .unwrap_or_else(|| active_provider_summary(app))
}

fn active_provider_summary(app: &App) -> String {
    if app.screen == Screen::ProviderSetup {
        if let Some(provider) = app.providers.get(app.provider_index) {
            return format!(
                "{} · {} · {}",
                provider.id,
                selected_model_id(provider, app.provider_model_index),
                selected_effort_label(provider, app.provider_effort_index)
            );
        }
    }
    app.provider_selection
        .as_ref()
        .map(|provider| format!("{} · {} · {}", provider.id, provider.model, provider.effort))
        .unwrap_or_else(|| "provider auto · modelo default · effort medium".to_string())
}

fn truncate(s: &str, max: usize) -> String {
    if s.chars().count() <= max {
        s.to_string()
    } else {
        let mut out: String = s.chars().take(max.saturating_sub(1)).collect();
        out.push('…');
        out
    }
}

fn auth_label(auth_present: Option<bool>) -> &'static str {
    match auth_present {
        Some(true) => "presente",
        Some(false) => "ausente",
        None => "não requerido",
    }
}

fn auth_color(auth_present: Option<bool>, theme: &theme::Theme) -> Color {
    match auth_present {
        Some(true) => theme.success,
        Some(false) => theme.warning,
        None => theme.text_muted,
    }
}

fn auth_methods_label(methods: &[ProviderAuthStatus]) -> String {
    if methods.is_empty() {
        return "não declarado".to_string();
    }
    methods
        .iter()
        .map(|method| {
            let state = if method
                .detail
                .as_deref()
                .is_some_and(|detail| detail.contains("modo rápido"))
            {
                "instalado"
            } else if method.present {
                "presente"
            } else {
                "ausente"
            };
            format!("{}:{}", method.label, state)
        })
        .collect::<Vec<_>>()
        .join(", ")
}

fn auth_login_hint(methods: &[ProviderAuthStatus]) -> String {
    if methods.iter().any(|method| {
        method
            .detail
            .as_deref()
            .is_some_and(|detail| detail.contains("modo rápido"))
    }) {
        return "rode `sdd providers doctor` para validar login real".to_string();
    }
    let mut commands = methods
        .iter()
        .filter(|method| !method.present)
        .filter_map(|method| method.login_command.as_deref())
        .filter(|command| !command.trim().is_empty())
        .collect::<Vec<_>>();
    commands.sort_unstable();
    commands.dedup();
    if commands.is_empty() {
        return "sessão pronta ou variável de ambiente configurada".to_string();
    }
    commands
        .into_iter()
        .map(|command| format!("rode `{command}`"))
        .collect::<Vec<_>>()
        .join(" · ")
}

fn selected_model_id(provider: &ProviderCatalogEntry, index: usize) -> String {
    provider
        .models
        .get(index.min(provider.models.len().saturating_sub(1)))
        .map(|model| model.id.clone())
        .or_else(|| provider.model.clone())
        .unwrap_or_else(|| "default/config".to_string())
}

fn selected_model_label(provider: &ProviderCatalogEntry, index: usize) -> String {
    provider
        .models
        .get(index.min(provider.models.len().saturating_sub(1)))
        .map(|model| {
            if model.label == model.id {
                model.id.clone()
            } else {
                format!("{} ({})", model.label, model.id)
            }
        })
        .or_else(|| provider.model.clone())
        .unwrap_or_else(|| "default/config".to_string())
}

fn selected_effort_label(provider: &ProviderCatalogEntry, index: usize) -> String {
    provider
        .efforts
        .get(index.min(provider.efforts.len().saturating_sub(1)))
        .cloned()
        .or_else(|| provider.effort.clone())
        .unwrap_or_else(|| "medium".to_string())
}

fn effort_segments(
    provider: &ProviderCatalogEntry,
    selected_index: usize,
    fallback: &str,
) -> Line<'static> {
    let efforts = if provider.efforts.is_empty() {
        vec![fallback.to_string()]
    } else {
        provider.efforts.clone()
    };
    let th = current_theme();
    let mut spans = vec![Span::styled("  Effort  ", Style::default().fg(th.primary))];
    for (idx, effort) in efforts.iter().enumerate() {
        let selected = idx == selected_index.min(efforts.len().saturating_sub(1));
        let style = if selected {
            Style::default()
                .fg(th.on_primary)
                .bg(th.primary)
                .add_modifier(Modifier::BOLD)
        } else {
            Style::default().fg(th.text_muted)
        };
        spans.push(Span::styled(format!(" {effort} "), style));
        spans.push(Span::raw(" "));
    }
    spans.push(Span::styled(
        "[/] toggle",
        Style::default().fg(th.text_muted),
    ));
    Line::from(spans)
}

fn current_provider_quota_label(app: &App) -> String {
    app.provider_selection
        .as_ref()
        .map(|provider| provider_usage_limits_label(&provider.id, &provider.usage_limits))
        .unwrap_or_else(|| "provider não resolvido".to_string())
}

fn parallel_usage_label(app: &App) -> String {
    let mut total = TokenUsageSample::default();
    let mut count = 0usize;
    for usage in app.parallel.iter().filter_map(|slot| slot.usage.as_ref()) {
        total.input_tokens_estimate += usage.input_tokens_estimate;
        total.cached_input_tokens_estimate += usage.cached_input_tokens_estimate;
        total.output_tokens_estimate += usage.output_tokens_estimate;
        total.reasoning_output_tokens_estimate += usage.reasoning_output_tokens_estimate;
        total.total_tokens_estimate += usage.total_tokens_estimate;
        total.input_bytes += usage.input_bytes;
        total.output_bytes += usage.output_bytes;
        total.provider_reported |= usage.provider_reported;
        count += 1;
    }
    if count == 0 {
        return "aguardando amostras".to_string();
    }
    format!("{} slot(s) · {}", count, usage_sample_label(&total))
}

fn execution_usage_label(app: &App) -> String {
    let mut total = TokenUsageSample::default();
    let mut count = 0usize;
    if let Some(session) = app.execution.as_ref() {
        for usage in session.tasks.iter().filter_map(|task| task.usage.as_ref()) {
            total.input_tokens_estimate += usage.input_tokens_estimate;
            total.cached_input_tokens_estimate += usage.cached_input_tokens_estimate;
            total.output_tokens_estimate += usage.output_tokens_estimate;
            total.reasoning_output_tokens_estimate += usage.reasoning_output_tokens_estimate;
            total.total_tokens_estimate += usage.total_tokens_estimate;
            total.input_bytes += usage.input_bytes;
            total.output_bytes += usage.output_bytes;
            total.provider_reported |= usage.provider_reported;
            count += 1;
        }
    }
    if count == 0 {
        return "aguardando amostras".to_string();
    }
    format!("{} task(s) · {}", count, usage_sample_label(&total))
}

fn provider_usage_limits_label(provider_id: &str, limits: &[ProviderUsageLimit]) -> String {
    if limits.is_empty() && provider_id == "codex" {
        return "Codex CLI não expõe saldo real; usando sessão local".to_string();
    }
    usage_limits_label(limits)
}

fn usage_limits_label(limits: &[ProviderUsageLimit]) -> String {
    if limits.is_empty() {
        return "não informado pelo provider".to_string();
    }
    limits
        .iter()
        .take(4)
        .map(usage_limit_label)
        .collect::<Vec<_>>()
        .join(" · ")
}

fn usage_limit_label(limit: &ProviderUsageLimit) -> String {
    let unit = usage_unit_label(&limit.unit);
    let window = usage_window_label(&limit.window);
    let amount = match (limit.remaining, limit.limit) {
        (Some(remaining), Some(total)) => {
            format!(
                "{} de {} {}",
                format_amount(remaining),
                format_amount(total),
                unit
            )
        }
        (Some(remaining), None) => format!("{} {} restantes", format_amount(remaining), unit),
        (None, Some(total)) => format!("limite {} {}", format_amount(total), unit),
        (None, None) => "saldo não informado".to_string(),
    };
    let mut label = if window.is_empty() {
        amount
    } else {
        format!("{amount}/{window}")
    };
    if let Some(resets_at) = limit.resets_at.as_deref().filter(|value| !value.is_empty()) {
        label.push_str(&format!(" reset {resets_at}"));
    }
    if let Some(source) = limit.source.as_deref().filter(|value| !value.is_empty()) {
        label.push_str(&format!(" ({source})"));
    }
    label
}

fn usage_unit_label(unit: &str) -> &str {
    match unit.trim().to_ascii_lowercase().as_str() {
        "hour" | "hours" | "hora" | "horas" => "h",
        "request" | "requests" | "requisicao" | "requisicoes" => "req",
        "credit" | "credits" | "credito" | "creditos" => "créditos",
        "token" | "tokens" => "tokens",
        _ if unit.trim().is_empty() => "unid.",
        _ => unit.trim(),
    }
}

fn usage_window_label(window: &str) -> &str {
    match window.trim().to_ascii_lowercase().as_str() {
        "hour" | "hourly" | "hora" => "hora",
        "day" | "daily" | "dia" => "dia",
        "week" | "weekly" | "semana" => "semana",
        "month" | "monthly" | "mes" | "mês" => "mês",
        _ => window.trim(),
    }
}

fn format_amount(value: f64) -> String {
    if (value.fract()).abs() < f64::EPSILON {
        format!("{}", value as i64)
    } else {
        format!("{value:.1}")
    }
}

fn token_budget_remaining_label(budget: u32, used: usize) -> String {
    let budget = budget as usize;
    let remaining = budget.saturating_sub(used);
    format!("~{} tokens", format_usize(remaining))
}

fn metric_line(label: &'static str, value: &str) -> Line<'static> {
    let th = current_theme();
    Line::from(vec![
        Span::styled(format!("  {label:<8}"), Style::default().fg(th.primary)),
        Span::styled(truncate(value, 42), Style::default().fg(th.text_strong)),
    ])
}

fn section_label(label: &'static str) -> Line<'static> {
    let th = current_theme();
    Line::from(Span::styled(
        format!("  {label}"),
        Style::default()
            .fg(th.section_header)
            .add_modifier(Modifier::BOLD),
    ))
}

fn context_lines(context: Option<&StageContextSample>) -> Vec<Line<'static>> {
    let Some(context) = context else {
        return vec![metric_line("Prompt", "aguardando montagem")];
    };
    let mut lines = vec![metric_line(
        "Prompt",
        &format!(
            "~{} tokens · {} bytes",
            format_usize(context.prompt_tokens_estimate),
            format_usize(context.prompt_bytes)
        ),
    )];
    lines.push(metric_line(
        "Artef.",
        &format!(
            "{} arquivo(s) · {} chars{}",
            context.artifact_count,
            format_usize(context.artifact_chars),
            if context.truncated {
                " · truncado"
            } else {
                ""
            }
        ),
    ));
    lines.push(metric_line(
        "Janela",
        &context
            .context_window_tokens
            .map(|window| {
                let pct = (context.prompt_tokens_estimate as f64 / window.max(1) as f64) * 100.0;
                format!(
                    "{} tokens · {:.1}% usado",
                    format_usize(window as usize),
                    pct
                )
            })
            .unwrap_or_else(|| "não configurada no provider".to_string()),
    ));
    lines
}

fn session_usage_label(app: &App) -> String {
    let usage = &app.session_usage;
    let current = app.current_usage.as_ref();
    let generation_count = usage.generation_count + usize::from(current.is_some());
    if generation_count == 0 {
        return "sem consumo estimado nesta sessão".to_string();
    }
    let input = usage.input_tokens_estimate
        + current
            .map(|sample| sample.input_tokens_estimate)
            .unwrap_or_default();
    let output = usage.output_tokens_estimate
        + current
            .map(|sample| sample.output_tokens_estimate)
            .unwrap_or_default();
    let total = usage.total_tokens_estimate
        + current
            .map(|sample| sample.total_tokens_estimate)
            .unwrap_or_default();
    let provider_reported_count = usage.provider_reported_count
        + usize::from(current.is_some_and(|sample| sample.provider_reported));
    let source = if provider_reported_count == generation_count {
        "reportado"
    } else if provider_reported_count == 0 {
        "estimado"
    } else {
        "misto"
    };
    format!(
        "{source}: in ~{} · out ~{} · total ~{} tokens · {} geração(ões)",
        format_usize(input),
        format_usize(output),
        format_usize(total),
        generation_count
    )
}

fn current_or_last_usage_label(app: &App) -> String {
    app.current_usage
        .as_ref()
        .or(app.last_usage.as_ref())
        .map(usage_sample_label)
        .unwrap_or_else(|| "aguardando primeira amostra".to_string())
}

fn current_or_last_cost_label(app: &App) -> String {
    app.current_usage
        .as_ref()
        .or(app.last_usage.as_ref())
        .map(cost_label)
        .unwrap_or_else(|| "aguardando usage".to_string())
}

fn last_usage_label(app: &App) -> String {
    app.last_usage
        .as_ref()
        .map(usage_sample_label)
        .unwrap_or_else(|| "sem geração nesta sessão".to_string())
}

fn cost_label(sample: &TokenUsageSample) -> String {
    let Some(cost) = &sample.cost else {
        return "preço não configurado no modelo".to_string();
    };
    format!(
        "{} {:.6} · in {:.6} · cache {:.6} · out {:.6}",
        cost.currency, cost.total_cost, cost.input_cost, cost.cached_input_cost, cost.output_cost
    )
}

fn usage_sample_label(sample: &TokenUsageSample) -> String {
    let source = if sample.provider_reported {
        "reportado"
    } else {
        "estimado"
    };
    format!(
        "{source}: in ~{}{} · out ~{}{} · total ~{} tokens",
        format_usize(sample.input_tokens_estimate),
        if sample.cached_input_tokens_estimate > 0 {
            format!(
                " (cache ~{})",
                format_usize(sample.cached_input_tokens_estimate)
            )
        } else {
            String::new()
        },
        format_usize(sample.output_tokens_estimate),
        if sample.reasoning_output_tokens_estimate > 0 {
            format!(
                " (reasoning ~{})",
                format_usize(sample.reasoning_output_tokens_estimate)
            )
        } else {
            String::new()
        },
        format_usize(sample.total_tokens_estimate)
    )
}

fn format_usize(value: usize) -> String {
    let raw = value.to_string();
    let mut out = String::new();
    for (idx, ch) in raw.chars().rev().enumerate() {
        if idx > 0 && idx % 3 == 0 {
            out.push('.');
        }
        out.push(ch);
    }
    out.chars().rev().collect()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::tui::app::{ExecutionSession, TaskRun};
    use crate::tui::execution::{DiffReport, ExecutionTask};
    use crate::tui::parallel::{AgentHandle, AgentSlotStatus};
    use ratatui::backend::TestBackend;
    use ratatui::Terminal;
    use std::path::PathBuf;
    use std::sync::mpsc;

    fn draw(app: &App, w: u16, h: u16) {
        let backend = TestBackend::new(w, h);
        let mut terminal = Terminal::new(backend).unwrap();
        terminal.draw(|f| render(f, app)).unwrap();
    }

    fn parallel_handle(id: &str, status: AgentSlotStatus) -> AgentHandle {
        let (_tx, rx) = mpsc::channel();
        AgentHandle {
            task_id: id.to_string(),
            task_label: format!("Task {id}"),
            rx,
            status,
            trace: vec![format!("trace {id}")],
            usage: None,
            result: None,
            fragment_path: PathBuf::from(format!("/tmp/{id}.md")),
        }
    }

    fn task_run(id: &str, status: TaskRunStatus) -> TaskRun {
        TaskRun {
            task: ExecutionTask {
                id: id.to_string(),
                title: format!("Task {id}"),
                body: String::new(),
                dependencies: Vec::new(),
                independent: true,
            },
            status,
            trace: vec![format!("trace {id}")],
            usage: None,
            result: None,
            report_path: PathBuf::from(format!("/tmp/{id}.md")),
            report: format!("Relatório {id}"),
            diff: Some(DiffReport {
                changed_files: vec![format!("src/{id}.rs")],
                summary: format!("M src/{id}.rs"),
                patch: format!("--- a/src/{id}.rs\n+++ b/src/{id}.rs\n"),
            }),
            before: None,
        }
    }

    fn execution_session() -> ExecutionSession {
        ExecutionSession {
            stage: Stage::Execution,
            force: false,
            tasks: vec![
                task_run("TSK-01", TaskRunStatus::Running),
                task_run("TSK-02", TaskRunStatus::Succeeded),
                task_run("TSK-03", TaskRunStatus::Failed),
            ],
            selected_index: 1,
            detail_view: TaskDetailView::Diff,
            current_index: Some(0),
            rx: None,
        }
    }

    #[test]
    fn renders_empty_list_no_panic() {
        let app = App::new(PathBuf::from("."), PathBuf::from("sdd"));
        for (w, h) in [(40, 12), (80, 24), (120, 40)] {
            draw(&app, w, h);
        }
    }

    #[test]
    fn renders_all_screens_no_panic() {
        let mut app = App::new(PathBuf::from("."), PathBuf::from("sdd"));
        for screen in [
            Screen::ProviderSetup,
            Screen::OrchestrationList,
            Screen::NewOrchestration,
            Screen::Running,
            Screen::TaskExecution,
            Screen::TaskDecision,
            Screen::ParallelRunning,
            Screen::ParallelDecision,
            Screen::ErrorPanel,
            Screen::Help,
        ] {
            app.screen = screen;
            if screen == Screen::ErrorPanel {
                app.error = Some("linha1\nlinha2".to_string());
            }
            if matches!(screen, Screen::ParallelRunning | Screen::ParallelDecision) {
                app.parallel_stage = Some(Stage::Execution);
                app.parallel = vec![
                    parallel_handle("T-01", AgentSlotStatus::EmExecucao),
                    parallel_handle("T-02", AgentSlotStatus::Concluido),
                    parallel_handle("T-03", AgentSlotStatus::Erro),
                ];
            }
            if matches!(screen, Screen::TaskExecution | Screen::TaskDecision) {
                app.execution = Some(execution_session());
            }
            draw(&app, 80, 24);
            draw(&app, 120, 32);
        }
    }

    #[test]
    fn renders_overlays_no_panic() {
        let mut app = App::new(PathBuf::from("."), PathBuf::from("sdd"));
        app.screen = Screen::OrchestrationList;
        for overlay in [
            Overlay::CommandPalette,
            Overlay::SlashMenu,
            Overlay::ProviderDialog,
            Overlay::Telemetry,
            Overlay::Confirm,
        ] {
            app.overlay = overlay;
            if overlay == Overlay::CommandPalette {
                app.palette.open();
                app.palette.push_char('n');
            }
            if overlay == Overlay::SlashMenu {
                app.slash_query = "model".to_string();
                app.slash_index = 0;
            }
            for (w, h) in [(40, 12), (80, 24), (120, 40)] {
                draw(&app, w, h);
            }
        }
    }

    #[test]
    fn slash_menu_rect_alinha_com_largura_do_campo() {
        let area = Rect::new(2, 1, 120, 40);
        let rect = slash_menu_rect(area, 14);

        assert_eq!(rect.x, area.x);
        assert_eq!(rect.width, area.width);
        assert_eq!(rect.height, 14);
        assert_eq!(rect.y, 23);
    }

    /// Guardrail M-1: nenhuma cor literal de `ratatui::style::Color` deve ser usada
    /// diretamente em `ui.rs` — toda cor vem do tema/sistema de componentes.
    #[test]
    fn ui_nao_usa_cor_literal_ad_hoc() {
        let src = include_str!("ui.rs");
        let needle = concat!("Color", "::");
        assert!(
            !src.contains(needle),
            "ui.rs deve usar tokens do tema (theme.rs) em vez de cores literais"
        );
    }
}
