qqq-cli 0.12.0

Local-first task queue for coding agent sessions
use super::{archived_label, block, field, link, parent, status, status_color, styled};
use serde_json::Value;

fn heading(label: &str, color: bool) -> String {
    styled(&format!("{label}:"), color.then_some("1;36"))
}

fn row(label: &str, value: &str, color: bool) -> String {
    format!(
        "  {}{}  {}",
        styled(label, color.then_some("2")),
        " ".repeat(21_usize.saturating_sub(label.len())),
        styled(value, (color && value == "-").then_some("2"))
    )
}

fn session_with_name(task: &Value, session_key: &str, name_key: &str) -> String {
    let session = field(task, session_key);
    if task[name_key].as_str().is_some() {
        format!("{session} ({})", field(task, name_key))
    } else {
        session
    }
}

fn message(value: &Value, color: bool) -> String {
    let body = block(value, "body")
        .lines()
        .map(|line| format!("  {line}"))
        .collect::<Vec<_>>()
        .join("\n");
    format!(
        "  {} · {} · {}\n{body}",
        styled(&format!("#{}", field(value, "id")), color.then_some("1")),
        field(value, "session"),
        styled(&field(value, "created_at"), color.then_some("2"))
    )
}

fn image(value: &Value, color: bool) -> String {
    format!(
        "  {}: {} {}",
        styled(
            &format!("Image #{}", field(value, "id")),
            color.then_some("1")
        ),
        field(value, "name"),
        styled(
            &format!(
                "({}, {} bytes)",
                field(value, "media_type"),
                field(value, "bytes")
            ),
            color.then_some("2")
        )
    )
}

fn event(value: &Value, color: bool) -> String {
    let action = field(value, "action");
    let code = if color {
        match action.as_str() {
            "claim" => Some("36"),
            "release" => Some("33"),
            "complete" => Some("90"),
            "error" => Some("31"),
            _ => None,
        }
    } else {
        None
    };
    format!(
        "  {}  {}  {}",
        styled(&field(value, "created_at"), color.then_some("2")),
        styled(&format!("{action:<8}"), code),
        field(value, "session")
    )
}

fn collection(label: &str, rows: &[Value], color: bool) -> String {
    let title = heading(label, color);
    if rows.is_empty() {
        return format!("{title} {}", styled("None", color.then_some("2")));
    }
    let body = rows
        .iter()
        .map(|value| match label {
            "Messages" => message(value, color),
            "Images" => image(value, color),
            _ => event(value, color),
        })
        .collect::<Vec<_>>()
        .join(if label == "Messages" { "\n\n" } else { "\n" });
    format!(
        "{title} {}\n{body}",
        styled(&format!("({})", rows.len()), color.then_some("2"))
    )
}

pub(super) fn render(value: &Value, color: bool) -> String {
    let task = &value["task"];
    let mut sections = vec![format!(
        "{} · {}",
        styled(&format!("#{}", field(task, "id")), color.then_some("1")),
        styled(status(task), status_color(task, color))
    )];
    if task["description"]
        .as_str()
        .is_some_and(|text| !text.is_empty())
    {
        sections.push(format!(
            "{}\n{}",
            heading("Description", color),
            block(task, "description")
        ));
    }
    sections.push(collection(
        "Messages",
        value["messages"]
            .as_array()
            .expect("detail section is an array"),
        color,
    ));
    if let Some(prerequisites) = task["prerequisites"]
        .as_array()
        .filter(|items| !items.is_empty())
    {
        let rows = prerequisites
            .iter()
            .map(|prerequisite| {
                let state = prerequisite["status"].as_str().unwrap_or("missing");
                let archived = if prerequisite["archived"].as_bool() == Some(true) {
                    " · archived"
                } else {
                    ""
                };
                let blocked = if state == "completed" {
                    ""
                } else {
                    " · blocks claim"
                };
                format!(
                    "  #{} · {state}{archived}{blocked}",
                    field(prerequisite, "id")
                )
            })
            .collect::<Vec<_>>()
            .join("\n");
        sections.push(format!("{}\n{rows}", heading("Prerequisites", color)));
    }
    sections.push(format!(
        "{}\n{}\n{}\n{}\n{}\n{}",
        heading("Details", color),
        row("Parent:", &parent(task), color),
        row("Priority:", &field(task, "priority"), color),
        row("Archived:", archived_label(task), color),
        row(
            "Created:",
            &styled(&field(task, "created_at"), color.then_some("2")),
            color
        ),
        row(
            "Updated:",
            &styled(&field(task, "updated_at"), color.then_some("2")),
            color
        )
    ));
    let tags = crate::tags::prefix(task);
    if !tags.is_empty() {
        sections.push(format!("{}\n{}", heading("Tags", color), tags));
    }
    let assignment = [
        row(
            "Harness:",
            &session_with_name(task, "harness_session", "harness_name"),
            color,
        ),
        row(
            "Orchestrator:",
            &session_with_name(task, "orchestrator_session", "orchestrator_name"),
            color,
        ),
    ]
    .join("\n");
    sections.push(format!("{}\n{assignment}", heading("Assignment", color)));
    for (key, label) in [("images", "Images"), ("events", "History")] {
        sections.push(collection(
            label,
            value[key].as_array().expect("detail section is an array"),
            color,
        ));
    }
    let saved_link = link(&value["herdr"], false);
    sections.push(match saved_link.split_once('\n') {
        Some((_, body)) => format!(
            "{}\n{}",
            heading("Herdr", color),
            body.lines()
                .map(|line| format!("  {line}"))
                .collect::<Vec<_>>()
                .join("\n")
        ),
        None => format!(
            "{} {}",
            heading("Herdr", color),
            styled("Not linked", color.then_some("2"))
        ),
    });
    if let Some(export) = value.get("export") {
        sections.push(format!(
            "{} image #{} to {}",
            styled("Exported", color.then_some("32")),
            field(export, "id"),
            field(export, "path")
        ));
    }
    sections.join("\n\n")
}