//! SBOM Compliance checking module.
//!
//! Validates SBOMs against format requirements and industry standards.

use crate::model::{NormalizedSbom, SbomFormat};
use serde::{Deserialize, Serialize};

/// CRA enforcement phase
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CraPhase {
    /// Phase 1: Reporting obligations — deadline 11 December 2027
    /// Basic SBOM requirements: product/component identification, manufacturer, version, format
    Phase1,
    /// Phase 2: Full compliance — deadline 11 December 2029
    /// Adds: vulnerability metadata, lifecycle/end-of-support, disclosure policy, EU `DoC`
    Phase2,
}

impl CraPhase {
    pub const fn name(self) -> &'static str {
        match self {
            Self::Phase1 => "Phase 1 (2027)",
            Self::Phase2 => "Phase 2 (2029)",
        }
    }

    pub const fn deadline(self) -> &'static str {
        match self {
            Self::Phase1 => "11 December 2027",
            Self::Phase2 => "11 December 2029",
        }
    }
}

/// Compliance level/profile
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ComplianceLevel {
    /// Minimum viable SBOM (basic identification)
    Minimum,
    /// Standard compliance (recommended fields)
    Standard,
    /// NTIA Minimum Elements compliance
    NtiaMinimum,
    /// EU CRA Phase 1 — Reporting obligations (deadline: 11 Dec 2027)
    CraPhase1,
    /// EU CRA Phase 2 — Full compliance (deadline: 11 Dec 2029)
    CraPhase2,
    /// FDA Medical Device SBOM requirements
    FdaMedicalDevice,
    /// NIST SP 800-218 Secure Software Development Framework
    NistSsdf,
    /// Executive Order 14028 Section 4 — Enhancing Software Supply Chain Security
    Eo14028,
    /// Comprehensive compliance (all recommended fields)
    Comprehensive,
}

impl ComplianceLevel {
    /// Get human-readable name
    #[must_use]
    pub const fn name(&self) -> &'static str {
        match self {
            Self::Minimum => "Minimum",
            Self::Standard => "Standard",
            Self::NtiaMinimum => "NTIA Minimum Elements",
            Self::CraPhase1 => "EU CRA Phase 1 (2027)",
            Self::CraPhase2 => "EU CRA Phase 2 (2029)",
            Self::FdaMedicalDevice => "FDA Medical Device",
            Self::NistSsdf => "NIST SSDF (SP 800-218)",
            Self::Eo14028 => "EO 14028 Section 4",
            Self::Comprehensive => "Comprehensive",
        }
    }

    /// Get compact tab label (max ~8 chars) for terminal display.
    #[must_use]
    pub const fn short_name(&self) -> &'static str {
        match self {
            Self::Minimum => "Min",
            Self::Standard => "Std",
            Self::NtiaMinimum => "NTIA",
            Self::CraPhase1 => "CRA-1",
            Self::CraPhase2 => "CRA-2",
            Self::FdaMedicalDevice => "FDA",
            Self::NistSsdf => "SSDF",
            Self::Eo14028 => "EO14028",
            Self::Comprehensive => "Full",
        }
    }

    /// Get description of what this level checks
    #[must_use]
    pub const fn description(&self) -> &'static str {
        match self {
            Self::Minimum => "Basic component identification only",
            Self::Standard => "Recommended fields for general use",
            Self::NtiaMinimum => "NTIA minimum elements for software transparency",
            Self::CraPhase1 => {
                "CRA reporting obligations — product ID, SBOM format, manufacturer (deadline: 11 Dec 2027)"
            }
            Self::CraPhase2 => {
                "Full CRA compliance — adds vulnerability metadata, lifecycle, disclosure (deadline: 11 Dec 2029)"
            }
            Self::FdaMedicalDevice => "FDA premarket submission requirements for medical devices",
            Self::NistSsdf => {
                "Secure Software Development Framework — provenance, build integrity, VCS references"
            }
            Self::Eo14028 => {
                "Executive Order 14028 — machine-readable SBOM, auto-generation, supply chain security"
            }
            Self::Comprehensive => "All recommended fields and best practices",
        }
    }

    /// Get all compliance levels
    #[must_use]
    pub const fn all() -> &'static [Self] {
        &[
            Self::Minimum,
            Self::Standard,
            Self::NtiaMinimum,
            Self::CraPhase1,
            Self::CraPhase2,
            Self::FdaMedicalDevice,
            Self::NistSsdf,
            Self::Eo14028,
            Self::Comprehensive,
        ]
    }

    /// Whether this level is a CRA check (either phase)
    #[must_use]
    pub const fn is_cra(&self) -> bool {
        matches!(self, Self::CraPhase1 | Self::CraPhase2)
    }

    /// Get CRA phase, if applicable
    #[must_use]
    pub const fn cra_phase(&self) -> Option<CraPhase> {
        match self {
            Self::CraPhase1 => Some(CraPhase::Phase1),
            Self::CraPhase2 => Some(CraPhase::Phase2),
            _ => None,
        }
    }
}

/// A compliance violation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Violation {
    /// Severity: error, warning, info
    pub severity: ViolationSeverity,
    /// Category of the violation
    pub category: ViolationCategory,
    /// Human-readable message
    pub message: String,
    /// Component or element that violated (if applicable)
    pub element: Option<String>,
    /// Standard/requirement being violated
    pub requirement: String,
}

impl Violation {
    /// Return remediation guidance for this violation based on the requirement.
    #[must_use]
    pub fn remediation_guidance(&self) -> &'static str {
        let req = self.requirement.to_lowercase();
        if req.contains("art. 13(4)") {
            "Ensure the SBOM is produced in CycloneDX 1.4+ (JSON or XML), SPDX 2.3+ (JSON or tag-value), or SPDX 3.0+ (JSON-LD). Older format versions may not be recognized as machine-readable under the CRA."
        } else if req.contains("art. 13(6)") && req.contains("vulnerability metadata") {
            "Add severity (e.g., CVSS score) and remediation details to each vulnerability entry. CycloneDX: use vulnerability.ratings[].score and vulnerability.analysis. SPDX: use annotation or externalRef."
        } else if req.contains("art. 13(6)") {
            "Add a security contact or vulnerability disclosure URL. CycloneDX: add a component externalReference with type 'security-contact' or set metadata.manufacturer.contact. SPDX: add an SECURITY external reference."
        } else if req.contains("art. 13(7)") {
            "Reference a coordinated vulnerability disclosure policy. CycloneDX: add an externalReference of type 'advisories' linking to your disclosure policy. SPDX: add an external document reference."
        } else if req.contains("art. 13(8)") {
            "Specify when security updates will no longer be provided. CycloneDX 1.5+: use component.releaseNotes or metadata properties. SPDX: use an annotation with end-of-support date."
        } else if req.contains("art. 13(11)") {
            "Include lifecycle or end-of-support metadata for components. CycloneDX: use component properties (e.g., cdx:lifecycle:status). SPDX: use annotations."
        } else if req.contains("art. 13(12)") && req.contains("version") {
            "Every component must have a version string. Use the actual release version (e.g., '1.2.3'), not a range or placeholder."
        } else if req.contains("art. 13(12)") {
            "The SBOM must identify the product by name. CycloneDX: set metadata.component.name. SPDX: set documentDescribes with the primary package name."
        } else if req.contains("art. 13(15)") && req.contains("email") {
            "Provide a valid contact email for the manufacturer. The email must contain an @ sign with valid local and domain parts."
        } else if req.contains("art. 13(15)") {
            "Identify the manufacturer/supplier. CycloneDX: set metadata.manufacturer or component.supplier. SPDX: set PackageSupplier."
        } else if req.contains("annex vii") {
            "Reference the EU Declaration of Conformity. CycloneDX: add an externalReference of type 'attestation' or 'certification'. SPDX: add an external document reference."
        } else if req.contains("annex i") && req.contains("identifier") {
            "Add a PURL, CPE, or SWID tag to each component for unique identification. PURLs are preferred (e.g., pkg:npm/lodash@4.17.21)."
        } else if req.contains("annex i") && req.contains("dependency") {
            "Add dependency relationships between components. CycloneDX: use the dependencies array. SPDX: use DEPENDS_ON relationships."
        } else if req.contains("annex i") && req.contains("primary") {
            "Identify the top-level product component. CycloneDX: set metadata.component. SPDX: use documentDescribes to point to the primary package."
        } else if req.contains("annex i") && req.contains("hash") {
            "Add cryptographic hashes (SHA-256 or stronger) to components for integrity verification."
        } else if req.contains("annex i") && req.contains("traceability") {
            "The primary product component needs a stable unique identifier (PURL or CPE) that persists across software updates for traceability."
        } else if req.contains("art. 13(3)") {
            "Regenerate the SBOM when components are added, removed, or updated. CRA Art. 13(3) requires timely updates reflecting the current state of the software."
        } else if req.contains("art. 13(5)") {
            "Ensure every component has license information. CycloneDX: use component.licenses[]. SPDX 2.x: use PackageLicenseDeclared / PackageLicenseConcluded. SPDX 3.0: use HAS_DECLARED_LICENSE / HAS_CONCLUDED_LICENSE relationships."
        } else if req.contains("art. 13(9)") {
            "Include vulnerability data or add a vulnerability-assertion external reference stating no known vulnerabilities. CycloneDX: use the vulnerabilities array. SPDX: use annotations or external references."
        } else if req.contains("annex i") && req.contains("supply chain") {
            "Populate the supplier field for all components, especially transitive dependencies. CycloneDX: use component.supplier. SPDX: use PackageSupplier."
        } else if req.contains("annex iii") {
            "Add document-level integrity metadata: a serial number (CycloneDX: serialNumber, SPDX: documentNamespace), or a digital signature/attestation with a cryptographic hash."
        } else if req.contains("nist ssdf") || req.contains("sp 800-218") {
            "Follow NIST SP 800-218 SSDF practices: include tool provenance, source VCS references, build metadata, and cryptographic hashes for all components."
        } else if req.contains("eo 14028") {
            "Follow EO 14028 Section 4(e) requirements: use a machine-readable format (CycloneDX 1.4+, SPDX 2.3+, or SPDX 3.0+), auto-generate the SBOM, include unique identifiers, versions, hashes, dependencies, and supplier information."
        } else {
            "Review the requirement and update the SBOM accordingly. Consult the EU CRA regulation (EU 2024/2847) for detailed guidance."
        }
    }
}

