//! Renders doctor reports for terminal users.
//!
//! The renderer is intentionally separate from check construction so the JSON
//! report can stay stable while the human view optimizes for scanability. It
//! groups checks by concern, colors only status/actionable tokens, and redacts
//! sensitive detail lines before showing them in detailed output.

mod detail;

use std::fmt::Write as _;

use detail::HumanDetail;
use detail::detail_lines;
use owo_colors::OwoColorize;
use owo_colors::XtermColors;

use super::CheckStatus;
use super::DoctorCheck;
use super::DoctorReport;

const NAME_WIDTH: usize = 12;
const DETAIL_LABEL_WIDTH: usize = 24;
const SEPARATOR_WIDTH: usize = 61;

const GROUPS: &[OutputGroup] = &[
    OutputGroup {
        title: "Environment",
        keys: &[
            "system", "runtime", "install", "search", "git", "terminal", "title", "state",
            "threads",
        ],
    },
    OutputGroup {
        title: "Configuration",
        keys: &["config", "auth", "mcp", "sandbox"],
    },
    OutputGroup {
        title: "Updates",
        keys: &["updates"],
    },
    OutputGroup {
        title: "Connectivity",
        keys: &["network", "websocket", "reachability"],
    },
    OutputGroup {
        title: "Background Server",
        keys: &["app-server"],
    },
];

struct OutputGroup {
    title: &'static str,
    keys: &'static [&'static str],
}

/// Rendering controls for human doctor output.
///
/// These options affect presentation only. They must not change which checks
/// run or which fields are present in the underlying JSON report.
#[derive(Clone, Copy, Debug)]
pub(super) struct HumanOutputOptions {
    pub(super) show_details: bool,
    pub(super) show_all: bool,
    pub(super) ascii: bool,
    pub(super) color_enabled: bool,
}

/// Formats a doctor report into the grouped terminal layout.
///
/// The renderer expects checks to carry stable categories, but it owns their
/// display order. Adding a new category without adding it to GROUPS keeps JSON
/// output intact but hides that row from the human view.
pub(super) fn render_human_report(report: &DoctorReport, options: HumanOutputOptions) -> String {
    let mut out = String::new();
    let _ = writeln!(
        out,
        "{} {}",
        bold("Codex Doctor", options),
        dim(&header_suffix(report), options)
    );
    out.push('\n');

    let notes = notes_for_report(report);
    if !notes.is_empty() {
        let _ = writeln!(out, "{}", bold("Notes", options));
        for note in &notes {
            write_note_row(&mut out, note, options);
        }
        let _ = writeln!(out, "{}", dim(&separator(options), options));
        out.push('\n');
    }

    let mut wrote_group = false;
    for group in GROUPS {
        let group_checks = checks_for_group(report, group);
        if group_checks.is_empty() {
            continue;
        }

        if wrote_group {
            out.push('\n');
        }
        wrote_group = true;

        let _ = writeln!(out, "{}", bold(group.title, options));
        for check in group_checks {
            write_check_row(&mut out, check, options);
        }
    }

    out.push('\n');
    let _ = writeln!(out, "{}", dim(&separator(options), options));
    let _ = writeln!(out, "{}", summary_line(report, options));
    out.push('\n');
    write_footer(&mut out, options);
    out
}

fn checks_for_group<'a>(report: &'a DoctorReport, group: &OutputGroup) -> Vec<&'a DoctorCheck> {
    group
        .keys
        .iter()
        .flat_map(|key| {
            report
                .checks
                .iter()
                .filter(move |check| check.category == *key)
        })
        .collect()
}

fn write_check_row(out: &mut String, check: &DoctorCheck, options: HumanOutputOptions) {
    let description = row_description(check, options);
    let status = display_status(check);
    let _ = writeln!(
        out,
        "  {}{} {}",
        status_marker_slot(status, options),
        format_args!("{:<NAME_WIDTH$}", check.category),
        style_description(&description, status, options)
    );

    if options.show_details {
        for detail in detail_lines(check, options) {
            write_detail_line(out, detail, options);
        }
    }
}

fn write_note_row(out: &mut String, note: &DoctorNote, options: HumanOutputOptions) {
    let _ = writeln!(
        out,
        "   {}{} {}",
        status_marker_slot(note.status, options),
        format_args!("{:<NAME_WIDTH$}", note.name),
        style_note_summary(note, options)
    );
}

fn write_detail_line(out: &mut String, detail: HumanDetail, options: HumanOutputOptions) {
    match detail {
        HumanDetail::Row {
            label,
            value,
            expected,
        } => {
            let is_issue = expected.is_some();
            let label = format!("{label:<DETAIL_LABEL_WIDTH$}");
            let value = if let Some(expected) = expected {
                format!(
                    "{} {}",
                    detail_value(&value, options),
                    dim(&format!("(expected {expected})"), options)
                )
            } else {
                detail_value(&value, options)
            };
            let _ = writeln!(
                out,
                "    {} {} {}",
                detail_marker(is_issue, options),
                detail_label(&label, options),
                value
            );
        }
        HumanDetail::Continuation(value) => {
            let spacer = " ".repeat(DETAIL_LABEL_WIDTH);
            let _ = writeln!(
                out,
                "      {} {}",
                detail_label(&spacer, options),
                detail_value(&value, options)
            );
        }
        HumanDetail::Bullet(value) => {
            let _ = writeln!(
                out,
                "    {} {}",
                very_dim(if options.ascii { "-" } else { "·" }, options),
                dim(&highlight_actions(&value, options), options)
            );
        }
        HumanDetail::Remedy(value) => {
            let marker = if options.ascii { "->" } else { "→" };
            let _ = writeln!(
                out,
                "    {} {}",
                orange(marker, options),
                highlight_actions(&value, options)
            );
        }
    }
}

fn row_description(check: &DoctorCheck, options: HumanOutputOptions) -> String {
    if matches!(check.status, CheckStatus::Warning | CheckStatus::Fail) && !check.issues.is_empty()
    {
        return issue_summary(check);
    }
    if matches!(check.status, CheckStatus::Warning | CheckStatus::Fail)
        && let Some(remediation) = &check.remediation
    {
        let dash = if options.ascii { " - " } else { " — " };
        let summary = &check.summary;
        return format!("{summary}{dash}{remediation}");
    }

    display_summary(check, options)
}

