//! Markdown report generator.

use super::escape::{
    escape_markdown_inline, escape_markdown_list, escape_markdown_table, escape_md_opt,
};
use super::{ReportConfig, ReportError, ReportFormat, ReportGenerator, ReportType};
use crate::diff::{DiffResult, SlaStatus, VulnerabilityDetail};
use crate::model::NormalizedSbom;
use crate::quality::{ComplianceChecker, ComplianceLevel, ComplianceResult, ViolationSeverity};
use std::fmt::Write;

/// Markdown report generator
pub struct MarkdownReporter {
    /// Include table of contents
    include_toc: bool,
}

impl MarkdownReporter {
    /// Create a new Markdown reporter
    #[must_use]
    pub const fn new() -> Self {
        Self { include_toc: true }
    }

    /// Set whether to include table of contents
    #[must_use]
    pub const fn include_toc(mut self, include: bool) -> Self {
        self.include_toc = include;
        self
    }
}

impl Default for MarkdownReporter {
    fn default() -> Self {
        Self::new()
    }
}

impl ReportGenerator for MarkdownReporter {
    fn generate_diff_report(
        &self,
        result: &DiffResult,
        old_sbom: &NormalizedSbom,
        new_sbom: &NormalizedSbom,
        config: &ReportConfig,
    ) -> Result<String, ReportError> {
        let mut md = String::new();

        // Title
        let title = config
            .title
            .clone()
            .unwrap_or_else(|| "SBOM Diff Report".to_string());
        writeln!(md, "# {}\n", escape_markdown_inline(&title))?;

        // Metadata
        writeln!(
            md,
            "**Generated by:** sbom-tools v{}",
            env!("CARGO_PKG_VERSION")
        )?;
        writeln!(
            md,
            "**Date:** {}\n",
            chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
        )?;

        // Table of contents
        if self.include_toc {
            writeln!(md, "## Table of Contents\n")?;
            writeln!(md, "- [Summary](#summary)")?;
            if config.includes(ReportType::Components) {
                writeln!(md, "- [Component Changes](#component-changes)")?;
            }
            if config.includes(ReportType::Dependencies) {
                writeln!(md, "- [Dependency Changes](#dependency-changes)")?;
            }
            if config.includes(ReportType::Licenses)
                && (!result.licenses.new_licenses.is_empty()
                    || !result.licenses.removed_licenses.is_empty()
                    || !result.licenses.conflicts.is_empty())
            {
                writeln!(md, "- [License Changes](#license-changes)")?;
            }
            if config.includes(ReportType::Vulnerabilities)
                && (!result.vulnerabilities.introduced.is_empty()
                    || !result.vulnerabilities.resolved.is_empty())
            {
                writeln!(md, "- [Vulnerability Changes](#vulnerability-changes)")?;
            }
            if result
                .graph_summary
                .as_ref()
                .is_some_and(|s| s.total_changes > 0)
            {
                writeln!(md, "- [Graph Changes](#graph-changes)")?;
            }
            writeln!(md, "- [CRA Compliance](#cra-compliance)")?;
            writeln!(md)?;
        }

        // Summary section
        writeln!(md, "## Summary\n")?;
        writeln!(md, "| Metric | Old SBOM | New SBOM |")?;
        writeln!(md, "|--------|----------|----------|")?;
        writeln!(
            md,
            "| **Format** | {} | {} |",
            old_sbom.document.format, new_sbom.document.format
        )?;
        writeln!(
            md,
            "| **Components** | {} | {} |",
            old_sbom.component_count(),
            new_sbom.component_count()
        )?;
        writeln!(
            md,
            "| **Dependencies** | {} | {} |",
            old_sbom.edges.len(),
            new_sbom.edges.len()
        )?;
        writeln!(md)?;

        writeln!(md, "### Change Summary\n")?;
        writeln!(md, "| Category | Count |")?;
        writeln!(md, "|----------|-------|")?;
        writeln!(
            md,
            "| Components Added | {} |",
            result.summary.components_added
        )?;
        writeln!(
            md,
            "| Components Removed | {} |",
            result.summary.components_removed
        )?;
        writeln!(
            md,
            "| Components Modified | {} |",
            result.summary.components_modified
        )?;
        writeln!(
            md,
            "| Vulnerabilities Introduced | {} |",
            result.summary.vulnerabilities_introduced
        )?;
        writeln!(
            md,
            "| Vulnerabilities Resolved | {} |",
            result.summary.vulnerabilities_resolved
        )?;
        writeln!(md, "| **Semantic Score** | {:.1} |", result.semantic_score)?;
        writeln!(md)?;

        // Component changes section
        if config.includes(ReportType::Components) {
            writeln!(md, "## Component Changes\n")?;

            if !result.components.added.is_empty() {
                writeln!(md, "### Added Components\n")?;
                writeln!(md, "| Name | Version | Ecosystem |")?;
                writeln!(md, "|------|---------|-----------|")?;
                for comp in &result.components.added {
                    writeln!(
                        md,
                        "| {} | {} | {} |",
                        escape_markdown_table(&comp.name),
                        escape_md_opt(comp.new_version.as_deref()),
                        escape_md_opt(comp.ecosystem.as_deref())
                    )?;
                }
                writeln!(md)?;
            }

            if !result.components.removed.is_empty() {
                writeln!(md, "### Removed Components\n")?;
                writeln!(md, "| Name | Version | Ecosystem |")?;
                writeln!(md, "|------|---------|-----------|")?;
                for comp in &result.components.removed {
                    writeln!(
                        md,
                        "| {} | {} | {} |",
                        escape_markdown_table(&comp.name),
                        escape_md_opt(comp.old_version.as_deref()),
                        escape_md_opt(comp.ecosystem.as_deref())
                    )?;
                }
                writeln!(md)?;
            }

            if !result.components.modified.is_empty() {
                writeln!(md, "### Modified Components\n")?;
                writeln!(md, "| Name | Old Version | New Version | Changes |")?;
                writeln!(md, "|------|-------------|-------------|---------|")?;
                for comp in &result.components.modified {
                    let changes: Vec<String> = comp
                        .field_changes
                        .iter()
                        .map(|c| escape_markdown_table(&c.field))
                        .collect();
                    writeln!(
                        md,
                        "| {} | {} | {} | {} |",
                        escape_markdown_table(&comp.name),
                        escape_md_opt(comp.old_version.as_deref()),
                        escape_md_opt(comp.new_version.as_deref()),
                        changes.join(", ")
                    )?;
                }
                writeln!(md)?;
            }
        }

        // Dependency changes section
        if config.includes(ReportType::Dependencies) && !result.dependencies.is_empty() {
            writeln!(md, "## Dependency Changes\n")?;

            if !result.dependencies.added.is_empty() {
                writeln!(md, "### Added Dependencies\n")?;
                writeln!(md, "| From | To | Relationship |")?;
                writeln!(md, "|------|----|--------------|")?;
                for dep in &result.dependencies.added {
                    writeln!(
                        md,
                        "| {} | {} | {} |",
                        escape_markdown_table(&dep.from),
                        escape_markdown_table(&dep.to),
                        escape_markdown_table(&dep.relationship)
                    )?;
                }
                writeln!(md)?;
            }

            if !result.dependencies.removed.is_empty() {
                writeln!(md, "### Removed Dependencies\n")?;
                writeln!(md, "| From | To | Relationship |")?;
                writeln!(md, "|------|----|--------------|")?;
                for dep in &result.dependencies.removed {
                    writeln!(
                        md,
                        "| {} | {} | {} |",
                        escape_markdown_table(&dep.from),
                        escape_markdown_table(&dep.to),
                        escape_markdown_table(&dep.relationship)
                    )?;
                }
                writeln!(md)?;
            }
        }

        // License changes section
        if config.includes(ReportType::Licenses)
            && (!result.licenses.new_licenses.is_empty()
                || !result.licenses.removed_licenses.is_empty()
                || !result.licenses.conflicts.is_empty())
        {
            writeln!(md, "## License Changes\n")?;

            if !result.licenses.new_licenses.is_empty() {
                writeln!(md, "### New Licenses\n")?;
                for lic in &result.licenses.new_licenses {
                    let escaped_components: Vec<String> = lic
                        .components
                        .iter()
                        .map(|c| escape_markdown_list(c))
                        .collect();
                    writeln!(
                        md,
                        "- **{}**: {}",
                        escape_markdown_list(&lic.license),
                        escaped_components.join(", ")
                    )?;
                }
                writeln!(md)?;
            }

            if !result.licenses.removed_licenses.is_empty() {
                writeln!(md, "### Removed Licenses\n")?;
                for lic in &result.licenses.removed_licenses {
                    let escaped_components: Vec<String> = lic
                        .components
                        .iter()
                        .map(|c| escape_markdown_list(c))
                        .collect();
                    writeln!(
                        md,
                        "- **{}**: {}",
                        escape_markdown_list(&lic.license),
                        escaped_components.join(", ")
                    )?;
                }
                writeln!(md)?;
            }

            if !result.licenses.conflicts.is_empty() {
                writeln!(md, "### License Conflicts\n")?;
                writeln!(md, "| License A | License B | Component | Description |")?;
                writeln!(md, "|-----------|-----------|-----------|-------------|")?;
                for conflict in &result.licenses.conflicts {
                    writeln!(
                        md,
                        "| {} | {} | {} | {} |",
                        escape_markdown_table(&conflict.license_a),
                        escape_markdown_table(&conflict.license_b),
                        escape_markdown_table(&conflict.component),
                        escape_markdown_table(&conflict.description)
                    )?;
                }
                writeln!(md)?;
            }
        }

        // Vulnerability changes section
        if config.includes(ReportType::Vulnerabilities)
            && (!result.vulnerabilities.introduced.is_empty()
                || !result.vulnerabilities.resolved.is_empty())
        {
            writeln!(md, "## Vulnerability Changes\n")?;

            if !result.vulnerabilities.introduced.is_empty() {
                writeln!(md, "### Introduced Vulnerabilities\n")?;
                writeln!(
                    md,
                    "| ID | Severity | CVSS | SLA | Type | Component | Version | VEX |"
                )?;
                writeln!(
                    md,
                    "|----|----------|------|-----|------|-----------|---------|-----|"
                )?;
                for vuln in &result.vulnerabilities.introduced {
                    let depth_label = match vuln.component_depth {
                        Some(1) => "Direct",
                        Some(_) => "Transitive",
                        None => "-",
                    };
                    let sla_display = format_sla_display(vuln);
                    let vex_display = format_vex_display(vuln.vex_state.as_ref());
                    writeln!(
                        md,
                        "| {} | {} | {} | {} | {} | {} | {} | {} |",
                        escape_markdown_table(&vuln.id),
                        escape_markdown_table(&vuln.severity),
                        vuln.cvss_score
                            .map(|s| format!("{s:.1}"))
                            .as_deref()
                            .unwrap_or("-"),
                        escape_markdown_table(&sla_display),
                        depth_label,
                        escape_markdown_table(&vuln.component_name),
                        escape_md_opt(vuln.version.as_deref()),
                        vex_display,
                    )?;
                }
                writeln!(md)?;
            }

            if !result.vulnerabilities.resolved.is_empty() {
                writeln!(md, "### Resolved Vulnerabilities\n")?;
                writeln!(md, "| ID | Severity | SLA | Type | Component | VEX |")?;
                writeln!(md, "|----|----------|-----|------|-----------|-----|")?;
                for vuln in &result.vulnerabilities.resolved {
                    let depth_label = match vuln.component_depth {
                        Some(1) => "Direct",
                        Some(_) => "Transitive",
                        None => "-",
                    };
                    let sla_display = format_sla_display(vuln);
                    let vex_display = format_vex_display(vuln.vex_state.as_ref());
                    writeln!(
                        md,
                        "| {} | {} | {} | {} | {} | {} |",
                        escape_markdown_table(&vuln.id),
                        escape_markdown_table(&vuln.severity),
                        escape_markdown_table(&sla_display),
                        depth_label,
                        escape_markdown_table(&vuln.component_name),
                        vex_display,
                    )?;
                }
                writeln!(md)?;
            }
        }

        // VEX coverage summary (if any vulns have VEX data)
        {
            let vex_summary = result.vulnerabilities.vex_summary();
            if vex_summary.total_vulns > 0 {
                writeln!(md, "### VEX Coverage\n")?;
                writeln!(md, "| Metric | Value |")?;
                writeln!(md, "|--------|-------|")?;
                writeln!(
                    md,
                    "| Coverage | {:.1}% ({}/{}) |",
                    vex_summary.coverage_pct, vex_summary.with_vex, vex_summary.total_vulns
                )?;
                writeln!(md, "| Actionable | {} |", vex_summary.actionable)?;
                for (state, count) in &vex_summary.by_state {
                    writeln!(md, "| {state} | {count} |")?;
                }
                writeln!(md)?;
            }
        }

        // Graph changes section
        if let Some(ref summary) = result.graph_summary
            && summary.total_changes > 0
        {
            writeln!(md, "## Graph Changes\n")?;
            writeln!(md, "| Type | Count |")?;
            writeln!(md, "|------|-------|")?;
            writeln!(
                md,
                "| Dependencies Added | {} |",
                summary.dependencies_added
            )?;
            writeln!(
                md,
                "| Dependencies Removed | {} |",
                summary.dependencies_removed
            )?;
            writeln!(
                md,
                "| Relationship Changed | {} |",
                summary.relationship_changed
            )?;
            writeln!(md, "| Reparented | {} |", summary.reparented)?;
            writeln!(md, "| Depth Changed | {} |", summary.depth_changed)?;
            writeln!(md, "| **Total** | **{}** |", summary.total_changes)?;
            writeln!(md)?;

            // Detailed graph changes table
            if !result.graph_changes.is_empty() {
                writeln!(md, "### Graph Change Details\n")?;
                writeln!(md, "| Impact | Type | Component | Details |")?;
                writeln!(md, "|--------|------|-----------|---------|")?;
                for change in &result.graph_changes {
                    let impact = change.impact.as_str().to_uppercase();
                    let (change_type, details) = match &change.change {
                        crate::diff::DependencyChangeType::DependencyAdded {
                            dependency_name,
                            ..
                        } => ("Added", format!("+ {dependency_name}")),
                        crate::diff::DependencyChangeType::DependencyRemoved {
                            dependency_name,
                            ..
                        } => ("Removed", format!("- {dependency_name}")),
                        crate::diff::DependencyChangeType::RelationshipChanged {
                            dependency_name,
                            old_relationship,
                            new_relationship,
                            ..
                        } => (
                            "Rel Changed",
                            format!("{dependency_name}: {old_relationship} → {new_relationship}"),
                        ),
                        crate::diff::DependencyChangeType::Reparented {
                            old_parent_name,
                            new_parent_name,
                            ..
                        } => (
                            "Reparented",
                            format!("{old_parent_name} → {new_parent_name}"),
                        ),
                        crate::diff::DependencyChangeType::DepthChanged {
                            old_depth,
                            new_depth,
                        } => {
                            let od = if *old_depth == u32::MAX {
                                "unreachable".to_string()
                            } else {
                                old_depth.to_string()
                            };
                            let nd = if *new_depth == u32::MAX {
                                "unreachable".to_string()
                            } else {
                                new_depth.to_string()
                            };
                            ("Depth", format!("{od} → {nd}"))
                        }
                    };
                    writeln!(
                        md,
                        "| {} | {} | {} | {} |",
                        escape_markdown_table(&impact),
                        change_type,
                        escape_markdown_table(&change.component_name),
                        escape_markdown_table(&details),
                    )?;
                }
                writeln!(md)?;
            }

            if summary.by_impact.critical > 0 || summary.by_impact.high > 0 {
                writeln!(md, "### Impact Summary\n")?;
                writeln!(md, "| Impact | Count |")?;
                writeln!(md, "|--------|-------|")?;
                if summary.by_impact.critical > 0 {
                    writeln!(md, "| Critical | {} |", summary.by_impact.critical)?;
                }
                if summary.by_impact.high > 0 {
                    writeln!(md, "| High | {} |", summary.by_impact.high)?;
                }
                if summary.by_impact.medium > 0 {
                    writeln!(md, "| Medium | {} |", summary.by_impact.medium)?;
                }
                if summary.by_impact.low > 0 {
                    writeln!(md, "| Low | {} |", summary.by_impact.low)?;
                }
                writeln!(md)?;
            }
        }

        // End-of-Life section (from new SBOM)
        {
            let eol_components: Vec<_> = new_sbom
                .components
                .values()
                .filter(|c| {
                    c.eol.as_ref().is_some_and(|e| {
                        matches!(
                            e.status,
                            crate::model::EolStatus::EndOfLife
                                | crate::model::EolStatus::ApproachingEol
                        )
                    })
                })
                .collect();

            if !eol_components.is_empty() {
                writeln!(md, "## End-of-Life Components\n")?;
                writeln!(md, "| Component | Version | Status | Product | EOL Date |")?;
                writeln!(md, "|-----------|---------|--------|---------|----------|")?;
                for comp in &eol_components {
                    let eol = comp.eol.as_ref().expect("filtered to eol.is_some()");
                    writeln!(
                        md,
                        "| {} | {} | {} | {} | {} |",
                        escape_markdown_table(&comp.name),
                        escape_md_opt(comp.version.as_deref()),
                        escape_markdown_table(eol.status.label()),
                        escape_markdown_table(&eol.product),
                        eol.eol_date
                            .map_or_else(|| "-".to_string(), |d| d.to_string()),
                    )?;
                }
                writeln!(md)?;
            }
        }

        // CRA Compliance section
        {
            let old_cra = config.old_cra_compliance.clone().unwrap_or_else(|| {
                ComplianceChecker::new(ComplianceLevel::CraPhase2).check(old_sbom)
            });
            let new_cra = config.new_cra_compliance.clone().unwrap_or_else(|| {
                ComplianceChecker::new(ComplianceLevel::CraPhase2).check(new_sbom)
            });
            write_cra_compliance_diff(&mut md, &old_cra, &new_cra)?;
        }

        // Footer
        writeln!(md, "---\n")?;
        writeln!(md, "*Generated by sbom-tools*")?;

        Ok(md)
    }