/// Severity of a compliance violation
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ViolationSeverity {
    /// Must be fixed for compliance
    Error,
    /// Should be fixed, but not strictly required
    Warning,
    /// Informational recommendation
    Info,
}

/// Category of compliance violation
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ViolationCategory {
    /// Document metadata issue
    DocumentMetadata,
    /// Component identification issue
    ComponentIdentification,
    /// Dependency information issue
    DependencyInfo,
    /// License information issue
    LicenseInfo,
    /// Supplier information issue
    SupplierInfo,
    /// Hash/integrity issue
    IntegrityInfo,
    /// Security/vulnerability disclosure info
    SecurityInfo,
    /// Format-specific requirement
    FormatSpecific,
}

impl ViolationCategory {
    #[must_use]
    pub const fn name(&self) -> &'static str {
        match self {
            Self::DocumentMetadata => "Document Metadata",
            Self::ComponentIdentification => "Component Identification",
            Self::DependencyInfo => "Dependency Information",
            Self::LicenseInfo => "License Information",
            Self::SupplierInfo => "Supplier Information",
            Self::IntegrityInfo => "Integrity Information",
            Self::SecurityInfo => "Security Information",
            Self::FormatSpecific => "Format-Specific",
        }
    }

    /// Short name suitable for compact table display (max 10 chars).
    #[must_use]
    pub const fn short_name(&self) -> &'static str {
        match self {
            Self::DocumentMetadata => "Doc Meta",
            Self::ComponentIdentification => "Comp IDs",
            Self::DependencyInfo => "Deps",
            Self::LicenseInfo => "License",
            Self::SupplierInfo => "Supplier",
            Self::IntegrityInfo => "Integrity",
            Self::SecurityInfo => "Security",
            Self::FormatSpecific => "Format",
        }
    }

    /// All category variants in display order.
    #[must_use]
    pub const fn all() -> &'static [Self] {
        &[
            Self::SupplierInfo,
            Self::ComponentIdentification,
            Self::DocumentMetadata,
            Self::IntegrityInfo,
            Self::LicenseInfo,
            Self::DependencyInfo,
            Self::SecurityInfo,
            Self::FormatSpecific,
        ]
    }
}

/// Result of compliance checking
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComplianceResult {
    /// Overall compliance status
    pub is_compliant: bool,
    /// Compliance level checked against
    pub level: ComplianceLevel,
    /// All violations found
    pub violations: Vec<Violation>,
    /// Error count
    pub error_count: usize,
    /// Warning count
    pub warning_count: usize,
    /// Info count
    pub info_count: usize,
}

impl ComplianceResult {
    /// Create a new compliance result
    #[must_use]
    pub fn new(level: ComplianceLevel, violations: Vec<Violation>) -> Self {
        let error_count = violations
            .iter()
            .filter(|v| v.severity == ViolationSeverity::Error)
            .count();
        let warning_count = violations
            .iter()
            .filter(|v| v.severity == ViolationSeverity::Warning)
            .count();
        let info_count = violations
            .iter()
            .filter(|v| v.severity == ViolationSeverity::Info)
            .count();

        Self {
            is_compliant: error_count == 0,
            level,
            violations,
            error_count,
            warning_count,
            info_count,
        }
    }

    /// Get violations filtered by severity
    #[must_use]
    pub fn violations_by_severity(&self, severity: ViolationSeverity) -> Vec<&Violation> {
        self.violations
            .iter()
            .filter(|v| v.severity == severity)
            .collect()
    }

    /// Get violations filtered by category
    #[must_use]
    pub fn violations_by_category(&self, category: ViolationCategory) -> Vec<&Violation> {
        self.violations
            .iter()
            .filter(|v| v.category == category)
            .collect()
    }
}

/// Compliance checker for SBOMs
#[derive(Debug, Clone)]
pub struct ComplianceChecker {
    /// Compliance level to check
    level: ComplianceLevel,
}

impl ComplianceChecker {
    /// Create a new compliance checker
    #[must_use]
    pub const fn new(level: ComplianceLevel) -> Self {
        Self { level }
    }

    /// Check an SBOM for compliance
    #[must_use]
    pub fn check(&self, sbom: &NormalizedSbom) -> ComplianceResult {
        let mut violations = Vec::new();

        match self.level {
            ComplianceLevel::NistSsdf => {
                self.check_nist_ssdf(sbom, &mut violations);
            }
            ComplianceLevel::Eo14028 => {
                self.check_eo14028(sbom, &mut violations);
            }
            _ => {
                // Check document-level requirements
                self.check_document_metadata(sbom, &mut violations);

                // Check component requirements
                self.check_components(sbom, &mut violations);

                // Check dependency requirements
                self.check_dependencies(sbom, &mut violations);

                // Check vulnerability metadata (CRA readiness)
                self.check_vulnerability_metadata(sbom, &mut violations);

                // Check format-specific requirements
                self.check_format_specific(sbom, &mut violations);

                // Check CRA-specific gap requirements (Art. 13(3), 13(5), 13(9), Annex I Part III, Annex III)
                if self.level.is_cra() {
                    self.check_cra_gaps(sbom, &mut violations);
                }
            }
        }

        ComplianceResult::new(self.level, violations)
    }

