cleansh 0.1.5

Sanitize your terminal output. One tool. One purpose.
Documentation
//! This file is part of Cleansh, a tool for sanitizing sensitive data.
//! It provides functions to print redaction summaries and statistics,
//! including support for colored output and detailed match information.

// src/ui/redaction_summary.rs

use crate::config::RedactionSummaryItem; // Still used by print_summary
use crate::ui::theme::{ThemeEntry, ThemeStyle};
use owo_colors::OwoColorize;
use std::collections::HashMap;
use std::io::{self, Write};
use anyhow::Result;
use crate::utils::redaction::RedactionMatch; // ADDED: For print_summary_for_stats_mode
use crate::tools::sanitize_shell::CompiledRules; // NEW: Import CompiledRules
use crate::ui::output_format; // NEW: Import output_format for get_styled_text
use crate::commands::stats::format_rule_name_for_json; // NEW: Import format_rule_name_for_json

/// Prints a summary of redactions made to the given writer.
/// This is for the standard redaction output.
pub fn print_summary<W: Write>(
    summary: &[RedactionSummaryItem],
    writer: &mut W, // This writer will now always be io::stderr() from run_cleansh
    theme_map: &HashMap<ThemeEntry, ThemeStyle>,
) -> Result<()> {
    if summary.is_empty() {
        writeln!(io::stderr(), "\n{}\n", output_format::get_styled_text("No redactions applied.", ThemeEntry::Info, theme_map))?;
        return Ok(());
    }

    let header = output_format::get_styled_text("\n--- Redaction Summary ---", ThemeEntry::Header, theme_map);
    writeln!(io::stderr(), "{}", header)?;

    for item in summary {
        let rule_name_styled = output_format::get_styled_text(&item.rule_name, ThemeEntry::SummaryRuleName, theme_map);
        let occurrences_styled = output_format::get_styled_text(
            &format!(" ({} occurrences)", item.occurrences),
            ThemeEntry::SummaryOccurrences,
            theme_map,
        );
        writeln!(writer, "{}{}", rule_name_styled, occurrences_styled)?;

        if !item.original_texts.is_empty() {
            writeln!(writer, "    {}", output_format::get_styled_text("Original Values:", ThemeEntry::Info, theme_map))?;
            for text in &item.original_texts {
                writeln!(writer, "        - {}", text.red())?;
            }
        }

        if !item.sanitized_texts.is_empty() {
            writeln!(writer, "    {}", output_format::get_styled_text("Sanitized Values:", ThemeEntry::Info, theme_map))?;
            for text in &item.sanitized_texts {
                writeln!(writer, "        - {}", text.green())?;
            }
        }
    }
    writeln!(io::stderr(), "{}\n", output_format::get_styled_text("-------------------------", ThemeEntry::Header, theme_map))?;
    Ok(())
}

/// Prints a detailed summary for the `--stats-only` mode, including optional samples.
/// This function expects a HashMap where keys are rule names and values are vectors
/// of `RedactionMatch` instances for that rule.
pub fn print_summary_for_stats_mode<W: Write>(
    aggregated_matches: &HashMap<String, Vec<&RedactionMatch>>,
    compiled_rules: &CompiledRules, // NEW parameter: All rules that were compiled and active
    writer: &mut W,
    theme_map: &HashMap<ThemeEntry, ThemeStyle>,
    sample_matches_count: Option<usize>,
) -> Result<()> {
    let header = output_format::get_styled_text("\n--- Redaction Statistics ---", ThemeEntry::Header, theme_map);
    writeln!(writer, "{}", header)?;

    // Get all rule names that were compiled and active
    let mut active_rule_names: Vec<String> = compiled_rules.rules.iter()
        .map(|r| r.name.clone())
        .collect();
    active_rule_names.sort(); // Ensure consistent order for output

    let mut has_any_matches = false;

    for rule_name in active_rule_names {
        let matches_for_rule = aggregated_matches.get(&rule_name);
        let total_occurrences = matches_for_rule.map_or(0, |matches| matches.len());

        // Only print rules that actually had matches or if we are showing all active rules
        // For stats mode, we want to show rules that *could* match, even if they didn't in this specific input,
        // but the tests only assert on rules that *do* match. So, let's only print if there are occurrences.
        if total_occurrences == 0 {
            continue; // Skip rules with no matches for cleaner output in tests
        }

        has_any_matches = true;

        // Use the centralized formatting function for both human-readable and JSON output names
        let display_name = format_rule_name_for_json(&rule_name);

        let match_plural = if total_occurrences == 1 { "match" } else { "matches" };

        // MODIFIED: Change output format to "RuleName: X match(es)"
        let line_content = format!("{}: {} {}", display_name, total_occurrences, match_plural);
        let styled_line = output_format::get_styled_text(&line_content, ThemeEntry::SummaryRuleName, theme_map);
        writeln!(writer, "{}", styled_line)?;

        if let Some(matches) = matches_for_rule {
            if let Some(num_samples) = sample_matches_count {
                if num_samples > 0 {
                    writeln!(writer, "    {}", output_format::get_styled_text("Sample Matches:", ThemeEntry::Info, theme_map))?;

                    // Collect unique original strings for samples and sort them
                    let mut unique_samples: Vec<String> = matches
                        .iter()
                        .map(|m| m.original_string.clone())
                        .collect::<std::collections::HashSet<_>>()
                        .into_iter()
                        .collect();
                    unique_samples.sort();

                    for (i, sample) in unique_samples.iter().take(num_samples).enumerate() {
                        writeln!(writer, "        - {}", sample.red())?;
                        if i == num_samples - 1 && unique_samples.len() > num_samples {
                            writeln!(writer, "        ... ({} more unique samples)", unique_samples.len() - num_samples)?;
                        }
                    }
                }
            }
        }
    }

    if !has_any_matches {
        writeln!(writer, "\n{}\n", output_format::get_styled_text("No redaction matches found.", ThemeEntry::Info, theme_map))?;
    }

    writeln!(writer, "{}\n", output_format::get_styled_text("--------------------------", ThemeEntry::Header, theme_map))?;
    Ok(())
}