    fn generate_view_report(
        &self,
        sbom: &NormalizedSbom,
        config: &ReportConfig,
    ) -> Result<String, ReportError> {
        let mut md = String::new();

        // Title
        let title = config
            .title
            .clone()
            .unwrap_or_else(|| "SBOM Report".to_string());
        writeln!(md, "# {}\n", escape_markdown_inline(&title))?;

        // Metadata
        writeln!(md, "**Format:** {}", sbom.document.format)?;
        writeln!(md, "**Version:** {}", sbom.document.format_version)?;
        if let Some(name) = &sbom.document.name {
            writeln!(md, "**Name:** {}", escape_markdown_inline(name))?;
        }
        writeln!(md)?;

        // Summary
        writeln!(md, "## Summary\n")?;
        writeln!(md, "| Metric | Value |")?;
        writeln!(md, "|--------|-------|")?;
        writeln!(md, "| Total Components | {} |", sbom.component_count())?;
        writeln!(md, "| Total Dependencies | {} |", sbom.edges.len())?;

        let vuln_counts = sbom.vulnerability_counts();
        writeln!(md, "| Total Vulnerabilities | {} |", vuln_counts.total())?;
        writeln!(md, "| Critical | {} |", vuln_counts.critical)?;
        writeln!(md, "| High | {} |", vuln_counts.high)?;
        writeln!(md, "| Medium | {} |", vuln_counts.medium)?;
        writeln!(md, "| Low | {} |", vuln_counts.low)?;
        writeln!(md)?;

        // Components
        writeln!(md, "## Components\n")?;
        writeln!(
            md,
            "| Name | Version | Ecosystem | License | Vulnerabilities |"
        )?;
        writeln!(
            md,
            "|------|---------|-----------|---------|-----------------|"
        )?;

        for comp in sbom.components.values() {
            let license = comp
                .licenses
                .declared
                .first()
                .map(|l| escape_markdown_table(&l.expression));
            let license = license.as_deref().unwrap_or("-");
            writeln!(
                md,
                "| {} | {} | {} | {} | {} |",
                escape_markdown_table(&comp.name),
                escape_md_opt(comp.version.as_deref()),
                comp.ecosystem
                    .as_ref()
                    .map(|e| escape_markdown_table(&e.to_string()))
                    .as_deref()
                    .unwrap_or("-"),
                license,
                comp.vulnerabilities.len()
            )?;
        }

        // CRA Compliance section
        {
            let cra = config
                .view_cra_compliance
                .clone()
                .unwrap_or_else(|| ComplianceChecker::new(ComplianceLevel::CraPhase2).check(sbom));
            write_cra_compliance_view(&mut md, &cra)?;
        }

        Ok(md)
    }