    fn check_document_metadata(&self, sbom: &NormalizedSbom, violations: &mut Vec<Violation>) {
        use crate::model::{CreatorType, ExternalRefType};

        // All levels require creator information
        if sbom.document.creators.is_empty() {
            violations.push(Violation {
                severity: match self.level {
                    ComplianceLevel::Minimum => ViolationSeverity::Warning,
                    _ => ViolationSeverity::Error,
                },
                category: ViolationCategory::DocumentMetadata,
                message: "SBOM must have creator/tool information".to_string(),
                element: None,
                requirement: "Document creator identification".to_string(),
            });
        }

        // CRA: Manufacturer identification and product name
        if self.level.is_cra() {
            let has_org = sbom
                .document
                .creators
                .iter()
                .any(|c| c.creator_type == CreatorType::Organization);
            if !has_org {
                violations.push(Violation {
                    severity: ViolationSeverity::Warning,
                    category: ViolationCategory::DocumentMetadata,
                    message:
                        "[CRA Art. 13(15)] SBOM should identify the manufacturer (organization)"
                            .to_string(),
                    element: None,
                    requirement: "CRA Art. 13(15): Manufacturer identification".to_string(),
                });
            }

            // Validate manufacturer email format if present
            for creator in &sbom.document.creators {
                if creator.creator_type == CreatorType::Organization
                    && let Some(email) = &creator.email
                    && !is_valid_email_format(email)
                {
                    violations.push(Violation {
                        severity: ViolationSeverity::Warning,
                        category: ViolationCategory::DocumentMetadata,
                        message: format!(
                            "[CRA Art. 13(15)] Manufacturer email '{email}' appears invalid"
                        ),
                        element: None,
                        requirement: "CRA Art. 13(15): Valid contact information".to_string(),
                    });
                }
            }

            if sbom.document.name.is_none() {
                violations.push(Violation {
                    severity: ViolationSeverity::Warning,
                    category: ViolationCategory::DocumentMetadata,
                    message: "[CRA Art. 13(12)] SBOM should include the product name".to_string(),
                    element: None,
                    requirement: "CRA Art. 13(12): Product identification".to_string(),
                });
            }

            // CRA: Security contact / vulnerability disclosure point
            // First check document-level security contact (preferred)
            let has_doc_security_contact = sbom.document.security_contact.is_some()
                || sbom.document.vulnerability_disclosure_url.is_some();

            // Fallback: check component-level external refs
            let has_component_security_contact = sbom.components.values().any(|comp| {
                comp.external_refs.iter().any(|r| {
                    matches!(
                        r.ref_type,
                        ExternalRefType::SecurityContact
                            | ExternalRefType::Support
                            | ExternalRefType::Advisories
                    )
                })
            });

            if !has_doc_security_contact && !has_component_security_contact {
                violations.push(Violation {
                    severity: ViolationSeverity::Warning,
                    category: ViolationCategory::SecurityInfo,
                    message: "[CRA Art. 13(6)] SBOM should include a security contact or vulnerability disclosure reference".to_string(),
                    element: None,
                    requirement: "CRA Art. 13(6): Vulnerability disclosure contact".to_string(),
                });
            }

            // CRA: Check for primary/root product component identification
            if sbom.primary_component_id.is_none() && sbom.components.len() > 1 {
                violations.push(Violation {
                    severity: ViolationSeverity::Warning,
                    category: ViolationCategory::DocumentMetadata,
                    message: "[CRA Annex I] SBOM should identify the primary product component (CycloneDX metadata.component or SPDX documentDescribes)".to_string(),
                    element: None,
                    requirement: "CRA Annex I: Primary product identification".to_string(),
                });
            }

            // CRA: Check for support end date (informational)
            if sbom.document.support_end_date.is_none() {
                violations.push(Violation {
                    severity: ViolationSeverity::Info,
                    category: ViolationCategory::SecurityInfo,
                    message: "[CRA Art. 13(8)] Consider specifying a support end date for security updates".to_string(),
                    element: None,
                    requirement: "CRA Art. 13(8): Support period disclosure".to_string(),
                });
            }

            // CRA Art. 13(4): Machine-readable SBOM format validation
            // The CRA requires SBOMs in a "commonly used and machine-readable" format.
            // CycloneDX 1.4+ and SPDX 2.3+ are widely accepted as machine-readable.
            let format_ok = match sbom.document.format {
                SbomFormat::CycloneDx => {
                    let v = &sbom.document.spec_version;
                    !(v.starts_with("1.0")
                        || v.starts_with("1.1")
                        || v.starts_with("1.2")
                        || v.starts_with("1.3"))
                }
                SbomFormat::Spdx => {
                    let v = &sbom.document.spec_version;
                    v.starts_with("2.3") || v.starts_with("3.")
                }
            };
            if !format_ok {
                violations.push(Violation {
                    severity: ViolationSeverity::Warning,
                    category: ViolationCategory::FormatSpecific,
                    message: format!(
                        "[CRA Art. 13(4)] SBOM format version {} {} may not meet CRA machine-readable requirements; use CycloneDX 1.4+, SPDX 2.3+, or SPDX 3.0+",
                        sbom.document.format, sbom.document.spec_version
                    ),
                    element: None,
                    requirement: "CRA Art. 13(4): Machine-readable SBOM format".to_string(),
                });
            }

            // CRA Annex I, Part II, 1: Unique product identifier traceability
            // The primary/root component should have a stable unique identifier (PURL or CPE)
            // that can be traced across software updates.
            if let Some(ref primary_id) = sbom.primary_component_id
                && let Some(primary) = sbom.components.get(primary_id)
                && primary.identifiers.purl.is_none()
                && primary.identifiers.cpe.is_empty()
            {
                violations.push(Violation {
                            severity: ViolationSeverity::Warning,
                            category: ViolationCategory::ComponentIdentification,
                            message: format!(
                                "[CRA Annex I, Part II] Primary component '{}' missing unique identifier (PURL/CPE) for cross-update traceability",
                                primary.name
                            ),
                            element: Some(primary.name.clone()),
                            requirement: "CRA Annex I, Part II, 1: Product identifier traceability across updates".to_string(),
                        });
            }
        }

        // CRA Phase 2-only checks (deadline: 11 Dec 2029)
        if matches!(self.level, ComplianceLevel::CraPhase2) {
            // CRA Art. 13(7): Coordinated vulnerability disclosure policy reference
            // Check for a vulnerability disclosure policy URL or advisories reference
            let has_vuln_disclosure_policy = sbom.document.vulnerability_disclosure_url.is_some()
                || sbom.components.values().any(|comp| {
                    comp.external_refs
                        .iter()
                        .any(|r| matches!(r.ref_type, ExternalRefType::Advisories))
                });
            if !has_vuln_disclosure_policy {
                violations.push(Violation {
                    severity: ViolationSeverity::Warning,
                    category: ViolationCategory::SecurityInfo,
                    message: "[CRA Art. 13(7)] SBOM should reference a coordinated vulnerability disclosure policy (advisories URL or disclosure URL)".to_string(),
                    element: None,
                    requirement: "CRA Art. 13(7): Coordinated vulnerability disclosure policy".to_string(),
                });
            }

            // CRA Art. 13(11): Component lifecycle status
            // Check whether the primary component (or any top-level component) has end-of-life
            // or lifecycle information. Currently we check support_end_date at doc level.
            // Also check for lifecycle properties on components.
            let has_lifecycle_info = sbom.document.support_end_date.is_some()
                || sbom.components.values().any(|comp| {
                    comp.extensions.properties.iter().any(|p| {
                        let name_lower = p.name.to_lowercase();
                        name_lower.contains("lifecycle")
                            || name_lower.contains("end-of-life")
                            || name_lower.contains("eol")
                            || name_lower.contains("end-of-support")
                    })
                });
            if !has_lifecycle_info {
                violations.push(Violation {
                    severity: ViolationSeverity::Info,
                    category: ViolationCategory::SecurityInfo,
                    message: "[CRA Art. 13(11)] Consider including component lifecycle/end-of-support information".to_string(),
                    element: None,
                    requirement: "CRA Art. 13(11): Component lifecycle status".to_string(),
                });
            }

            // CRA Annex VII: EU Declaration of Conformity reference
            // Check for an attestation, certification, or declaration-of-conformity reference
            let has_conformity_ref = sbom.components.values().any(|comp| {
                comp.external_refs.iter().any(|r| {
                    matches!(
                        r.ref_type,
                        ExternalRefType::Attestation | ExternalRefType::Certification
                    ) || (matches!(r.ref_type, ExternalRefType::Other(ref s) if s.to_lowercase().contains("declaration-of-conformity"))
                    )
                })
            });
            if !has_conformity_ref {
                violations.push(Violation {
                    severity: ViolationSeverity::Info,
                    category: ViolationCategory::DocumentMetadata,
                    message: "[CRA Annex VII] Consider including a reference to the EU Declaration of Conformity (attestation or certification external reference)".to_string(),
                    element: None,
                    requirement: "CRA Annex VII: EU Declaration of Conformity reference".to_string(),
                });
            }
        }

        // FDA requires manufacturer (organization) as creator
        if matches!(self.level, ComplianceLevel::FdaMedicalDevice) {
            let has_org = sbom
                .document
                .creators
                .iter()
                .any(|c| c.creator_type == CreatorType::Organization);
            if !has_org {
                violations.push(Violation {
                    severity: ViolationSeverity::Warning,
                    category: ViolationCategory::DocumentMetadata,
                    message: "FDA: SBOM should have manufacturer (organization) as creator"
                        .to_string(),
                    element: None,
                    requirement: "FDA: Manufacturer identification".to_string(),
                });
            }

            // FDA recommends contact information
            let has_contact = sbom.document.creators.iter().any(|c| c.email.is_some());
            if !has_contact {
                violations.push(Violation {
                    severity: ViolationSeverity::Warning,
                    category: ViolationCategory::DocumentMetadata,
                    message: "FDA: SBOM creators should include contact email".to_string(),
                    element: None,
                    requirement: "FDA: Contact information".to_string(),
                });
            }

            // FDA: Document name required
            if sbom.document.name.is_none() {
                violations.push(Violation {
                    severity: ViolationSeverity::Warning,
                    category: ViolationCategory::DocumentMetadata,
                    message: "FDA: SBOM should have a document name/title".to_string(),
                    element: None,
                    requirement: "FDA: Document identification".to_string(),
                });
            }
        }

        // NTIA requires timestamp
        if matches!(
            self.level,
            ComplianceLevel::NtiaMinimum | ComplianceLevel::Comprehensive
        ) {
            // Timestamp is always set in our model, but check if it's meaningful
            // For now, we'll skip this check as we always set a timestamp
        }

        // Standard+ requires serial number/document ID
        if matches!(
            self.level,
            ComplianceLevel::Standard
                | ComplianceLevel::FdaMedicalDevice
                | ComplianceLevel::CraPhase1
                | ComplianceLevel::CraPhase2
                | ComplianceLevel::Comprehensive
        ) && sbom.document.serial_number.is_none()
        {
            violations.push(Violation {
                severity: ViolationSeverity::Warning,
                category: ViolationCategory::DocumentMetadata,
                message: "SBOM should have a serial number/unique identifier".to_string(),
                element: None,
                requirement: "Document unique identification".to_string(),
            });
        }
    }

