xript-wiz 0.4.1

Interactive TUI wizard for the xript toolchain — powered by xript fragments.
use std::path::Path;

use ratatui::style::{Color, Modifier, Style};
use ratatui::text::Span;
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
use ratatui::Frame;
use ratatui::layout::Rect;

use crate::app::App;

pub fn render(frame: &mut Frame, area: Rect, app: &App) {
    let (chunks, next_chunk) = crate::screens::common::render_file_input(
        frame,
        area,
        app,
        "Enter path to a manifest file:",
        "audit",
    );

    if let Some(ref result) = app.result_fragment {
        let text = match result.as_str() {
            Some(s) => s.to_string(),
            None => result.to_string(),
        };

        let result_color = if text.starts_with('\u{2718}') {
            Color::Red
        } else if text.contains("Ungated") || text.contains("gaps") || text.contains("Unused") {
            Color::Yellow
        } else {
            Color::Green
        };

        let result_block = Block::default()
            .borders(Borders::ALL)
            .border_style(Style::default().fg(result_color))
            .title(Span::styled(
                " Audit Report ",
                Style::default()
                    .fg(result_color)
                    .add_modifier(Modifier::BOLD),
            ));

        let result_para = Paragraph::new(text)
            .style(Style::default().fg(Color::White))
            .block(result_block)
            .wrap(Wrap { trim: false });

        if next_chunk < chunks.len() {
            frame.render_widget(result_para, chunks[next_chunk]);
        }
    }
}

pub fn run_audit(path_str: &str) -> serde_json::Value {
    let path = Path::new(path_str.trim());

    let content = match std::fs::read_to_string(path) {
        Ok(c) => c,
        Err(e) => return serde_json::json!(format!("\u{2718} Could not read file: {}", e)),
    };

    let parsed: serde_json::Value = match serde_json::from_str(&content) {
        Ok(v) => v,
        Err(e) => return serde_json::json!(format!("\u{2718} Invalid JSON: {}", e)),
    };

    let bindings = parsed.get("bindings").and_then(|v| v.as_object());
    let capabilities = parsed.get("capabilities").and_then(|v| v.as_object());
    let hooks = parsed.get("hooks").and_then(|v| v.as_object());

    let mut ungated: Vec<String> = Vec::new();
    let mut referenced_caps: Vec<String> = Vec::new();

    if let Some(b) = bindings {
        collect_binding_audit(b, "", &mut ungated, &mut referenced_caps);
    }
    if let Some(h) = hooks {
        for (name, hook) in h {
            if let Some(cap) = hook.get("capability").and_then(|v| v.as_str()) {
                referenced_caps.push(cap.to_string());
            } else {
                ungated.push(format!("hook:{}", name));
            }
        }
    }

    let defined_caps: Vec<String> = capabilities
        .map(|c| c.keys().cloned().collect())
        .unwrap_or_default();

    let unused: Vec<&String> = defined_caps
        .iter()
        .filter(|c| !referenced_caps.contains(c))
        .collect();

    let gaps: Vec<&String> = referenced_caps
        .iter()
        .filter(|c| !defined_caps.contains(c))
        .collect();

    let mut risk_counts = [0u32; 3];
    if let Some(caps) = capabilities {
        for (_name, cap) in caps {
            match cap.get("risk").and_then(|v| v.as_str()).unwrap_or("low") {
                "low" => risk_counts[0] += 1,
                "medium" => risk_counts[1] += 1,
                "high" => risk_counts[2] += 1,
                _ => risk_counts[0] += 1,
            }
        }
    }

    let name = parsed.get("name").and_then(|v| v.as_str()).unwrap_or("unknown");
    let mut report = format!("\u{2714} Audit: {}\n", name);

    report.push_str(&format!("\nCapabilities: {} defined\n", defined_caps.len()));
    if !defined_caps.is_empty() {
        report.push_str(&format!(
            "  Risk: {} low, {} medium, {} high\n",
            risk_counts[0], risk_counts[1], risk_counts[2]
        ));
    }

    if !ungated.is_empty() {
        report.push_str(&format!("\nUngated ({}):\n", ungated.len()));
        for item in &ungated {
            report.push_str(&format!("  \u{2022} {}\n", item));
        }
    } else {
        report.push_str("\nAll bindings and hooks are gated \u{2714}\n");
    }

    if !unused.is_empty() {
        report.push_str(&format!("\nUnused capabilities ({}):\n", unused.len()));
        for cap in &unused {
            report.push_str(&format!("  \u{2022} {}\n", cap));
        }
    }

    if !gaps.is_empty() {
        report.push_str(&format!("\nCapability gaps ({}):\n", gaps.len()));
        for cap in &gaps {
            report.push_str(&format!("  \u{2022} {} (referenced but not defined)\n", cap));
        }
    }

    serde_json::json!(report)
}

fn collect_binding_audit(
    obj: &serde_json::Map<String, serde_json::Value>,
    prefix: &str,
    ungated: &mut Vec<String>,
    referenced_caps: &mut Vec<String>,
) {
    for (name, binding) in obj {
        let full_path = if prefix.is_empty() {
            name.clone()
        } else {
            format!("{}.{}", prefix, name)
        };

        if let Some(members) = binding.get("members").and_then(|v| v.as_object()) {
            collect_binding_audit(members, &full_path, ungated, referenced_caps);
        } else {
            if let Some(cap) = binding.get("capability").and_then(|v| v.as_str()) {
                referenced_caps.push(cap.to_string());
            } else {
                ungated.push(full_path);
            }
        }
    }
}