fn issue_summary(check: &DoctorCheck) -> String {
    match check.issues.as_slice() {
        [] => check.summary.clone(),
        [issue] => issue.cause.clone(),
        issues => format!(
            "{} issues - {}",
            issues.len(),
            issues
                .iter()
                .take(2)
                .map(|issue| issue.cause.as_str())
                .collect::<Vec<_>>()
                .join("; ")
        ),
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum DisplayStatus {
    Ok,
    Update,
    Note,
    Warning,
    Fail,
    Idle,
}

struct DoctorNote {
    status: DisplayStatus,
    name: String,
    summary: String,
}

fn display_status(check: &DoctorCheck) -> DisplayStatus {
    if check.category == "app-server"
        && check.status == CheckStatus::Ok
        && check
            .details
            .iter()
            .any(|detail| detail == "status: not running")
    {
        return DisplayStatus::Idle;
    }

    match check.status {
        CheckStatus::Ok => DisplayStatus::Ok,
        CheckStatus::Warning => DisplayStatus::Warning,
        CheckStatus::Fail => DisplayStatus::Fail,
    }
}

fn status_marker(status: DisplayStatus, options: HumanOutputOptions) -> String {
    let marker = if options.ascii {
        match status {
            DisplayStatus::Ok => "[ok]",
            DisplayStatus::Update => "[up]",
            DisplayStatus::Note | DisplayStatus::Warning => "[!!]",
            DisplayStatus::Fail => "[XX]",
            DisplayStatus::Idle => "[--]",
        }
    } else {
        match status {
            DisplayStatus::Ok => "✓",
            DisplayStatus::Update => "↑",
            DisplayStatus::Note | DisplayStatus::Warning => "⚠",
            DisplayStatus::Fail => "✗",
            DisplayStatus::Idle => "○",
        }
    };

    match status {
        DisplayStatus::Ok => green(marker, options),
        DisplayStatus::Update => amber(marker, options),
        DisplayStatus::Note | DisplayStatus::Warning => orange(marker, options),
        DisplayStatus::Fail => red(marker, options),
        DisplayStatus::Idle => dim(marker, options),
    }
}

fn status_marker_slot(status: DisplayStatus, options: HumanOutputOptions) -> String {
    let marker = status_marker(status, options);
    format!("{marker} ")
}

fn style_description(
    description: &str,
    status: DisplayStatus,
    options: HumanOutputOptions,
) -> String {
    let highlighted = highlight_actions(description, options);
    match status {
        DisplayStatus::Ok | DisplayStatus::Idle => dim(&highlighted, options),
        DisplayStatus::Update => amber(&highlighted, options),
        DisplayStatus::Note | DisplayStatus::Warning | DisplayStatus::Fail => highlighted,
    }
}

fn detail_marker(is_issue: bool, options: HumanOutputOptions) -> String {
    if !is_issue {
        return " ".to_string();
    }
    orange(if options.ascii { ">" } else { "▸" }, options)
}

fn style_note_summary(note: &DoctorNote, options: HumanOutputOptions) -> String {
    if note.status == DisplayStatus::Update {
        return style_update_note_summary(&note.summary, options);
    }
    style_description(&note.summary, note.status, options)
}

fn style_update_note_summary(summary: &str, options: HumanOutputOptions) -> String {
    if !options.color_enabled {
        return summary.to_string();
    }

    let Some((version, rest)) = summary.split_once(" available") else {
        return amber(summary, options);
    };
    let Some((action, parenthetical)) = rest.split_once(" (") else {
        return format!(
            "{}{}",
            amber(&format!("{version} available"), options),
            amber(rest, options)
        );
    };
    format!(
        "{}{} {}",
        amber(&format!("{version} available"), options),
        amber(action, options),
        dim(&format!("({parenthetical}"), options)
    )
}

fn summary_line(report: &DoctorReport, options: HumanOutputOptions) -> String {
    let notes = notes_for_report(report);
    let counts = StatusCounts::from_report(report, notes.len());
    let separator = dim(if options.ascii { " | " } else { " · " }, options);
    let status = overall_status_label(report.overall_status);
    let mut parts = vec![count_label(counts.ok, "ok", DisplayStatus::Ok, options)];
    if counts.idle > 0 {
        parts.push(count_label(
            counts.idle,
            "idle",
            DisplayStatus::Idle,
            options,
        ));
    }
    if counts.notes > 0 {
        parts.push(count_label(
            counts.notes,
            "notes",
            DisplayStatus::Note,
            options,
        ));
    }
    parts.push(count_label(
        counts.warning,
        "warn",
        DisplayStatus::Warning,
        options,
    ));
    parts.push(count_label(
        counts.fail,
        "fail",
        DisplayStatus::Fail,
        options,
    ));
    format!(
        "{} {}",
        parts.join(&separator),
        styled_overall_status(status, report.overall_status, options)
    )
}

fn count_label(
    count: usize,
    label: &str,
    status: DisplayStatus,
    options: HumanOutputOptions,
) -> String {
    let count = dim(&count.to_string(), options);
    let label = match status {
        DisplayStatus::Ok => green(label, options),
        DisplayStatus::Update => amber(label, options),
        DisplayStatus::Note | DisplayStatus::Warning => orange(label, options),
        DisplayStatus::Fail => red(label, options),
        DisplayStatus::Idle => dim(label, options),
    };
    format!("{count} {label}")
}

fn overall_status_label(status: CheckStatus) -> &'static str {
    match status {
        CheckStatus::Ok => "ok",
        CheckStatus::Warning => "degraded",
        CheckStatus::Fail => "failed",
    }
}

fn styled_overall_status(label: &str, status: CheckStatus, options: HumanOutputOptions) -> String {
    if !options.color_enabled {
        return label.to_string();
    }

    match status {
        CheckStatus::Ok => label.green().bold().to_string(),
        CheckStatus::Warning => label.yellow().bold().to_string(),
        CheckStatus::Fail => label.red().bold().to_string(),
    }
}

fn write_footer(out: &mut String, options: HumanOutputOptions) {
    if options.show_details {
        let _ = writeln!(
            out,
            "{} {:<24} {} {}",
            cyan("--summary", options),
            dim("compact output", options),
            cyan("--all", options),
            dim("expand truncated lists", options)
        );
    } else {
        let _ = writeln!(
            out,
            "{}",
            dim(
                "Run codex doctor without --summary for detailed diagnostics.",
                options
            )
        );
        let _ = writeln!(
            out,
            "{} {:<28} {} {}",
            cyan("--all", options),
            dim("expand truncated lists", options),
            cyan("--json", options),
            dim("redacted report", options)
        );
        return;
    }
    let _ = writeln!(
        out,
        "{} {}",
        cyan("--json", options),
        dim("redacted report", options)
    );
}

fn header_suffix(report: &DoctorReport) -> String {
    let version = format!("v{}", report.codex_version);
    report
        .checks
        .iter()
        .find(|check| check.category == "runtime")
        .and_then(|check| detail::detail_value(check, "platform"))
        .map_or(version.clone(), |platform| {
            format!("{version} · {platform}")
        })
}

fn notes_for_report(report: &DoctorReport) -> Vec<DoctorNote> {
    let mut notes = Vec::new();
    if let Some(check) = find_check(report, "updates") {
        update_note(check, report)
            .into_iter()
            .for_each(|note| notes.push(note));
    }
    if let Some(check) = find_check(report, "state") {
        rollout_note(check)
            .into_iter()
            .for_each(|note| notes.push(note));
    }
    if let Some(check) = find_check(report, "sandbox") {
        sandbox_note(check)
            .into_iter()
            .for_each(|note| notes.push(note));
    }
    non_ok_notes(report)
        .into_iter()
        .for_each(|note| notes.push(note));
    auth_reachability_note(report)
        .into_iter()
        .for_each(|note| notes.push(note));
    notes
}

fn find_check<'a>(report: &'a DoctorReport, category: &str) -> Option<&'a DoctorCheck> {
    report
        .checks
        .iter()
        .find(|check| check.category == category)
}