    fn check_components(&self, sbom: &NormalizedSbom, violations: &mut Vec<Violation>) {
        use crate::model::HashAlgorithm;

        for comp in sbom.components.values() {
            // All levels: component must have a name
            // (Always true in our model, but check anyway)
            if comp.name.is_empty() {
                violations.push(Violation {
                    severity: ViolationSeverity::Error,
                    category: ViolationCategory::ComponentIdentification,
                    message: "Component must have a name".to_string(),
                    element: Some(comp.identifiers.format_id.clone()),
                    requirement: "Component name (required)".to_string(),
                });
            }

            // NTIA minimum & FDA: version required
            if matches!(
                self.level,
                ComplianceLevel::NtiaMinimum
                    | ComplianceLevel::FdaMedicalDevice
                    | ComplianceLevel::Standard
                    | ComplianceLevel::CraPhase1
                    | ComplianceLevel::CraPhase2
                    | ComplianceLevel::Comprehensive
            ) && comp.version.is_none()
            {
                let (req, msg) = match self.level {
                    ComplianceLevel::FdaMedicalDevice => (
                        "FDA: Component version".to_string(),
                        format!("Component '{}' missing version", comp.name),
                    ),
                    ComplianceLevel::CraPhase1 | ComplianceLevel::CraPhase2 => (
                        "CRA Art. 13(12): Component version".to_string(),
                        format!(
                            "[CRA Art. 13(12)] Component '{}' missing version",
                            comp.name
                        ),
                    ),
                    _ => (
                        "NTIA: Component version".to_string(),
                        format!("Component '{}' missing version", comp.name),
                    ),
                };
                violations.push(Violation {
                    severity: ViolationSeverity::Error,
                    category: ViolationCategory::ComponentIdentification,
                    message: msg,
                    element: Some(comp.name.clone()),
                    requirement: req,
                });
            }

            // Standard+ & FDA: should have PURL or CPE
            if matches!(
                self.level,
                ComplianceLevel::Standard
                    | ComplianceLevel::FdaMedicalDevice
                    | ComplianceLevel::CraPhase1
                    | ComplianceLevel::CraPhase2
                    | ComplianceLevel::Comprehensive
            ) && comp.identifiers.purl.is_none()
                && comp.identifiers.cpe.is_empty()
                && comp.identifiers.swid.is_none()
            {
                let severity = if matches!(
                    self.level,
                    ComplianceLevel::FdaMedicalDevice
                        | ComplianceLevel::CraPhase1
                        | ComplianceLevel::CraPhase2
                ) {
                    ViolationSeverity::Error
                } else {
                    ViolationSeverity::Warning
                };
                let (message, requirement) = match self.level {
                    ComplianceLevel::FdaMedicalDevice => (
                        format!(
                            "Component '{}' missing unique identifier (PURL/CPE/SWID)",
                            comp.name
                        ),
                        "FDA: Unique component identifier".to_string(),
                    ),
                    ComplianceLevel::CraPhase1 | ComplianceLevel::CraPhase2 => (
                        format!(
                            "[CRA Annex I] Component '{}' missing unique identifier (PURL/CPE/SWID)",
                            comp.name
                        ),
                        "CRA Annex I: Unique component identifier (PURL/CPE/SWID)".to_string(),
                    ),
                    _ => (
                        format!(
                            "Component '{}' missing unique identifier (PURL/CPE/SWID)",
                            comp.name
                        ),
                        "Standard identifier (PURL/CPE)".to_string(),
                    ),
                };
                violations.push(Violation {
                    severity,
                    category: ViolationCategory::ComponentIdentification,
                    message,
                    element: Some(comp.name.clone()),
                    requirement,
                });
            }

            // NTIA minimum & FDA: supplier required
            if matches!(
                self.level,
                ComplianceLevel::NtiaMinimum
                    | ComplianceLevel::FdaMedicalDevice
                    | ComplianceLevel::CraPhase1
                    | ComplianceLevel::CraPhase2
                    | ComplianceLevel::Comprehensive
            ) && comp.supplier.is_none()
                && comp.author.is_none()
            {
                let severity = match self.level {
                    ComplianceLevel::CraPhase1 | ComplianceLevel::CraPhase2 => {
                        ViolationSeverity::Warning
                    }
                    _ => ViolationSeverity::Error,
                };
                let (message, requirement) = match self.level {
                    ComplianceLevel::FdaMedicalDevice => (
                        format!("Component '{}' missing supplier/manufacturer", comp.name),
                        "FDA: Supplier/manufacturer information".to_string(),
                    ),
                    ComplianceLevel::CraPhase1 | ComplianceLevel::CraPhase2 => (
                        format!(
                            "[CRA Art. 13(15)] Component '{}' missing supplier/manufacturer",
                            comp.name
                        ),
                        "CRA Art. 13(15): Supplier/manufacturer information".to_string(),
                    ),
                    _ => (
                        format!("Component '{}' missing supplier/manufacturer", comp.name),
                        "NTIA: Supplier information".to_string(),
                    ),
                };
                violations.push(Violation {
                    severity,
                    category: ViolationCategory::SupplierInfo,
                    message,
                    element: Some(comp.name.clone()),
                    requirement,
                });
            }

            // Standard+: should have license information
            if matches!(
                self.level,
                ComplianceLevel::Standard | ComplianceLevel::Comprehensive
            ) && comp.licenses.declared.is_empty()
                && comp.licenses.concluded.is_none()
            {
                violations.push(Violation {
                    severity: ViolationSeverity::Warning,
                    category: ViolationCategory::LicenseInfo,
                    message: format!("Component '{}' should have license information", comp.name),
                    element: Some(comp.name.clone()),
                    requirement: "License declaration".to_string(),
                });
            }

            // FDA & Comprehensive: must have cryptographic hashes
            if matches!(
                self.level,
                ComplianceLevel::FdaMedicalDevice | ComplianceLevel::Comprehensive
            ) {
                if comp.hashes.is_empty() {
                    violations.push(Violation {
                        severity: if self.level == ComplianceLevel::FdaMedicalDevice {
                            ViolationSeverity::Error
                        } else {
                            ViolationSeverity::Warning
                        },
                        category: ViolationCategory::IntegrityInfo,
                        message: format!("Component '{}' missing cryptographic hash", comp.name),
                        element: Some(comp.name.clone()),
                        requirement: if self.level == ComplianceLevel::FdaMedicalDevice {
                            "FDA: Cryptographic hash for integrity".to_string()
                        } else {
                            "Integrity verification (hashes)".to_string()
                        },
                    });
                } else if self.level == ComplianceLevel::FdaMedicalDevice {
                    // FDA: Check for strong hash algorithm (SHA-256 or better)
                    let has_strong_hash = comp.hashes.iter().any(|h| {
                        matches!(
                            h.algorithm,
                            HashAlgorithm::Sha256
                                | HashAlgorithm::Sha384
                                | HashAlgorithm::Sha512
                                | HashAlgorithm::Sha3_256
                                | HashAlgorithm::Sha3_384
                                | HashAlgorithm::Sha3_512
                                | HashAlgorithm::Blake2b256
                                | HashAlgorithm::Blake2b384
                                | HashAlgorithm::Blake2b512
                                | HashAlgorithm::Blake3
                                | HashAlgorithm::Streebog256
                                | HashAlgorithm::Streebog512
                        )
                    });
                    if !has_strong_hash {
                        violations.push(Violation {
                            severity: ViolationSeverity::Warning,
                            category: ViolationCategory::IntegrityInfo,
                            message: format!(
                                "Component '{}' has only weak hash algorithm (use SHA-256+)",
                                comp.name
                            ),
                            element: Some(comp.name.clone()),
                            requirement: "FDA: Strong cryptographic hash (SHA-256 or better)"
                                .to_string(),
                        });
                    }
                }
            }

            // CRA: hashes are recommended for integrity verification
            if self.level.is_cra() && comp.hashes.is_empty() {
                violations.push(Violation {
                    severity: ViolationSeverity::Info,
                    category: ViolationCategory::IntegrityInfo,
                    message: format!(
                        "[CRA Annex I] Component '{}' missing cryptographic hash (recommended for integrity)",
                        comp.name
                    ),
                    element: Some(comp.name.clone()),
                    requirement: "CRA Annex I: Component integrity information (hash)".to_string(),
                });
            }
        }
    }

