o7 0.1.1

O7 workflow DSL runner
Documentation
//! Wizard UI — ratatui drawing for the o7 wiz wizard.

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

use crate::wiz::types::{WizPhase, WizState};

/// Draw the full wizard screen.
pub fn draw_wiz(frame: &mut Frame, state: &WizState) {
    let area = frame.area();

    match state.phase {
        WizPhase::Writing => {
            let para =
                Paragraph::new("Writing configuration...").style(Style::default().fg(Color::Cyan));
            frame.render_widget(para, area);
            return;
        }
        WizPhase::Done => {
            let (text, color) = if state.error.is_some() {
                (
                    format!(
                        "Error writing config: {}",
                        state.error.as_deref().unwrap_or("unknown")
                    ),
                    Color::Red,
                )
            } else {
                ("Configuration saved!".to_string(), Color::Green)
            };
            let para = Paragraph::new(text).style(Style::default().fg(color));
            frame.render_widget(para, area);
            return;
        }
        WizPhase::Aborted => {
            let para =
                Paragraph::new("Wizard aborted.").style(Style::default().fg(Color::DarkGray));
            frame.render_widget(para, area);
            return;
        }
        _ => {}
    }

    // Normal questions/review layout: header | tab bar | content | footer.
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Length(2), // Header
            Constraint::Length(2), // Tab bar
            Constraint::Min(5),    // Content
            Constraint::Min(2),    // Footer hints
        ])
        .split(area);

    draw_header(frame, chunks[0]);
    draw_tab_bar(frame, state, chunks[1]);
    if state.show_review {
        draw_review(frame, state, chunks[2]);
    } else {
        draw_question(frame, state, chunks[2]);
    }
    draw_footer(frame, state, chunks[3]);
}

fn draw_header(frame: &mut Frame, area: Rect) {
    let para = Paragraph::new("O7 Configuration Wizard")
        .style(Style::default().add_modifier(Modifier::BOLD));
    frame.render_widget(para, area);
}

fn draw_tab_bar(frame: &mut Frame, state: &WizState, area: Rect) {
    let mut spans: Vec<Span> = Vec::new();
    for (i, q) in state.questions.iter().enumerate() {
        let is_selected = !state.show_review && i == state.selected_tab;
        let has_answer = state
            .answers
            .get(&q.id)
            .map(|s| !s.is_empty())
            .unwrap_or(false);
        let color = if is_selected {
            Color::Cyan
        } else if has_answer {
            Color::Green
        } else {
            Color::DarkGray
        };
        let style = if is_selected {
            Style::default()
                .fg(color)
                .add_modifier(Modifier::BOLD | Modifier::REVERSED)
        } else {
            Style::default().fg(color)
        };
        spans.push(Span::styled(format!(" {} ", i + 1), style));
        spans.push(Span::raw("  "));
    }
    // Review tab
    let review_style = if state.show_review {
        Style::default()
            .fg(Color::Yellow)
            .add_modifier(Modifier::BOLD | Modifier::REVERSED)
    } else {
        Style::default().fg(Color::DarkGray)
    };
    spans.push(Span::styled(" Review ", review_style));

    let line = Line::from(spans);
    let para = Paragraph::new(line);
    frame.render_widget(para, area);
}

fn draw_question(frame: &mut Frame, state: &WizState, area: Rect) {
    let q = &state.questions[state.selected_tab];

    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Length(2), Constraint::Min(3)])
        .split(area);

    // Prompt
    let prompt =
        Paragraph::new(q.prompt.as_str()).style(Style::default().add_modifier(Modifier::BOLD));
    frame.render_widget(prompt, chunks[0]);

    // Text buffer with cursor
    let buffer = &state.text_buffer;
    let cursor = state.text_cursor;
    let content_line = if buffer.is_empty() {
        Line::from(vec![
            Span::raw("> "),
            Span::styled("Type your answer...", Style::default().fg(Color::DarkGray)),
        ])
    } else {
        let chars: Vec<char> = buffer.chars().collect();
        let before: String = chars[..cursor].iter().collect();
        let after: String = chars[cursor..].iter().collect();
        Line::from(vec![
            Span::raw("> "),
            Span::raw(before),
            Span::styled(
                "|",
                Style::default()
                    .fg(Color::Cyan)
                    .add_modifier(Modifier::BOLD),
            ),
            Span::raw(after),
        ])
    };
    let text_widget = Paragraph::new(content_line);
    frame.render_widget(text_widget, chunks[1]);
}

fn draw_review(frame: &mut Frame, state: &WizState, area: Rect) {
    let block = Block::default()
        .title(" Review your answers ")
        .borders(Borders::ALL)
        .border_style(Style::default().fg(Color::Cyan));

    let items: Vec<ListItem> = state
        .questions
        .iter()
        .map(|q| {
            let answer = state
                .answers
                .get(&q.id)
                .map(|s| s.as_str())
                .unwrap_or("---");
            let prompt_span = Span::styled(
                format!("{}: ", q.prompt),
                Style::default().fg(Color::DarkGray),
            );
            let answer_span = Span::styled(answer, Style::default().fg(Color::Green));
            ListItem::new(Line::from(vec![prompt_span, answer_span]))
        })
        .collect();

    let list = List::new(items).block(block);
    frame.render_widget(list, area);
}

fn draw_footer(frame: &mut Frame, state: &WizState, area: Rect) {
    let hints = if state.show_review {
        vec![
            ("Enter", "Save config"),
            ("S+Tab", "Back"),
            ("Esc", "Abort"),
        ]
    } else {
        vec![
            ("Tab", "Next"),
            ("S+Tab", "Prev"),
            ("Left/Right", "Cursor"),
            ("^W", "Delete word"),
            ("Enter", "Confirm"),
            ("Esc", "Abort"),
        ]
    };
    let para = Paragraph::new(render_hint_line(&hints)).wrap(Wrap { trim: false });
    frame.render_widget(para, area);
}

fn render_hint_line(hints: &[(&str, &str)]) -> Line<'static> {
    let mut spans = Vec::new();
    for (idx, (key, label)) in hints.iter().enumerate() {
        if idx > 0 {
            spans.push(Span::styled("  |  ", Style::default().fg(Color::DarkGray)));
        }
        spans.push(Span::styled(
            key.to_string(),
            Style::default()
                .fg(Color::Cyan)
                .add_modifier(Modifier::BOLD),
        ));
        spans.push(Span::raw(": "));
        spans.push(Span::styled(
            label.to_string(),
            Style::default().fg(Color::Gray),
        ));
    }
    Line::from(spans)
}