    fn format(&self) -> ReportFormat {
        ReportFormat::Markdown
    }
}

/// Format a delta indicator: arrow showing improvement/regression
fn delta_indicator(old_val: usize, new_val: usize, lower_is_better: bool) -> &'static str {
    if old_val == new_val {
        ""
    } else if (new_val < old_val) == lower_is_better {
        " (+)" // improvement
    } else {
        " (!)" // regression
    }
}

/// Compute compliance score as percentage (0-100)
fn compliance_score(result: &ComplianceResult) -> u8 {
    let total = result.violations.len() + 1; // +1 avoids div-by-zero, counts "base pass"
    let issues = result.error_count + result.warning_count;
    let score = if issues >= total {
        0
    } else {
        ((total - issues) * 100) / total
    };
    score.min(100) as u8
}

/// Write CRA compliance comparison for diff reports
fn write_cra_compliance_diff(
    md: &mut String,
    old: &ComplianceResult,
    new: &ComplianceResult,
) -> std::fmt::Result {
    writeln!(md, "## CRA Compliance\n")?;

    // Status summary with delta indicators
    let old_status = if old.is_compliant {
        "Compliant"
    } else {
        "Non-compliant"
    };
    let new_status = if new.is_compliant {
        "Compliant"
    } else {
        "Non-compliant"
    };
    let old_score = compliance_score(old);
    let new_score = compliance_score(new);
    let err_delta = delta_indicator(old.error_count, new.error_count, true);
    let warn_delta = delta_indicator(old.warning_count, new.warning_count, true);
    let score_delta = delta_indicator(old_score.into(), new_score.into(), false);

    writeln!(md, "| | Old SBOM | New SBOM | Trend |")?;
    writeln!(md, "|--|----------|----------|-------|")?;
    writeln!(md, "| **Status** | {old_status} | {new_status} | |")?;
    writeln!(
        md,
        "| **Score** | {old_score}% | {new_score}% | {score_delta} |"
    )?;
    writeln!(
        md,
        "| **Level** | {} | {} | |",
        old.level.name(),
        new.level.name()
    )?;
    writeln!(
        md,
        "| **Errors** | {} | {} | {err_delta} |",
        old.error_count, new.error_count
    )?;
    writeln!(
        md,
        "| **Warnings** | {} | {} | {warn_delta} |",
        old.warning_count, new.warning_count
    )?;
    writeln!(md)?;

    // Show new SBOM violations if any
    if !new.violations.is_empty() {
        writeln!(md, "### Violations (New SBOM)\n")?;
        write_violation_table(md, &new.violations)?;
    }

    Ok(())
}