    fn check_dependencies(&self, sbom: &NormalizedSbom, violations: &mut Vec<Violation>) {
        // NTIA & FDA require dependency relationships
        if matches!(
            self.level,
            ComplianceLevel::NtiaMinimum
                | ComplianceLevel::FdaMedicalDevice
                | ComplianceLevel::CraPhase1
                | ComplianceLevel::CraPhase2
                | ComplianceLevel::Comprehensive
        ) {
            let has_deps = !sbom.edges.is_empty();
            let has_multiple_components = sbom.components.len() > 1;

            if has_multiple_components && !has_deps {
                let (message, requirement) = match self.level {
                    ComplianceLevel::CraPhase1 | ComplianceLevel::CraPhase2 => (
                        "[CRA Annex I] SBOM with multiple components must include dependency relationships".to_string(),
                        "CRA Annex I: Dependency relationships".to_string(),
                    ),
                    _ => (
                        "SBOM with multiple components must include dependency relationships".to_string(),
                        "NTIA: Dependency relationships".to_string(),
                    ),
                };
                violations.push(Violation {
                    severity: ViolationSeverity::Error,
                    category: ViolationCategory::DependencyInfo,
                    message,
                    element: None,
                    requirement,
                });
            }
        }

        // CRA: warn if multiple root components (no incoming edges) and no primary component set
        if self.level.is_cra() && sbom.components.len() > 1 && sbom.primary_component_id.is_none() {
            use std::collections::HashSet;
            let mut incoming: HashSet<&crate::model::CanonicalId> = HashSet::new();
            for edge in &sbom.edges {
                incoming.insert(&edge.to);
            }
            let root_count = sbom.components.len().saturating_sub(incoming.len());
            if root_count > 1 {
                violations.push(Violation {
                    severity: ViolationSeverity::Warning,
                    category: ViolationCategory::DependencyInfo,
                    message: "[CRA Annex I] SBOM appears to have multiple root components; identify a primary product component for top-level dependencies".to_string(),
                    element: None,
                    requirement: "CRA Annex I: Top-level dependency clarity".to_string(),
                });
            }
        }
    }

    fn check_vulnerability_metadata(&self, sbom: &NormalizedSbom, violations: &mut Vec<Violation>) {
        if !matches!(self.level, ComplianceLevel::CraPhase2) {
            return;
        }

        for (comp, vuln) in sbom.all_vulnerabilities() {
            if vuln.severity.is_none() && vuln.cvss.is_empty() {
                violations.push(Violation {
                    severity: ViolationSeverity::Warning,
                    category: ViolationCategory::SecurityInfo,
                    message: format!(
                        "[CRA Art. 13(6)] Vulnerability '{}' in '{}' lacks severity or CVSS score",
                        vuln.id, comp.name
                    ),
                    element: Some(comp.name.clone()),
                    requirement: "CRA Art. 13(6): Vulnerability metadata completeness".to_string(),
                });
            }

            if let Some(remediation) = &vuln.remediation
                && remediation.fixed_version.is_none()
                && remediation.description.is_none()
            {
                violations.push(Violation {
                        severity: ViolationSeverity::Info,
                        category: ViolationCategory::SecurityInfo,
                        message: format!(
                            "[CRA Art. 13(6)] Vulnerability '{}' in '{}' has remediation without details",
                            vuln.id, comp.name
                        ),
                        element: Some(comp.name.clone()),
                        requirement: "CRA Art. 13(6): Remediation detail".to_string(),
                    });
            }
        }
    }