fn update_note(check: &DoctorCheck, report: &DoctorReport) -> Option<DoctorNote> {
    let status = detail::detail_value(check, "latest version status")?;
    if !status.contains("newer version is available") {
        return None;
    }
    let latest = detail::detail_value(check, "latest version")
        .or_else(|| detail::detail_value(check, "cached latest version"))
        .unwrap_or_else(|| "newer version".to_string());
    let dismissed = detail::detail_value(check, "dismissed version");
    let mut parenthetical = format!("current {}", report.codex_version);
    if let Some(dismissed) = dismissed
        && !detail::is_falsy(&dismissed)
    {
        parenthetical.push_str(&format!(", dismissed {dismissed}"));
    }
    Some(DoctorNote {
        status: DisplayStatus::Update,
        name: "updates".to_string(),
        summary: format!("{latest} available ({parenthetical})"),
    })
}

fn rollout_note(check: &DoctorCheck) -> Option<DoctorNote> {
    let active = detail::detail_value(check, "active rollout files")?;
    let (files, bytes) = detail::rollout_files_and_bytes(&active)?;
    if files < 1000 && bytes < 1024 * 1024 * 1024 {
        return None;
    }
    Some(DoctorNote {
        status: DisplayStatus::Warning,
        name: "rollouts".to_string(),
        summary: format!(
            "{} active files · {} on disk",
            detail::format_count(files),
            detail::format_bytes(bytes)
        ),
    })
}

fn sandbox_note(check: &DoctorCheck) -> Option<DoctorNote> {
    let filesystem = detail::detail_value(check, "filesystem sandbox")?;
    let network = detail::detail_value(check, "network sandbox")?;
    if filesystem == "restricted" && network == "restricted" {
        return None;
    }
    Some(DoctorNote {
        status: DisplayStatus::Warning,
        name: "sandbox".to_string(),
        summary: format!("filesystem {filesystem} · network {network}"),
    })
}

fn non_ok_notes(report: &DoctorReport) -> Vec<DoctorNote> {
    report
        .checks
        .iter()
        .filter(|check| matches!(check.status, CheckStatus::Warning | CheckStatus::Fail))
        .map(|check| DoctorNote {
            status: display_status(check),
            name: check.category.clone(),
            summary: actionable_note_summary(check),
        })
        .collect()
}

fn actionable_note_summary(check: &DoctorCheck) -> String {
    if !check.issues.is_empty() {
        return issue_summary(check);
    }
    if let Some(remediation) = &check.remediation {
        return format!("{} - {remediation}", check.summary);
    }
    check.summary.clone()
}

fn auth_reachability_note(report: &DoctorReport) -> Option<DoctorNote> {
    let websocket = find_check(report, "websocket")?;
    let reachability = find_check(report, "reachability")?;
    let auth_mode = detail::detail_value(websocket, "auth mode")?;
    let reachability_mode = detail::detail_value(reachability, "reachability mode")?;
    let auth_mode_lower = auth_mode.to_ascii_lowercase();
    let reachability_mode_lower = reachability_mode.to_ascii_lowercase();
    if auth_mode_lower.contains("chatgpt") && reachability_mode_lower.contains("api key") {
        return Some(DoctorNote {
            status: DisplayStatus::Warning,
            name: "auth".to_string(),
            summary: "mixed auth signals: ChatGPT login plus API key env var; HTTP reachability uses API-key mode".to_string(),
        });
    }
    None
}

fn display_summary(check: &DoctorCheck, options: HumanOutputOptions) -> String {
    match check.category.as_str() {
        "system" => system_summary(check),
        "runtime" => runtime_summary(check),
        "install" if check.status == CheckStatus::Ok => "consistent".to_string(),
        "search" => search_summary(check),
        "git" => git_summary(check),
        "terminal" => terminal_summary(check),
        "title" => title_summary(check, options),
        "state" => state_summary(check),
        "config" if check.status == CheckStatus::Ok => "loaded".to_string(),
        "mcp" => mcp_summary(check),
        "sandbox" => sandbox_summary(check),
        "network" => network_summary(check),
        "websocket" => websocket_summary(check),
        "app-server" => app_server_summary(check),
        _ => check.summary.clone(),
    }
}

fn system_summary(check: &DoctorCheck) -> String {
    detail::detail_value(check, "os language").unwrap_or_else(|| check.summary.clone())
}

fn runtime_summary(check: &DoctorCheck) -> String {
    if detail::detail_value(check, "current executable")
        .is_some_and(|path| path.contains("/target/debug/"))
    {
        return "local debug build".to_string();
    }
    detail::detail_value(check, "install method").unwrap_or_else(|| check.summary.clone())
}