/// Write CRA compliance for view reports
fn write_cra_compliance_view(md: &mut String, result: &ComplianceResult) -> std::fmt::Result {
    writeln!(md, "## CRA Compliance\n")?;

    let status = if result.is_compliant {
        "Compliant"
    } else {
        "Non-compliant"
    };
    let score = compliance_score(result);
    writeln!(md, "**Status:** {status}  ")?;
    writeln!(md, "**Score:** {score}%  ")?;
    writeln!(md, "**Level:** {}  ", result.level.name())?;
    writeln!(
        md,
        "**Issues:** {} errors, {} warnings\n",
        result.error_count, result.warning_count
    )?;

    if !result.violations.is_empty() {
        write_violation_table(md, &result.violations)?;
    }

    Ok(())
}

/// Aggregate violations by (severity, category, requirement) to reduce noise.
/// Per-component violations like "Component X missing supplier" x9 become
/// "9 components missing supplier (X, Y, Z, ...)".
fn aggregate_violations(violations: &[crate::quality::Violation]) -> Vec<AggregatedViolation<'_>> {
    use std::collections::BTreeMap;

    // Group by (severity, category, requirement)
    let mut groups: BTreeMap<(u8, &str, &str), Vec<&crate::quality::Violation>> = BTreeMap::new();
    for v in violations {
        let sev_ord = match v.severity {
            ViolationSeverity::Error => 0,
            ViolationSeverity::Warning => 1,
            ViolationSeverity::Info => 2,
        };
        groups
            .entry((sev_ord, v.category.name(), v.requirement.as_str()))
            .or_default()
            .push(v);
    }

    groups
        .into_values()
        .map(|group| {
            if group.len() == 1 {
                AggregatedViolation {
                    severity: group[0].severity,
                    category: group[0].category.name(),
                    requirement: &group[0].requirement,
                    message: group[0].message.clone(),
                    remediation: group[0].remediation_guidance(),
                    count: 1,
                }
            } else {
                let elements: Vec<&str> =
                    group.iter().filter_map(|v| v.element.as_deref()).collect();
                let message = if elements.is_empty() {
                    group[0].message.clone()
                } else {
                    let preview: Vec<&str> = elements.iter().take(5).copied().collect();
                    let suffix = if elements.len() > 5 {
                        format!(", ... +{} more", elements.len() - 5)
                    } else {
                        String::new()
                    };
                    format!(
                        "{} components affected ({}{})",
                        elements.len(),
                        preview.join(", "),
                        suffix
                    )
                };
                AggregatedViolation {
                    severity: group[0].severity,
                    category: group[0].category.name(),
                    requirement: &group[0].requirement,
                    message,
                    remediation: group[0].remediation_guidance(),
                    count: group.len(),
                }
            }
        })
        .collect()
}

