complior-cli 1.0.0

AI Act Compliance Scanner & Fixer — CLI
use super::common::ensure_engine;
use crate::cli::RedteamAction;
use crate::config::TuiConfig;

pub async fn run_redteam_command(action: &RedteamAction, config: &TuiConfig) -> i32 {
    match action {
        RedteamAction::Run {
            agent,
            categories,
            max_probes,
            json,
        } => run_redteam_run(agent, categories, *max_probes, *json, config).await,
        RedteamAction::Last { json } => run_redteam_last(*json, config).await,
        RedteamAction::Target {
            url,
            json,
            ci,
            threshold,
        } => {
            // Alias: complior redteam target <url> → eval --security
            super::eval::run_eval_command(
                url,
                false,
                false,
                true,
                false,
                None,
                &[],
                *json,
                *ci,
                *threshold,
                None,
                None,
                None,
                None,
                None,
                false,
                5,
                false,
                false,
                None,
                config,
            )
            .await
        }
    }
}

async fn run_redteam_run(
    agent: &str,
    categories: &[String],
    max_probes: Option<u32>,
    json: bool,
    config: &TuiConfig,
) -> i32 {
    let client = match ensure_engine(config).await {
        Ok(c) => c,
        Err(code) => return code,
    };

    let mut body = serde_json::json!({ "agentName": agent });
    if !categories.is_empty() {
        body["categories"] = serde_json::json!(categories);
    }
    if let Some(max) = max_probes {
        body["maxProbes"] = serde_json::json!(max);
    }

    eprintln!("Running red-team probes against agent '{agent}'...");

    match client.post_json("/redteam/run", &body).await {
        Ok(result) => {
            if let Some(err_msg) = result.get("error").and_then(|v| v.as_str()) {
                let msg = result
                    .get("message")
                    .and_then(|v| v.as_str())
                    .unwrap_or(err_msg);
                eprintln!("Error: {msg}");
                return 1;
            }

            if json {
                println!(
                    "{}",
                    serde_json::to_string_pretty(&result).unwrap_or_default()
                );
                return 0;
            }

            format_redteam_report(&result);
            0
        }
        Err(e) => {
            eprintln!("Error: {e}");
            1
        }
    }
}

async fn run_redteam_last(json: bool, config: &TuiConfig) -> i32 {
    let client = match ensure_engine(config).await {
        Ok(c) => c,
        Err(code) => return code,
    };

    match client.get_json("/redteam/last").await {
        Ok(result) => {
            if let Some(err_msg) = result.get("error").and_then(|v| v.as_str()) {
                let msg = result
                    .get("message")
                    .and_then(|v| v.as_str())
                    .unwrap_or(err_msg);
                eprintln!("Error: {msg}");
                return 1;
            }

            if json {
                println!(
                    "{}",
                    serde_json::to_string_pretty(&result).unwrap_or_default()
                );
                return 0;
            }

            format_redteam_report(&result);
            0
        }
        Err(e) => {
            eprintln!("Error: {e}");
            1
        }
    }
}

fn format_redteam_report(report: &serde_json::Value) {
    let agent = report
        .get("agentName")
        .and_then(|v| v.as_str())
        .unwrap_or("?");
    let total = report
        .get("totalProbes")
        .and_then(serde_json::Value::as_u64)
        .unwrap_or(0);
    let passed = report
        .get("passCount")
        .and_then(serde_json::Value::as_u64)
        .unwrap_or(0);
    let failed = report
        .get("failCount")
        .and_then(serde_json::Value::as_u64)
        .unwrap_or(0);
    let inconclusive = report
        .get("inconclusiveCount")
        .and_then(serde_json::Value::as_u64)
        .unwrap_or(0);
    let duration = report
        .get("duration")
        .and_then(serde_json::Value::as_u64)
        .unwrap_or(0);

    let score = report
        .get("securityScore")
        .and_then(|s| s.get("score"))
        .and_then(serde_json::Value::as_f64)
        .unwrap_or(0.0);
    let grade = report
        .get("securityScore")
        .and_then(|s| s.get("grade"))
        .and_then(|v| v.as_str())
        .unwrap_or("?");
    let capped = report
        .get("securityScore")
        .and_then(|s| s.get("criticalCapped"))
        .and_then(serde_json::Value::as_bool)
        .unwrap_or(false);

    println!();
    println!("  Red-Team Security Report: {agent}");
    println!("  {}", "=".repeat(50));
    println!();

    // Score bar
    let filled = (score / 100.0 * 30.0) as usize;
    let bar: String = format!(
        "[{}{}] {:.0}/100 ({})",
        "#".repeat(filled),
        "-".repeat(30 - filled),
        score,
        grade,
    );
    println!("  Security Score: {bar}");

    if capped {
        println!("  WARNING: Score capped due to critical gap");
    }

    println!();
    println!(
        "  Probes: {total} total | {passed} passed | {failed} failed | {inconclusive} inconclusive"
    );
    println!("  Duration: {:.1}s", duration as f64 / 1000.0);

    // OWASP category breakdown
    if let Some(mapping) = report.get("owaspMapping").and_then(|v| v.as_object()) {
        println!();
        println!(
            "  {:<8} {:<28} {:>6} {:>7} {:>7} {:>7}",
            "ID", "CATEGORY", "SCORE", "PASS", "FAIL", "TOTAL"
        );
        println!("  {}", "-".repeat(68));

        let mut entries: Vec<_> = mapping.iter().collect();
        entries.sort_by_key(|(k, _)| (*k).clone());

        for (_, cat) in entries {
            let cat_id = cat
                .get("categoryId")
                .and_then(|v| v.as_str())
                .unwrap_or("?");
            let cat_name = cat
                .get("categoryName")
                .and_then(|v| v.as_str())
                .unwrap_or("?");
            let cat_score = cat
                .get("score")
                .and_then(serde_json::Value::as_f64)
                .unwrap_or(0.0);
            let cat_pass = cat
                .get("passed")
                .and_then(serde_json::Value::as_u64)
                .unwrap_or(0);
            let cat_fail = cat
                .get("failed")
                .and_then(serde_json::Value::as_u64)
                .unwrap_or(0);
            let cat_total = cat
                .get("total")
                .and_then(serde_json::Value::as_u64)
                .unwrap_or(0);

            // Truncate long names
            let name = if cat_name.len() > 26 {
                &cat_name[..26]
            } else {
                cat_name
            };
            println!(
                "  {cat_id:<8} {name:<28} {cat_score:>5.0}% {cat_pass:>7} {cat_fail:>7} {cat_total:>7}"
            );
        }
    }

    println!();
}