fn search_summary(check: &DoctorCheck) -> String {
    let provider = detail::detail_value(check, "search provider");
    let command = detail::detail_value(check, "search command");
    let readiness = detail::detail_value(check, "search command readiness");
    match (readiness, provider, command) {
        (Some(readiness), Some(provider), Some(command)) if check.status == CheckStatus::Ok => {
            format!("{readiness} ({provider}, `{command}`)")
        }
        _ => check.summary.clone(),
    }
}

fn git_summary(check: &DoctorCheck) -> String {
    detail::detail_value(check, "git version")
        .or_else(|| detail::detail_value(check, "selected git"))
        .unwrap_or_else(|| check.summary.clone())
}

fn terminal_summary(check: &DoctorCheck) -> String {
    let mut parts = Vec::new();
    if let Some(terminal) = detail::detail_value(check, "terminal") {
        let version = detail::detail_value(check, "terminal version");
        parts.push(version.map_or(terminal.clone(), |version| format!("{terminal} {version}")));
    }
    if let Some(multiplexer) = detail::detail_value(check, "multiplexer") {
        parts.push(multiplexer);
    }
    if let Some(term) = detail::detail_value(check, "TERM") {
        parts.push(format!("TERM={term}"));
    }
    if parts.is_empty() {
        check.summary.clone()
    } else {
        parts.join(" · ")
    }
}

fn title_summary(check: &DoctorCheck, options: HumanOutputOptions) -> String {
    let source = detail::detail_value(check, "terminal title source");
    let project = detail::detail_value(check, "terminal title project value");
    match (source, project) {
        (Some(source), Some(project)) => {
            let separator = if options.ascii { " | " } else { " · " };
            format!("{source}{separator}project {project}")
        }
        (Some(source), None) => source,
        _ => check.summary.clone(),
    }
}

fn state_summary(check: &DoctorCheck) -> String {
    let databases_ok = [
        "state DB integrity",
        "log DB integrity",
        "goals DB integrity",
        "memories DB integrity",
    ]
    .into_iter()
    .all(|label| detail::detail_value(check, label).is_some_and(|value| value == "ok"));
    if check.status == CheckStatus::Ok && databases_ok {
        "databases healthy".to_string()
    } else {
        check.summary.clone()
    }
}

fn mcp_summary(check: &DoctorCheck) -> String {
    let Some(count) = detail::detail_value(check, "configured servers") else {
        return check.summary.clone();
    };
    let disabled =
        detail::detail_value(check, "disabled servers").unwrap_or_else(|| "0".to_string());
    let transports = check
        .details
        .iter()
        .filter_map(|detail| detail.split_once(" servers: "))
        .filter(|(transport, _)| *transport != "configured" && *transport != "disabled")
        .map(|(transport, count)| format!("{count} {transport}"))
        .collect::<Vec<_>>();
    if transports.is_empty() {
        format!("{count} servers · {disabled} disabled")
    } else {
        format!(
            "{} server ({}) · {} disabled",
            count,
            transports.join(", "),
            disabled
        )
    }
}

fn sandbox_summary(check: &DoctorCheck) -> String {
    let approval = detail::detail_value(check, "approval policy");
    let filesystem = detail::detail_value(check, "filesystem sandbox");
    let network = detail::detail_value(check, "network sandbox");
    match (approval, filesystem, network) {
        (Some(approval), Some(filesystem), Some(network)) => {
            format!("{filesystem} fs + {network} network · approval {approval}")
        }
        _ => check.summary.clone(),
    }
}

fn network_summary(check: &DoctorCheck) -> String {
    detail::detail_value(check, "proxy env vars")
        .map(|value| {
            if value == "none" {
                "no proxy env vars".to_string()
            } else {
                "proxy env vars present".to_string()
            }
        })
        .unwrap_or_else(|| check.summary.clone())
}

fn websocket_summary(check: &DoctorCheck) -> String {
    let status = detail::detail_value(check, "handshake result")
        .or_else(|| detail::detail_value(check, "handshake status"));
    let timeout = detail::detail_value(check, "connect timeout")
        .map(|value| value.replace("000 ms", "s").replace(" ms", "ms"));
    match (status, timeout) {
        (Some(status), Some(timeout)) => format!("connected ({status}) · {timeout} timeout"),
        _ => check.summary.clone(),
    }
}

fn app_server_summary(check: &DoctorCheck) -> String {
    let status = detail::detail_value(check, "status");
    let mode = detail::detail_value(check, "mode");
    match (status, mode) {
        (Some(status), Some(mode)) => format!("{status} ({mode} mode)"),
        _ => check.summary.clone(),
    }
}

fn separator(options: HumanOutputOptions) -> String {
    if options.ascii {
        "-".repeat(SEPARATOR_WIDTH)
    } else {
        "─".repeat(SEPARATOR_WIDTH)
    }
}

fn highlight_actions(text: &str, options: HumanOutputOptions) -> String {
    if !options.color_enabled {
        return text.to_string();
    }

    let mut out = String::new();
    let mut parts = text.split('`');
    if let Some(first) = parts.next() {
        out.push_str(&highlight_flags(first, options));
    }
    let mut in_code = true;
    for part in parts {
        if in_code {
            out.push_str(&cyan(part, options));
        } else {
            out.push_str(&highlight_flags(part, options));
        }
        in_code = !in_code;
    }
    out
}

fn highlight_flags(text: &str, options: HumanOutputOptions) -> String {
    text.split_inclusive(char::is_whitespace)
        .map(|token| {
            let trimmed = token.trim_end();
            let suffix = &token[trimmed.len()..];
            let bare = trimmed.trim_end_matches([',', '.', ':', ';', ')']);
            let punctuation = &trimmed[bare.len()..];
            if bare.starts_with("--") {
                let highlighted = cyan(bare, options);
                format!("{highlighted}{punctuation}{suffix}")
            } else {
                token.to_string()
            }
        })
        .collect()
}

