use crate::types::{Finding, Severity};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditReport {
pub text_len_chars: usize,
pub word_count: usize,
pub score: f32, pub findings: Vec<Finding>,
pub finding_counts: HashMap<String, usize>, pub category_counts: HashMap<String, usize>,
pub execution_time_ms: f64,
}
impl AuditReport {
pub fn calculate(
text: &str,
findings: Vec<Finding>,
word_count: usize,
execution_time_ms: f64,
) -> Self {
let mut finding_counts = HashMap::new();
let mut category_counts = HashMap::new();
let mut total_penalty = 0.0f32;
for f in &findings {
let sev = Severity::parse(&f.severity);
*finding_counts.entry(sev.as_str().to_string()).or_insert(0) += 1;
*category_counts.entry(f.category.clone()).or_insert(0) += 1;
let base_penalty = match sev {
Severity::Critical => 12.0,
Severity::Major => 8.0,
Severity::Minor => 3.0,
Severity::Suggestion => 1.0,
};
total_penalty += base_penalty;
}
let effective_words = (word_count as f32).max(50.0);
let density_factor = 200.0 / effective_words;
let scaled_penalty = total_penalty * density_factor;
let score = (100.0 - scaled_penalty).clamp(0.0, 100.0);
AuditReport {
text_len_chars: text.chars().count(),
word_count,
score: (score * 10.0).round() / 10.0, findings,
finding_counts,
category_counts,
execution_time_ms,
}
}
pub fn to_json(&self) -> String {
serde_json::to_string_pretty(self).unwrap_or_else(|_| "{}".to_string())
}
pub fn to_markdown(&self) -> String {
let mut md = String::new();
md.push_str(&format!(
"# BÁO CÁO KIỂM TOÁN VĂN BẢN (Điểm: {}/100)\n\n",
self.score
));
md.push_str(&format!(
"- **Số từ**: {}\n- **Số lỗi phát hiện**: {}\n- **Thời gian xử lý**: {:.2} ms\n\n",
self.word_count,
self.findings.len(),
self.execution_time_ms
));
if self.findings.is_empty() {
md.push_str("✅ **Văn bản đạt chuẩn thuần Việt! Không phát hiện lỗi cấu trúc.**\n");
return md;
}
md.push_str("| Vị trí | Mức độ | Nhóm | Đoạn văn vi phạm | Chi tiết lỗi |\n");
md.push_str("| :--- | :--- | :--- | :--- | :--- |\n");
for f in &self.findings {
let sev = Severity::parse(&f.severity);
let sev_badge = match sev {
Severity::Critical => "🚨 Critical",
Severity::Major => "🔴 Major",
Severity::Minor => "🟡 Minor",
Severity::Suggestion => "💡 Suggestion",
};
md.push_str(&format!(
"| {}..{} | {} | {} | `{}` | {} |\n",
f.start, f.end, sev_badge, f.category, f.matched_span, f.rationale
));
}
md
}
}