    /// CRA gap checks: Art. 13(3), 13(5), 13(9), Annex I Part III, Annex III
    fn check_cra_gaps(&self, sbom: &NormalizedSbom, violations: &mut Vec<Violation>) {
        // B1: Art. 13(3) — Update frequency / SBOM freshness
        let age_days = (chrono::Utc::now() - sbom.document.created).num_days();
        if age_days > 90 {
            violations.push(Violation {
                severity: ViolationSeverity::Warning,
                category: ViolationCategory::DocumentMetadata,
                message: format!(
                    "[CRA Art. 13(3)] SBOM is {age_days} days old; CRA requires timely updates when components change"
                ),
                element: None,
                requirement: "CRA Art. 13(3): SBOM update frequency".to_string(),
            });
        } else if age_days > 30 {
            violations.push(Violation {
                severity: ViolationSeverity::Info,
                category: ViolationCategory::DocumentMetadata,
                message: format!(
                    "[CRA Art. 13(3)] SBOM is {age_days} days old; consider regenerating after component changes"
                ),
                element: None,
                requirement: "CRA Art. 13(3): SBOM update frequency".to_string(),
            });
        }

        // B2: Art. 13(5) — Licensed component tracking (all components should have license info)
        let total = sbom.components.len();
        let without_license = sbom
            .components
            .values()
            .filter(|c| c.licenses.declared.is_empty() && c.licenses.concluded.is_none())
            .count();
        if without_license > 0 {
            let pct = (without_license * 100) / total.max(1);
            let severity = if pct > 50 {
                ViolationSeverity::Warning
            } else {
                ViolationSeverity::Info
            };
            violations.push(Violation {
                severity,
                category: ViolationCategory::LicenseInfo,
                message: format!(
                    "[CRA Art. 13(5)] {without_license}/{total} components ({pct}%) missing license information"
                ),
                element: None,
                requirement: "CRA Art. 13(5): Licensed component tracking".to_string(),
            });
        }

        // B3: Art. 13(9) — Known vulnerabilities statement
        // SBOM should either contain vulnerability data or explicitly indicate "none known"
        let has_vuln_data = sbom
            .components
            .values()
            .any(|c| !c.vulnerabilities.is_empty());
        let has_vuln_assertion = sbom.components.values().any(|comp| {
            comp.external_refs.iter().any(|r| {
                matches!(
                    r.ref_type,
                    crate::model::ExternalRefType::VulnerabilityAssertion
                        | crate::model::ExternalRefType::ExploitabilityStatement
                )
            })
        });
        if !has_vuln_data && !has_vuln_assertion {
            violations.push(Violation {
                severity: ViolationSeverity::Info,
                category: ViolationCategory::SecurityInfo,
                message:
                    "[CRA Art. 13(9)] No vulnerability data or vulnerability assertion found; \
                    include vulnerability information or a statement of no known vulnerabilities"
                        .to_string(),
                element: None,
                requirement: "CRA Art. 13(9): Known vulnerabilities statement".to_string(),
            });
        }

        // B4: Annex I Part III — Supply chain transparency
        // Transitive dependencies should have supplier information for supply chain visibility
        if !sbom.edges.is_empty() {
            let transitive_without_supplier = sbom
                .components
                .values()
                .filter(|c| c.supplier.is_none() && c.author.is_none())
                .count();
            if transitive_without_supplier > 0 {
                let pct = (transitive_without_supplier * 100) / total.max(1);
                if pct > 30 {
                    violations.push(Violation {
                        severity: ViolationSeverity::Warning,
                        category: ViolationCategory::SupplierInfo,
                        message: format!(
                            "[CRA Annex I, Part III] {transitive_without_supplier}/{total} components ({pct}%) \
                            missing supplier information for supply chain transparency"
                        ),
                        element: None,
                        requirement: "CRA Annex I, Part III: Supply chain transparency".to_string(),
                    });
                }
            }
        }

        // B5: Annex III — Document signature/integrity
        // Check for document-level hash, signature, or attestation
        let has_doc_integrity = sbom.document.serial_number.is_some()
            || sbom.components.values().any(|comp| {
                comp.external_refs.iter().any(|r| {
                    matches!(
                        r.ref_type,
                        crate::model::ExternalRefType::Attestation
                            | crate::model::ExternalRefType::Certification
                    ) && !r.hashes.is_empty()
                })
            });
        if !has_doc_integrity {
            violations.push(Violation {
                severity: ViolationSeverity::Info,
                category: ViolationCategory::IntegrityInfo,
                message: "[CRA Annex III] Consider adding document-level integrity metadata \
                    (serial number, digital signature, or attestation with hash)"
                    .to_string(),
                element: None,
                requirement: "CRA Annex III: Document signature/integrity".to_string(),
            });
        }

        // B6: Art. 13(8) / Art. 13(11) — Component lifecycle / EOL detection
        // If EOL enrichment data is present, warn about EOL components
        let eol_count = sbom
            .components
            .values()
            .filter(|c| {
                c.eol
                    .as_ref()
                    .is_some_and(|e| e.status == crate::model::EolStatus::EndOfLife)
            })
            .count();
        if eol_count > 0 {
            violations.push(Violation {
                severity: ViolationSeverity::Warning,
                category: ViolationCategory::SecurityInfo,
                message: format!(
                    "[CRA Art. 13(8)] {eol_count} component(s) have reached end-of-life and no longer receive security updates"
                ),
                element: None,
                requirement: "CRA Art. 13(8): Support period / lifecycle management".to_string(),
            });
        }

        let approaching_eol_count = sbom
            .components
            .values()
            .filter(|c| {
                c.eol
                    .as_ref()
                    .is_some_and(|e| e.status == crate::model::EolStatus::ApproachingEol)
            })
            .count();
        if approaching_eol_count > 0 {
            violations.push(Violation {
                severity: ViolationSeverity::Info,
                category: ViolationCategory::SecurityInfo,
                message: format!(
                    "[CRA Art. 13(11)] {approaching_eol_count} component(s) are approaching end-of-life within 6 months"
                ),
                element: None,
                requirement: "CRA Art. 13(11): Component lifecycle monitoring".to_string(),
            });
        }

        // SPDX 3.0 profile conformance checks (Phase 6)
        if sbom.document.format == crate::model::SbomFormat::Spdx
            && sbom.document.spec_version.starts_with("3.")
        {
            // Check if Security profile is declared when vulnerabilities are present
            let has_vulns = sbom
                .components
                .values()
                .any(|c| !c.vulnerabilities.is_empty());
            let has_security_profile = sbom
                .document
                .distribution_classification
                .as_ref()
                .is_some_and(|p| p.to_lowercase().contains("security"));

            if has_vulns && !has_security_profile {
                violations.push(Violation {
                    severity: ViolationSeverity::Info,
                    category: ViolationCategory::DocumentMetadata,
                    message:
                        "[CRA Art. 13(6)] SPDX 3.0 document contains vulnerabilities but does not declare Security profile conformance; declare profileConformance: [\"security\"] for CRA Art. 13(6) compliance"
                            .to_string(),
                    element: None,
                    requirement: "CRA Art. 13(6): SPDX 3.0 Security profile conformance"
                        .to_string(),
                });
            }

            // Check if SimpleLicensing profile is declared when licenses are tracked
            let has_licenses = sbom
                .components
                .values()
                .any(|c| !c.licenses.declared.is_empty() || c.licenses.concluded.is_some());
            let has_licensing_profile = sbom
                .document
                .distribution_classification
                .as_ref()
                .is_some_and(|p| {
                    p.to_lowercase().contains("simplelicensing")
                        || p.to_lowercase().contains("licensing")
                });

            if has_licenses && !has_licensing_profile {
                violations.push(Violation {
                    severity: ViolationSeverity::Info,
                    category: ViolationCategory::LicenseInfo,
                    message:
                        "[CRA Art. 13(5)] SPDX 3.0 document tracks licenses but does not declare SimpleLicensing profile conformance; declare profileConformance: [\"simpleLicensing\"] for completeness"
                            .to_string(),
                    element: None,
                    requirement: "CRA Art. 13(5): SPDX 3.0 SimpleLicensing profile conformance"
                        .to_string(),
                });
            }
        }
    }