pub(super) fn redact_detail(detail: &str) -> String {
    let lower = detail.to_ascii_lowercase();
    let label = lower.split(':').next().unwrap_or_default();
    if label.contains("env var") {
        return redact_urls(detail);
    }
    if detail
        .split_once(": ")
        .is_some_and(|(_, value)| is_safe_presence_value(value))
    {
        return redact_urls(detail);
    }

    let secret_keys = [
        "openai_api_key",
        "codex_api_key",
        "codex_access_token",
        "authorization",
        "bearer_token",
        "token",
        "secret",
    ];
    if secret_keys.iter().any(|key| lower.contains(key)) {
        let name = detail.split(':').next().unwrap_or(detail);
        format!("{name}: <redacted>")
    } else {
        redact_urls(detail)
    }
}

fn is_safe_presence_value(value: &str) -> bool {
    matches!(
        value.trim().to_ascii_lowercase().as_str(),
        "true" | "false" | "yes" | "no" | "present" | "absent" | "missing" | "not set"
    )
}

fn redact_urls(detail: &str) -> String {
    detail
        .split_inclusive(char::is_whitespace)
        .map(redact_url_token)
        .collect()
}

fn redact_url_token(token: &str) -> String {
    let Some(scheme_end) = token.find("://") else {
        return token.to_string();
    };
    let mut suffix_start = token.len();
    while suffix_start > scheme_end + 3
        && matches!(
            token.as_bytes()[suffix_start - 1],
            b' ' | b'\t' | b'\n' | b'\r' | b'.' | b',' | b';' | b':' | b')' | b']'
        )
    {
        suffix_start -= 1;
    }

    let (body, suffix) = token.split_at(suffix_start);
    let scheme_prefix_end = scheme_end + 3;
    let rest = &body[scheme_prefix_end..];
    let authority_end = rest
        .find(['/', '?', '#'])
        .map(|index| scheme_prefix_end + index)
        .unwrap_or(body.len());
    let authority = &body[scheme_prefix_end..authority_end];
    let authority = authority
        .rsplit_once('@')
        .map_or(authority, |(_, host)| host);
    let path = &body[authority_end..];
    let path = path
        .find(['?', '#'])
        .map(|index| &path[..index])
        .unwrap_or(path);
    let path = redact_url_path(path);
    format!(
        "{}{}{}{}",
        &body[..scheme_prefix_end],
        authority,
        path,
        suffix
    )
}

fn redact_url_path(path: &str) -> String {
    let mut segments = path.split('/').filter(|segment| !segment.is_empty());
    let Some(first_segment) = segments.next() else {
        return path.to_string();
    };
    if segments.next().is_some() {
        format!("/{first_segment}/<redacted>")
    } else {
        path.to_string()
    }
}

#[derive(Default)]
struct StatusCounts {
    ok: usize,
    idle: usize,
    notes: usize,
    warning: usize,
    fail: usize,
}

impl StatusCounts {
    fn from_report(report: &DoctorReport, notes: usize) -> Self {
        let mut counts = Self {
            notes,
            ..Self::default()
        };
        for check in &report.checks {
            match display_status(check) {
                DisplayStatus::Ok => counts.ok += 1,
                DisplayStatus::Idle => counts.idle += 1,
                DisplayStatus::Warning => counts.warning += 1,
                DisplayStatus::Fail => counts.fail += 1,
                DisplayStatus::Update | DisplayStatus::Note => {}
            }
        }
        counts
    }
}

fn bold(text: &str, options: HumanOutputOptions) -> String {
    if options.color_enabled {
        text.bold().to_string()
    } else {
        text.to_string()
    }
}

fn dim(text: &str, options: HumanOutputOptions) -> String {
    if options.color_enabled {
        text.dimmed().to_string()
    } else {
        text.to_string()
    }
}

fn very_dim(text: &str, options: HumanOutputOptions) -> String {
    color256(text, /*code*/ 238, options)
}

fn detail_label(text: &str, options: HumanOutputOptions) -> String {
    color256(text, /*code*/ 240, options)
}

fn detail_value(text: &str, options: HumanOutputOptions) -> String {
    if !options.color_enabled {
        return text.to_string();
    }
    style_detail_text(text, options)
}

fn style_detail_text(text: &str, options: HumanOutputOptions) -> String {
    let mut out = String::new();
    let mut parts = text.split('`');
    if let Some(first) = parts.next() {
        out.push_str(&style_detail_plain_text(first, options));
    }
    let mut in_code = true;
    for part in parts {
        if in_code {
            out.push_str(&cyan(part, options));
        } else {
            out.push_str(&style_detail_plain_text(part, options));
        }
        in_code = !in_code;
    }
    out
}

fn style_detail_plain_text(text: &str, options: HumanOutputOptions) -> String {
    text.split_inclusive(char::is_whitespace)
        .map(|token| style_detail_token(token, options))
        .collect()
}

fn style_detail_token(token: &str, options: HumanOutputOptions) -> String {
    let trimmed = token.trim_end();
    let suffix = &token[trimmed.len()..];
    let bare = trimmed.trim_end_matches([',', '.', ':', ';', ')']);
    let punctuation = &trimmed[bare.len()..];
    let styled = style_detail_bare_token(bare, options);
    format!("{styled}{punctuation}{suffix}")
}

fn style_detail_bare_token(bare: &str, options: HumanOutputOptions) -> String {
    if bare.is_empty() {
        return String::new();
    }
    if bare == "<redacted>" {
        return color256(&bare.italic().to_string(), /*code*/ 244, options);
    }
    if bare.contains("(missing)") || detail::is_falsy(bare) {
        return color256(bare, /*code*/ 240, options);
    }
    if let Some((label, value)) = bare.split_once(':')
        && detail::is_falsy(value)
    {
        return format!("{label}:{}", color256(value, /*code*/ 240, options));
    }
    if bare == "ok" {
        return green(bare, options);
    }
    if bare.starts_with("--") || looks_copyable(bare) {
        return cyan(bare, options);
    }
    if matches!(bare, "B" | "KB" | "MB" | "GB" | "TB" | "files" | "file") {
        return dim(bare, options);
    }
    bare.to_string()
}

fn green(text: &str, options: HumanOutputOptions) -> String {
    color256(text, /*code*/ 10, options)
}

fn amber(text: &str, options: HumanOutputOptions) -> String {
    color256(text, /*code*/ 220, options)
}

fn orange(text: &str, options: HumanOutputOptions) -> String {
    color256(text, /*code*/ 214, options)
}

fn red(text: &str, options: HumanOutputOptions) -> String {
    color256(text, /*code*/ 196, options)
}