struct AggregatedViolation<'a> {
    severity: ViolationSeverity,
    category: &'a str,
    requirement: &'a str,
    message: String,
    remediation: &'static str,
    count: usize,
}

/// Write a markdown table of CRA compliance violations (aggregated)
fn write_violation_table(
    md: &mut String,
    violations: &[crate::quality::Violation],
) -> std::fmt::Result {
    let aggregated = aggregate_violations(violations);
    writeln!(
        md,
        "| Severity | Category | Requirement | Message | Remediation |"
    )?;
    writeln!(
        md,
        "|----------|----------|-------------|---------|-------------|"
    )?;
    for v in &aggregated {
        let severity = match v.severity {
            ViolationSeverity::Error => "Error",
            ViolationSeverity::Warning => "Warning",
            ViolationSeverity::Info => "Info",
        };
        let count_suffix = if v.count > 1 {
            format!(" (x{})", v.count)
        } else {
            String::new()
        };
        writeln!(
            md,
            "| {}{} | {} | {} | {} | {} |",
            severity,
            escape_markdown_table(&count_suffix),
            escape_markdown_table(v.category),
            escape_markdown_table(v.requirement),
            escape_markdown_table(&v.message),
            escape_markdown_table(v.remediation),
        )?;
    }
    writeln!(md)?;
    Ok(())
}

/// Format SLA status for display in reports
fn format_sla_display(vuln: &VulnerabilityDetail) -> String {
    match vuln.sla_status() {
        SlaStatus::Overdue(days) => format!("{days}d late"),
        SlaStatus::DueSoon(days) | SlaStatus::OnTrack(days) => format!("{days}d left"),
        SlaStatus::NoDueDate => vuln
            .days_since_published
            .map_or_else(|| "-".to_string(), |d| format!("{d}d old")),
    }
}

fn format_vex_display(vex_state: Option<&crate::model::VexState>) -> &'static str {
    match vex_state {
        Some(crate::model::VexState::NotAffected) => "Not Affected",
        Some(crate::model::VexState::Fixed) => "Fixed",
        Some(crate::model::VexState::Affected) => "Affected",
        Some(crate::model::VexState::UnderInvestigation) => "Under Investigation",
        None => "-",
    }
}