    /// NIST SP 800-218 Secure Software Development Framework checks
    fn check_nist_ssdf(&self, sbom: &NormalizedSbom, violations: &mut Vec<Violation>) {
        use crate::model::ExternalRefType;

        // PS.1 — Provenance: creator/tool information
        if sbom.document.creators.is_empty() {
            violations.push(Violation {
                severity: ViolationSeverity::Error,
                category: ViolationCategory::DocumentMetadata,
                message:
                    "SBOM must identify its creator (tool or organization) for provenance tracking"
                        .to_string(),
                element: None,
                requirement: "NIST SSDF PS.1: Provenance — creator identification".to_string(),
            });
        }

        let has_tool_creator = sbom
            .document
            .creators
            .iter()
            .any(|c| c.creator_type == crate::model::CreatorType::Tool);
        if !has_tool_creator {
            violations.push(Violation {
                severity: ViolationSeverity::Warning,
                category: ViolationCategory::DocumentMetadata,
                message: "SBOM should identify the generation tool for automated provenance"
                    .to_string(),
                element: None,
                requirement: "NIST SSDF PS.1: Provenance — tool identification".to_string(),
            });
        }

        // PS.2 — Build integrity: components should have hashes
        let total = sbom.components.len();
        let without_hash = sbom
            .components
            .values()
            .filter(|c| c.hashes.is_empty())
            .count();
        if without_hash > 0 {
            let pct = (without_hash * 100) / total.max(1);
            violations.push(Violation {
                severity: if pct > 50 {
                    ViolationSeverity::Error
                } else {
                    ViolationSeverity::Warning
                },
                category: ViolationCategory::IntegrityInfo,
                message: format!(
                    "{without_hash}/{total} components ({pct}%) missing cryptographic hashes for build integrity"
                ),
                element: None,
                requirement: "NIST SSDF PS.2: Build integrity — component hashes".to_string(),
            });
        }

        // PO.1 — VCS references: at least some components should reference their source
        let has_vcs_ref = sbom.components.values().any(|comp| {
            comp.external_refs
                .iter()
                .any(|r| matches!(r.ref_type, ExternalRefType::Vcs))
        });
        if !has_vcs_ref {
            violations.push(Violation {
                severity: ViolationSeverity::Warning,
                category: ViolationCategory::ComponentIdentification,
                message: "No components reference a VCS repository; include source repository links for traceability"
                    .to_string(),
                element: None,
                requirement: "NIST SSDF PO.1: Source code provenance — VCS references".to_string(),
            });
        }

        // PO.3 — Build metadata: check for build system/meta references
        let has_build_ref = sbom.components.values().any(|comp| {
            comp.external_refs.iter().any(|r| {
                matches!(
                    r.ref_type,
                    ExternalRefType::BuildMeta | ExternalRefType::BuildSystem
                )
            })
        });
        if !has_build_ref {
            violations.push(Violation {
                severity: ViolationSeverity::Info,
                category: ViolationCategory::DocumentMetadata,
                message: "No build metadata references found; include build system information for reproducibility"
                    .to_string(),
                element: None,
                requirement: "NIST SSDF PO.3: Build provenance — build metadata".to_string(),
            });
        }

        // PW.4 — Dependency management: dependency relationships required
        if sbom.components.len() > 1 && sbom.edges.is_empty() {
            violations.push(Violation {
                severity: ViolationSeverity::Error,
                category: ViolationCategory::DependencyInfo,
                message: "SBOM with multiple components must include dependency relationships"
                    .to_string(),
                element: None,
                requirement: "NIST SSDF PW.4: Dependency management — relationships".to_string(),
            });
        }

        // PW.6 — Vulnerability information
        let has_vuln_info = sbom
            .components
            .values()
            .any(|c| !c.vulnerabilities.is_empty());
        let has_security_ref = sbom.components.values().any(|comp| {
            comp.external_refs.iter().any(|r| {
                matches!(
                    r.ref_type,
                    ExternalRefType::Advisories
                        | ExternalRefType::SecurityContact
                        | ExternalRefType::VulnerabilityAssertion
                )
            })
        });
        if !has_vuln_info && !has_security_ref {
            violations.push(Violation {
                severity: ViolationSeverity::Info,
                category: ViolationCategory::SecurityInfo,
                message: "No vulnerability or security advisory references found; \
                    include vulnerability data or security contact for incident response"
                    .to_string(),
                element: None,
                requirement: "NIST SSDF PW.6: Vulnerability information".to_string(),
            });
        }

        // RV.1 — Component identification: unique identifiers (PURL/CPE)
        let without_id = sbom
            .components
            .values()
            .filter(|c| {
                c.identifiers.purl.is_none()
                    && c.identifiers.cpe.is_empty()
                    && c.identifiers.swid.is_none()
            })
            .count();
        if without_id > 0 {
            violations.push(Violation {
                severity: ViolationSeverity::Warning,
                category: ViolationCategory::ComponentIdentification,
                message: format!(
                    "{without_id}/{total} components missing unique identifier (PURL/CPE/SWID)"
                ),
                element: None,
                requirement: "NIST SSDF RV.1: Component identification — unique identifiers"
                    .to_string(),
            });
        }

        // PS.3 — Supplier identification
        let without_supplier = sbom
            .components
            .values()
            .filter(|c| c.supplier.is_none() && c.author.is_none())
            .count();
        if without_supplier > 0 {
            violations.push(Violation {
                severity: ViolationSeverity::Warning,
                category: ViolationCategory::SupplierInfo,
                message: format!(
                    "{without_supplier}/{total} components missing supplier/author information"
                ),
                element: None,
                requirement: "NIST SSDF PS.3: Supplier identification".to_string(),
            });
        }
    }

    /// Executive Order 14028 Section 4 checks
    fn check_eo14028(&self, sbom: &NormalizedSbom, violations: &mut Vec<Violation>) {
        use crate::model::ExternalRefType;

        // Sec 4(e) — Machine-readable format
        let format_ok = match sbom.document.format {
            crate::model::SbomFormat::CycloneDx => {
                let v = &sbom.document.spec_version;
                !(v.starts_with("1.0")
                    || v.starts_with("1.1")
                    || v.starts_with("1.2")
                    || v.starts_with("1.3"))
            }
            crate::model::SbomFormat::Spdx => {
                let v = &sbom.document.spec_version;
                v.starts_with("2.3") || v.starts_with("3.")
            }
        };
        if !format_ok {
            violations.push(Violation {
                severity: ViolationSeverity::Error,
                category: ViolationCategory::FormatSpecific,
                message: format!(
                    "SBOM format {} {} does not meet EO 14028 machine-readable requirements; \
                    use CycloneDX 1.4+, SPDX 2.3+, or SPDX 3.0+",
                    sbom.document.format, sbom.document.spec_version
                ),
                element: None,
                requirement: "EO 14028 Sec 4(e): Machine-readable SBOM format".to_string(),
            });
        }

        // Sec 4(e) — Automated generation: tool creator should be present
        let has_tool = sbom
            .document
            .creators
            .iter()
            .any(|c| c.creator_type == crate::model::CreatorType::Tool);
        if !has_tool {
            violations.push(Violation {
                severity: ViolationSeverity::Warning,
                category: ViolationCategory::DocumentMetadata,
                message: "SBOM should be auto-generated by a tool; no tool creator identified"
                    .to_string(),
                element: None,
                requirement: "EO 14028 Sec 4(e): Automated SBOM generation".to_string(),
            });
        }

        // Sec 4(e) — Creator identification
        if sbom.document.creators.is_empty() {
            violations.push(Violation {
                severity: ViolationSeverity::Error,
                category: ViolationCategory::DocumentMetadata,
                message: "SBOM must identify its creator (vendor or tool)".to_string(),
                element: None,
                requirement: "EO 14028 Sec 4(e): SBOM creator identification".to_string(),
            });
        }

        // Sec 4(e) — Component identification with unique identifiers
        let total = sbom.components.len();
        let without_id = sbom
            .components
            .values()
            .filter(|c| {
                c.identifiers.purl.is_none()
                    && c.identifiers.cpe.is_empty()
                    && c.identifiers.swid.is_none()
            })
            .count();
        if without_id > 0 {
            violations.push(Violation {
                severity: ViolationSeverity::Error,
                category: ViolationCategory::ComponentIdentification,
                message: format!(
                    "{without_id}/{total} components missing unique identifier (PURL/CPE/SWID)"
                ),
                element: None,
                requirement: "EO 14028 Sec 4(e): Component unique identification".to_string(),
            });
        }

        // Sec 4(e) — Dependency relationships
        if sbom.components.len() > 1 && sbom.edges.is_empty() {
            violations.push(Violation {
                severity: ViolationSeverity::Error,
                category: ViolationCategory::DependencyInfo,
                message: "SBOM with multiple components must include dependency relationships"
                    .to_string(),
                element: None,
                requirement: "EO 14028 Sec 4(e): Dependency relationships".to_string(),
            });
        }

        // Sec 4(e) — Version information
        let without_version = sbom
            .components
            .values()
            .filter(|c| c.version.is_none())
            .count();
        if without_version > 0 {
            violations.push(Violation {
                severity: ViolationSeverity::Error,
                category: ViolationCategory::ComponentIdentification,
                message: format!(
                    "{without_version}/{total} components missing version information"
                ),
                element: None,
                requirement: "EO 14028 Sec 4(e): Component version".to_string(),
            });
        }

        // Sec 4(e) — Cryptographic hashes for integrity
        let without_hash = sbom
            .components
            .values()
            .filter(|c| c.hashes.is_empty())
            .count();
        if without_hash > 0 {
            violations.push(Violation {
                severity: ViolationSeverity::Warning,
                category: ViolationCategory::IntegrityInfo,
                message: format!("{without_hash}/{total} components missing cryptographic hashes"),
                element: None,
                requirement: "EO 14028 Sec 4(e): Component integrity verification".to_string(),
            });
        }

        // Sec 4(g) — Vulnerability disclosure
        let has_security_ref = sbom.document.security_contact.is_some()
            || sbom.document.vulnerability_disclosure_url.is_some()
            || sbom.components.values().any(|comp| {
                comp.external_refs.iter().any(|r| {
                    matches!(
                        r.ref_type,
                        ExternalRefType::SecurityContact | ExternalRefType::Advisories
                    )
                })
            });
        if !has_security_ref {
            violations.push(Violation {
                severity: ViolationSeverity::Warning,
                category: ViolationCategory::SecurityInfo,
                message: "No security contact or vulnerability disclosure reference found"
                    .to_string(),
                element: None,
                requirement: "EO 14028 Sec 4(g): Vulnerability disclosure process".to_string(),
            });
        }

        // Sec 4(e) — Supplier identification
        let without_supplier = sbom
            .components
            .values()
            .filter(|c| c.supplier.is_none() && c.author.is_none())
            .count();
        if without_supplier > 0 {
            let pct = (without_supplier * 100) / total.max(1);
            if pct > 30 {
                violations.push(Violation {
                    severity: ViolationSeverity::Warning,
                    category: ViolationCategory::SupplierInfo,
                    message: format!(
                        "{without_supplier}/{total} components ({pct}%) missing supplier information"
                    ),
                    element: None,
                    requirement: "EO 14028 Sec 4(e): Supplier identification".to_string(),
                });
            }
        }
    }