fn cyan(text: &str, options: HumanOutputOptions) -> String {
    color256(text, /*code*/ 117, options)
}

fn color256(text: &str, code: u8, options: HumanOutputOptions) -> String {
    if options.color_enabled {
        text.color(XtermColors::from(code)).to_string()
    } else {
        text.to_string()
    }
}

fn looks_copyable(text: &str) -> bool {
    text.starts_with("http://")
        || text.starts_with("https://")
        || text.starts_with("wss://")
        || text.starts_with("~/")
        || text.starts_with('/')
        || text.starts_with("./")
        || text.starts_with("../")
}

#[cfg(test)]
mod tests {
    use pretty_assertions::assert_eq;

    use super::*;

    fn detailed_no_color_unicode_options() -> HumanOutputOptions {
        HumanOutputOptions {
            show_details: true,
            show_all: false,
            ascii: false,
            color_enabled: false,
        }
    }

    fn summary_no_color_unicode_options() -> HumanOutputOptions {
        HumanOutputOptions {
            show_details: false,
            show_all: false,
            ascii: false,
            color_enabled: false,
        }
    }

    fn detailed_all_no_color_unicode_options() -> HumanOutputOptions {
        HumanOutputOptions {
            show_details: true,
            show_all: true,
            ascii: false,
            color_enabled: false,
        }
    }

    fn detailed_color_unicode_options() -> HumanOutputOptions {
        HumanOutputOptions {
            show_details: true,
            show_all: false,
            ascii: false,
            color_enabled: true,
        }
    }

    fn sample_report() -> DoctorReport {
        let checks = vec![
            DoctorCheck::new(
                "system.environment",
                "system",
                CheckStatus::Ok,
                "OS language en-US",
            )
            .detail("os: macOS 15.0")
            .detail("os language: en-US")
            .detail("VISUAL: code --wait")
            .detail("EDITOR: vim")
            .detail("PAGER: less -R")
            .detail("GIT_PAGER: delta")
            .detail("GH_PAGER: less")
            .detail("LESS: -FRX"),
            DoctorCheck::new(
                "runtime.provenance",
                "runtime",
                CheckStatus::Ok,
                "running local build on darwin-arm64",
            ),
            DoctorCheck::new(
                "installation",
                "install",
                CheckStatus::Ok,
                "installation looks consistent",
            ),
            DoctorCheck::new(
                "runtime.search",
                "search",
                CheckStatus::Ok,
                "search is OK (bundled)",
            ),
            DoctorCheck::new(
                "git.environment",
                "git",
                CheckStatus::Ok,
                "git version 2.54.0",
            )
            .detail("selected git: /usr/bin/git")
            .detail("git version: git version 2.54.0")
            .detail("repo detected: true"),
            DoctorCheck::new(
                "terminal.env",
                "terminal",
                CheckStatus::Warning,
                "narrow terminal",
            ),
            DoctorCheck::new(
                "terminal.title",
                "title",
                CheckStatus::Ok,
                "terminal title default",
            )
            .detail("terminal title source: default")
            .detail("terminal title items: activity, project-name")
            .detail("terminal title project value: codex"),
            DoctorCheck::new(
                "state.paths",
                "state",
                CheckStatus::Ok,
                "state paths inspectable",
            ),
            DoctorCheck::new(
                "auth.credentials",
                "auth",
                CheckStatus::Fail,
                "token expired",
            )
            .detail("OPENAI_API_KEY: present")
            .remediation("Run `codex login`."),
            DoctorCheck::new(
                "updates.status",
                "updates",
                CheckStatus::Ok,
                "update configuration is locally consistent",
            ),
            DoctorCheck::new(
                "network.env",
                "network",
                CheckStatus::Ok,
                "network environment readable",
            ),
            DoctorCheck::new(
                "network.websocket_reachability",
                "websocket",
                CheckStatus::Ok,
                "Responses WebSocket handshake succeeded",
            ),
            DoctorCheck::new(
                "app_server.status",
                "app-server",
                CheckStatus::Ok,
                "background server is not running",
            ),
            DoctorCheck::new(
                "network.provider_reachability",
                "reachability",
                CheckStatus::Ok,
                "active provider endpoints are reachable over HTTP",
            ),
        ];
        DoctorReport {
            schema_version: 1,
            generated_at: "0s since unix epoch".to_string(),
            overall_status: CheckStatus::Fail,
            codex_version: "0.0.0".to_string(),
            checks,
        }
    }

    #[test]
    fn render_human_report_includes_details_by_default_without_color() {
        let rendered = render_human_report(&sample_report(), detailed_no_color_unicode_options());
        let expected = format!(
            "\
Codex Doctor v0.0.0

Notes
   ⚠ terminal     narrow terminal
   ✗ auth         token expired - Run `codex login`.
─────────────────────────────────────────────────────────────

Environment
  ✓ system       en-US
      os                       macOS 15.0
      OS language              en-US
      VISUAL                   code --wait
      EDITOR                   vim
      PAGER                    less -R
      GIT_PAGER                delta
      GH_PAGER                 less
      LESS                     -FRX
  ✓ runtime      running local build on darwin-arm64
  ✓ install      consistent
      managed by               npm: no · bun: no · package root —
  ✓ search       search is OK (bundled)
  ✓ git          git version 2.54.0
      selected git             /usr/bin/git
      version                  git version 2.54.0
      repo detected            true
  ⚠ terminal     narrow terminal
  ✓ title        default · project codex
      title source             default
      title items              activity, project-name
      project value            codex
  ✓ state        state paths inspectable

Configuration
  ✗ auth         token expired — Run `codex login`.
      OPENAI_API_KEY           present

Updates
  ✓ updates      update configuration is locally consistent

Connectivity
  ✓ network      network environment readable
  ✓ websocket    Responses WebSocket handshake succeeded
  ✓ reachability active provider endpoints are reachable over HTTP

Background Server
  ✓ app-server   background server is not running

{}
12 ok · 2 notes · 1 warn · 1 fail failed

--summary compact output           --all expand truncated lists
--json redacted report
",
            "─".repeat(SEPARATOR_WIDTH)
        );
        assert_eq!(rendered, expected);
    }

    #[test]
    fn render_human_report_snapshot_covers_environment_rows() {
        insta::assert_snapshot!(
            "doctor_human_report_environment_rows",
            render_human_report(&sample_report(), detailed_no_color_unicode_options())
        );
    }

    #[test]
    fn render_human_report_supports_summary_output_without_color() {
        let rendered = render_human_report(&sample_report(), summary_no_color_unicode_options());
        let expected = format!(
            "\
Codex Doctor v0.0.0

Notes
   ⚠ terminal     narrow terminal
   ✗ auth         token expired - Run `codex login`.
─────────────────────────────────────────────────────────────

Environment
  ✓ system       en-US
  ✓ runtime      running local build on darwin-arm64
  ✓ install      consistent
  ✓ search       search is OK (bundled)
  ✓ git          git version 2.54.0
  ⚠ terminal     narrow terminal
  ✓ title        default · project codex
  ✓ state        state paths inspectable

Configuration
  ✗ auth         token expired — Run `codex login`.

Updates
  ✓ updates      update configuration is locally consistent

Connectivity
  ✓ network      network environment readable
  ✓ websocket    Responses WebSocket handshake succeeded
  ✓ reachability active provider endpoints are reachable over HTTP

Background Server
  ✓ app-server   background server is not running

{}
12 ok · 2 notes · 1 warn · 1 fail failed

Run codex doctor without --summary for detailed diagnostics.
--all expand truncated lists       --json redacted report
",
            "─".repeat(SEPARATOR_WIDTH)
        );
        assert_eq!(rendered, expected);
    }

    #[test]
    fn render_human_report_includes_threads_row_in_environment() {
        let mut report = sample_report();
        report.checks.push(DoctorCheck::new(
            "state.rollout_db_parity",
            "threads",
            CheckStatus::Warning,
            "rollout files and state DB thread inventory differ",
        ));

        let rendered = render_human_report(&report, summary_no_color_unicode_options());

        let threads_line = rendered
            .lines()
            .find(|line| line.contains("threads"))
            .expect("threads row should be rendered");
        assert!(
            threads_line.contains("rollout files and state DB thread inventory differ"),
            "{threads_line}"
        );
    }

    #[test]
    fn render_human_report_includes_memories_db_in_state_health_summary() {
        let report = DoctorReport {
            schema_version: 1,
            generated_at: "0s since unix epoch".to_string(),
            overall_status: CheckStatus::Ok,
            codex_version: "0.0.0".to_string(),
            checks: vec![
                DoctorCheck::new(
                    "state.paths",
                    "state",
                    CheckStatus::Ok,
                    "state paths inspectable",
                )
                .detail("state DB: /tmp/state.sqlite")
                .detail("state DB integrity: ok")
                .detail("log DB: /tmp/logs.sqlite")
                .detail("log DB integrity: ok")
                .detail("goals DB: /tmp/goals.sqlite")
                .detail("goals DB integrity: ok")
                .detail("memories DB: /tmp/memories.sqlite")
                .detail("memories DB integrity: ok"),
            ],
        };

        let rendered = render_human_report(&report, detailed_no_color_unicode_options());

        assert!(rendered.contains("✓ state        databases healthy"));
        assert!(rendered.contains("memories DB              /tmp/memories.sqlite · integrity ok"));
    }

    #[test]
    fn render_human_report_supports_ascii_output() {
        let rendered = render_human_report(
            &sample_report(),
            HumanOutputOptions {
                show_details: false,
                show_all: false,
                ascii: true,
                color_enabled: false,
            },
        );
        let expected = format!(
            "\
Codex Doctor v0.0.0

Notes
   [!!] terminal     narrow terminal
   [XX] auth         token expired - Run `codex login`.
-------------------------------------------------------------

Environment
  [ok] system       en-US
  [ok] runtime      running local build on darwin-arm64
  [ok] install      consistent
  [ok] search       search is OK (bundled)
  [ok] git          git version 2.54.0
  [!!] terminal     narrow terminal
  [ok] title        default | project codex
  [ok] state        state paths inspectable

Configuration
  [XX] auth         token expired - Run `codex login`.

Updates
  [ok] updates      update configuration is locally consistent

Connectivity
  [ok] network      network environment readable
  [ok] websocket    Responses WebSocket handshake succeeded
  [ok] reachability active provider endpoints are reachable over HTTP

Background Server
  [ok] app-server   background server is not running

{}
12 ok | 2 notes | 1 warn | 1 fail failed

Run codex doctor without --summary for detailed diagnostics.
--all expand truncated lists       --json redacted report
",
            "-".repeat(SEPARATOR_WIDTH)
        );
        assert_eq!(rendered, expected);
    }

    #[test]
    fn render_human_report_includes_redacted_details() {
        let rendered = render_human_report(
            &sample_report(),
            HumanOutputOptions {
                show_details: true,
                show_all: false,
                ascii: false,
                color_enabled: false,
            },
        );
        assert!(rendered.contains("      OPENAI_API_KEY           present"));
    }

    #[test]
    fn render_human_report_explains_terminal_warning_issue() {
        let report = DoctorReport {
            schema_version: 1,
            generated_at: "0s since unix epoch".to_string(),
            overall_status: CheckStatus::Warning,
            codex_version: "0.0.0".to_string(),
            checks: vec![
                DoctorCheck::new(
                    "terminal.env",
                    "terminal",
                    CheckStatus::Warning,
                    "width 79 cols - output may wrap (recommended >=80)",
                )
                .detail("terminal: Ghostty")
                .detail("terminal version: 1.3.1")
                .detail("terminal size: 79x26")
                .issue(
                    super::super::DoctorIssue::new(
                        CheckStatus::Warning,
                        "width 79 cols - output may wrap (recommended >=80)",
                    )
                    .expected(">= 80 columns")
                    .remedy("resize the window to at least 80 columns")
                    .field("terminal size"),
                ),
            ],
        };

        let rendered = render_human_report(&report, detailed_no_color_unicode_options());

        assert!(
            rendered.contains("⚠ terminal     width 79 cols - output may wrap (recommended >=80)")
        );
        assert!(rendered.contains("▸ terminal size            79x26 (expected >= 80 columns)"));
        assert!(rendered.contains("→ resize the window to at least 80 columns"));
        assert!(!rendered.contains("⚠ terminal     Ghostty 1.3.1"));
    }