    fn check_format_specific(&self, sbom: &NormalizedSbom, violations: &mut Vec<Violation>) {
        match sbom.document.format {
            SbomFormat::CycloneDx => {
                self.check_cyclonedx_specific(sbom, violations);
            }
            SbomFormat::Spdx => {
                self.check_spdx_specific(sbom, violations);
            }
        }
    }

    fn check_cyclonedx_specific(&self, sbom: &NormalizedSbom, violations: &mut Vec<Violation>) {
        // CycloneDX specific checks
        let version = &sbom.document.spec_version;

        // Warn about older versions
        if version.starts_with("1.3") || version.starts_with("1.2") || version.starts_with("1.1") {
            violations.push(Violation {
                severity: ViolationSeverity::Info,
                category: ViolationCategory::FormatSpecific,
                message: format!("CycloneDX {version} is outdated, consider upgrading to 1.7+"),
                element: None,
                requirement: "Current CycloneDX version".to_string(),
            });
        }

        // Check for bom-ref on components (important for CycloneDX)
        for comp in sbom.components.values() {
            if comp.identifiers.format_id == comp.name {
                // Likely missing bom-ref
                violations.push(Violation {
                    severity: ViolationSeverity::Info,
                    category: ViolationCategory::FormatSpecific,
                    message: format!("Component '{}' may be missing bom-ref", comp.name),
                    element: Some(comp.name.clone()),
                    requirement: "CycloneDX: bom-ref for dependency tracking".to_string(),
                });
            }
        }
    }

    fn check_spdx_specific(&self, sbom: &NormalizedSbom, violations: &mut Vec<Violation>) {
        // SPDX specific checks
        let version = &sbom.document.spec_version;

        // Check version
        if !version.starts_with("2.") && !version.starts_with("3.") {
            violations.push(Violation {
                severity: ViolationSeverity::Warning,
                category: ViolationCategory::FormatSpecific,
                message: format!("Unknown SPDX version: {version}"),
                element: None,
                requirement: "Valid SPDX version".to_string(),
            });
        }

        // SPDX requires element identifiers
        // SPDX 2.x uses SPDXRef- prefix; SPDX 3.0 uses URN-style IDs (e.g., urn:spdx:...)
        let is_spdx3 = version.starts_with("3.");
        for comp in sbom.components.values() {
            let valid_id = if is_spdx3 {
                // SPDX 3.0 uses URN/IRI identifiers
                comp.identifiers.format_id.contains(':')
            } else {
                comp.identifiers.format_id.starts_with("SPDXRef-")
            };
            if !valid_id {
                let expected = if is_spdx3 {
                    "SPDX 3.0: URN/IRI identifier format"
                } else {
                    "SPDX 2.x: SPDXRef- identifier format"
                };
                violations.push(Violation {
                    severity: ViolationSeverity::Info,
                    category: ViolationCategory::FormatSpecific,
                    message: format!(
                        "Component '{}' has non-standard SPDX identifier format",
                        comp.name
                    ),
                    element: Some(comp.name.clone()),
                    requirement: expected.to_string(),
                });
            }
        }
    }
}

impl Default for ComplianceChecker {
    fn default() -> Self {
        Self::new(ComplianceLevel::Standard)
    }
}

/// Simple email format validation (checks basic structure, not full RFC 5322)
fn is_valid_email_format(email: &str) -> bool {
    // Basic checks: contains @, has local and domain parts, no spaces
    if email.contains(' ') || email.is_empty() {
        return false;
    }

    let parts: Vec<&str> = email.split('@').collect();
    if parts.len() != 2 {
        return false;
    }

    let local = parts[0];
    let domain = parts[1];

    // Local part must not be empty
    if local.is_empty() {
        return false;
    }

    // Domain must contain at least one dot and not start/end with dot
    if domain.is_empty()
        || !domain.contains('.')
        || domain.starts_with('.')
        || domain.ends_with('.')
    {
        return false;
    }

    true
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_compliance_level_names() {
        assert_eq!(ComplianceLevel::Minimum.name(), "Minimum");
        assert_eq!(ComplianceLevel::NtiaMinimum.name(), "NTIA Minimum Elements");
        assert_eq!(ComplianceLevel::CraPhase1.name(), "EU CRA Phase 1 (2027)");
        assert_eq!(ComplianceLevel::CraPhase2.name(), "EU CRA Phase 2 (2029)");
        assert_eq!(ComplianceLevel::NistSsdf.name(), "NIST SSDF (SP 800-218)");
        assert_eq!(ComplianceLevel::Eo14028.name(), "EO 14028 Section 4");
    }

    #[test]
    fn test_nist_ssdf_empty_sbom() {
        let sbom = NormalizedSbom::default();
        let checker = ComplianceChecker::new(ComplianceLevel::NistSsdf);
        let result = checker.check(&sbom);
        // Empty SBOM should have at least a creator violation
        assert!(
            result
                .violations
                .iter()
                .any(|v| v.requirement.contains("PS.1"))
        );
    }

    #[test]
    fn test_eo14028_empty_sbom() {
        let sbom = NormalizedSbom::default();
        let checker = ComplianceChecker::new(ComplianceLevel::Eo14028);
        let result = checker.check(&sbom);
        assert!(
            result
                .violations
                .iter()
                .any(|v| v.requirement.contains("EO 14028"))
        );
    }

    #[test]
    fn test_compliance_result_counts() {
        let violations = vec![
            Violation {
                severity: ViolationSeverity::Error,
                category: ViolationCategory::ComponentIdentification,
                message: "Error 1".to_string(),
                element: None,
                requirement: "Test".to_string(),
            },
            Violation {
                severity: ViolationSeverity::Warning,
                category: ViolationCategory::LicenseInfo,
                message: "Warning 1".to_string(),
                element: None,
                requirement: "Test".to_string(),
            },
            Violation {
                severity: ViolationSeverity::Info,
                category: ViolationCategory::FormatSpecific,
                message: "Info 1".to_string(),
                element: None,
                requirement: "Test".to_string(),
            },
        ];

        let result = ComplianceResult::new(ComplianceLevel::Standard, violations);
        assert!(!result.is_compliant);
        assert_eq!(result.error_count, 1);
        assert_eq!(result.warning_count, 1);
        assert_eq!(result.info_count, 1);
    }
}