    #[test]
    fn render_human_report_promotes_notes_without_changing_statuses() {
        let report = DoctorReport {
            schema_version: 1,
            generated_at: "0s since unix epoch".to_string(),
            overall_status: CheckStatus::Warning,
            codex_version: "0.0.0".to_string(),
            checks: vec![
                DoctorCheck::new(
                    "updates.status",
                    "updates",
                    CheckStatus::Ok,
                    "update configuration is locally consistent",
                )
                .detail("latest version status: newer version is available")
                .detail("latest version: 0.130.0")
                .detail("dismissed version: 0.128.0"),
                DoctorCheck::new(
                    "state.paths",
                    "state",
                    CheckStatus::Ok,
                    "state paths inspectable",
                )
                .detail("active rollout files: 1515 files, 2702146365 total bytes, 1783594 average bytes"),
                DoctorCheck::new(
                    "sandbox.helpers",
                    "sandbox",
                    CheckStatus::Ok,
                    "sandbox configuration is readable",
                )
                .detail("filesystem sandbox: danger-full-access")
                .detail("network sandbox: restricted")
                .detail("approval policy: Never"),
                DoctorCheck::new(
                    "mcp.config",
                    "mcp",
                    CheckStatus::Warning,
                    "MCP configuration has optional issues",
                ),
                DoctorCheck::new(
                    "network.websocket_reachability",
                    "websocket",
                    CheckStatus::Ok,
                    "Responses WebSocket handshake succeeded",
                )
                .detail("auth mode: chatgpt"),
                DoctorCheck::new(
                    "network.provider_reachability",
                    "reachability",
                    CheckStatus::Ok,
                    "active provider endpoints are reachable over HTTP",
                )
                .detail("reachability mode: API key auth"),
                DoctorCheck::new(
                    "app_server.status",
                    "app-server",
                    CheckStatus::Ok,
                    "background server is not running",
                )
                .detail("status: not running")
                .detail("mode: ephemeral"),
            ],
        };

        let rendered = render_human_report(&report, summary_no_color_unicode_options());

        assert!(rendered.contains("Notes\n   ↑ updates"));
        assert!(rendered.contains("0.130.0 available (current 0.0.0, dismissed 0.128.0)"));
        assert!(rendered.contains("⚠ rollouts"));
        assert!(rendered.contains("⚠ sandbox"));
        assert!(rendered.contains("⚠ mcp"));
        assert!(rendered.contains(
            "⚠ auth         mixed auth signals: ChatGPT login plus API key env var; HTTP reachability uses API-key mode"
        ));
        assert!(rendered.contains("○ app-server   not running (ephemeral mode)"));
        assert!(rendered.contains("5 ok · 1 idle · 5 notes · 1 warn · 0 fail degraded"));
    }

    #[test]
    fn render_human_report_expands_feature_flags_with_all() {
        let report = DoctorReport {
            schema_version: 1,
            generated_at: "0s since unix epoch".to_string(),
            overall_status: CheckStatus::Ok,
            codex_version: "0.0.0".to_string(),
            checks: vec![
                DoctorCheck::new("config.load", "config", CheckStatus::Ok, "config loaded")
                    .detail("model: gpt-5.5")
                    .detail("model provider: openai")
                    .detail("feature flags enabled: 3")
                    .detail("enabled feature flags: shell_tool, memories, goals")
                    .detail("feature flag overrides: memories=true"),
            ],
        };

        let compact = render_human_report(&report, detailed_no_color_unicode_options());
        let expanded = render_human_report(&report, detailed_all_no_color_unicode_options());

        assert!(!compact.contains("enabled flags"));
        assert!(
            compact.contains(
                "feature flags            3 enabled · 1 overridden (full list with --all)"
            )
        );
        assert!(expanded.contains("enabled flags            shell_tool, memories, goals"));
    }

    #[test]
    fn detail_value_colors_inline_statuses_and_low_signal_values() {
        let rendered = detail_value(
            "npm: no · commit unknown · integrity ok · ~/code/codex/target/debug/codex · <redacted>",
            detailed_color_unicode_options(),
        );

        assert!(rendered.contains("npm: \u{1b}[38;5;240mno"));
        assert!(rendered.contains("\u{1b}[38;5;240munknown"));
        assert!(rendered.contains("\u{1b}[38;5;10mok"));
        assert!(rendered.contains("\u{1b}[38;5;117m~/code/codex/target/debug/codex"));
        assert!(rendered.contains("\u{1b}[38;5;244m"));
    }

    #[test]
    fn update_note_emphasizes_available_version_and_dims_context() {
        let rendered = style_update_note_summary(
            "0.130.0 available (current 0.0.0, dismissed 0.128.0)",
            detailed_color_unicode_options(),
        );

        assert!(rendered.contains("\u{1b}[38;5;220m0.130.0 available"));
        assert!(rendered.contains("\u{1b}[2m(current 0.0.0, dismissed 0.128.0)"));
    }

    #[test]
    fn redact_detail_sanitizes_urls() {
        let redacted = redact_detail(
            "reachability failed: https://user:pass@example.com/mcp?x=abc#frag (connect failed)",
        );

        assert_eq!(
            redacted,
            "reachability failed: https://example.com/mcp (connect failed)"
        );
    }

    #[test]
    fn redact_detail_sanitizes_secret_url_path_segments() {
        let redacted = redact_detail("reachability failed: https://example.com/mcp/abc123xyz");

        assert_eq!(
            redacted,
            "reachability failed: https://example.com/mcp/<redacted>"
        );
    }

    #[test]
    fn redact_detail_preserves_env_var_names() {
        assert_eq!(
            redact_detail("auth env vars present: OPENAI_API_KEY, CODEX_API_KEY"),
            "auth env vars present: OPENAI_API_KEY, CODEX_API_KEY"
        );
    }

    #[test]
    fn redact_detail_preserves_secret_presence_booleans() {
        assert_eq!(
            redact_detail("stored ChatGPT tokens: true"),
            "stored ChatGPT tokens: true"
        );
        assert_eq!(
            redact_detail("stored ChatGPT tokens: false"),
            "stored ChatGPT tokens: false"
        );
    }

    #[test]
    fn render_human_report_can_emit_color() {
        let rendered = render_human_report(
            &sample_report(),
            HumanOutputOptions {
                show_details: false,
                show_all: false,
                ascii: false,
                color_enabled: true,
            },
        );
        assert!(rendered.contains("\u{1b}["));
    }
}
