//! PDF Conformance Validation
//!
//! This module provides validation for PDF/A, PDF/X, and PDF 2.0 conformance.
//! PDF/A is for archival, PDF/X is for print exchange, PDF 2.0 is the latest standard.

use std::ffi::{CString, c_char, c_int};
use std::sync::LazyLock;

use crate::ffi::{Handle, HandleStore};

// ============================================================================
// Handle Management
// ============================================================================

/// Handle store for conformance validators
static VALIDATORS: LazyLock<HandleStore<ConformanceValidator>> = LazyLock::new(HandleStore::new);

/// Handle store for validation results
static VALIDATION_RESULTS: LazyLock<HandleStore<ValidationResult>> =
    LazyLock::new(HandleStore::new);

// ============================================================================
// Conformance Levels
// ============================================================================

/// PDF/A conformance levels
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PdfALevel {
    /// Not PDF/A
    None = 0,
    /// PDF/A-1a (Level A conformance, ISO 19005-1)
    A1a = 1,
    /// PDF/A-1b (Level B conformance, ISO 19005-1)
    A1b = 2,
    /// PDF/A-2a (Level A conformance, ISO 19005-2)
    A2a = 3,
    /// PDF/A-2b (Level B conformance, ISO 19005-2)
    A2b = 4,
    /// PDF/A-2u (Level U conformance, ISO 19005-2)
    A2u = 5,
    /// PDF/A-3a (Level A conformance, ISO 19005-3)
    A3a = 6,
    /// PDF/A-3b (Level B conformance, ISO 19005-3)
    A3b = 7,
    /// PDF/A-3u (Level U conformance, ISO 19005-3)
    A3u = 8,
    /// PDF/A-4 (ISO 19005-4)
    A4 = 9,
    /// PDF/A-4e (engineering, ISO 19005-4)
    A4e = 10,
    /// PDF/A-4f (file attachments, ISO 19005-4)
    A4f = 11,
}

impl PdfALevel {
    /// Get the ISO standard number
    pub fn iso_standard(&self) -> &'static str {
        match self {
            PdfALevel::None => "None",
            PdfALevel::A1a | PdfALevel::A1b => "ISO 19005-1",
            PdfALevel::A2a | PdfALevel::A2b | PdfALevel::A2u => "ISO 19005-2",
            PdfALevel::A3a | PdfALevel::A3b | PdfALevel::A3u => "ISO 19005-3",
            PdfALevel::A4 | PdfALevel::A4e | PdfALevel::A4f => "ISO 19005-4",
        }
    }

    /// Get short name (e.g., "PDF/A-1a")
    pub fn short_name(&self) -> &'static str {
        match self {
            PdfALevel::None => "None",
            PdfALevel::A1a => "PDF/A-1a",
            PdfALevel::A1b => "PDF/A-1b",
            PdfALevel::A2a => "PDF/A-2a",
            PdfALevel::A2b => "PDF/A-2b",
            PdfALevel::A2u => "PDF/A-2u",
            PdfALevel::A3a => "PDF/A-3a",
            PdfALevel::A3b => "PDF/A-3b",
            PdfALevel::A3u => "PDF/A-3u",
            PdfALevel::A4 => "PDF/A-4",
            PdfALevel::A4e => "PDF/A-4e",
            PdfALevel::A4f => "PDF/A-4f",
        }
    }
}

/// PDF/X conformance levels
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PdfXLevel {
    /// Not PDF/X
    None = 0,
    /// PDF/X-1a:2001 (ISO 15930-1)
    X1a2001 = 1,
    /// PDF/X-1a:2003 (ISO 15930-4)
    X1a2003 = 2,
    /// PDF/X-3:2002 (ISO 15930-3)
    X32002 = 3,
    /// PDF/X-3:2003 (ISO 15930-6)
    X32003 = 4,
    /// PDF/X-4 (ISO 15930-7)
    X4 = 5,
    /// PDF/X-4p (ISO 15930-7)
    X4p = 6,
    /// PDF/X-5g (ISO 15930-8)
    X5g = 7,
    /// PDF/X-5n (ISO 15930-8)
    X5n = 8,
    /// PDF/X-5pg (ISO 15930-8)
    X5pg = 9,
    /// PDF/X-6 (ISO 15930-9)
    X6 = 10,
    /// PDF/X-6n (ISO 15930-9)
    X6n = 11,
    /// PDF/X-6p (ISO 15930-9)
    X6p = 12,
}

impl PdfXLevel {
    /// Get the ISO standard number
    pub fn iso_standard(&self) -> &'static str {
        match self {
            PdfXLevel::None => "None",
            PdfXLevel::X1a2001 => "ISO 15930-1",
            PdfXLevel::X1a2003 => "ISO 15930-4",
            PdfXLevel::X32002 => "ISO 15930-3",
            PdfXLevel::X32003 => "ISO 15930-6",
            PdfXLevel::X4 | PdfXLevel::X4p => "ISO 15930-7",
            PdfXLevel::X5g | PdfXLevel::X5n | PdfXLevel::X5pg => "ISO 15930-8",
            PdfXLevel::X6 | PdfXLevel::X6n | PdfXLevel::X6p => "ISO 15930-9",
        }
    }

    /// Get short name
    pub fn short_name(&self) -> &'static str {
        match self {
            PdfXLevel::None => "None",
            PdfXLevel::X1a2001 => "PDF/X-1a:2001",
            PdfXLevel::X1a2003 => "PDF/X-1a:2003",
            PdfXLevel::X32002 => "PDF/X-3:2002",
            PdfXLevel::X32003 => "PDF/X-3:2003",
            PdfXLevel::X4 => "PDF/X-4",
            PdfXLevel::X4p => "PDF/X-4p",
            PdfXLevel::X5g => "PDF/X-5g",
            PdfXLevel::X5n => "PDF/X-5n",
            PdfXLevel::X5pg => "PDF/X-5pg",
            PdfXLevel::X6 => "PDF/X-6",
            PdfXLevel::X6n => "PDF/X-6n",
            PdfXLevel::X6p => "PDF/X-6p",
        }
    }
}

/// PDF version levels
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PdfVersion {
    /// Unknown or invalid
    Unknown = 0,
    /// PDF 1.0
    V1_0 = 10,
    /// PDF 1.1
    V1_1 = 11,
    /// PDF 1.2
    V1_2 = 12,
    /// PDF 1.3
    V1_3 = 13,
    /// PDF 1.4
    V1_4 = 14,
    /// PDF 1.5
    V1_5 = 15,
    /// PDF 1.6
    V1_6 = 16,
    /// PDF 1.7 (ISO 32000-1)
    V1_7 = 17,
    /// PDF 2.0 (ISO 32000-2)
    V2_0 = 20,
}

impl PdfVersion {
    /// Get version string
    pub fn version_string(&self) -> &'static str {
        match self {
            PdfVersion::Unknown => "Unknown",
            PdfVersion::V1_0 => "1.0",
            PdfVersion::V1_1 => "1.1",
            PdfVersion::V1_2 => "1.2",
            PdfVersion::V1_3 => "1.3",
            PdfVersion::V1_4 => "1.4",
            PdfVersion::V1_5 => "1.5",
            PdfVersion::V1_6 => "1.6",
            PdfVersion::V1_7 => "1.7",
            PdfVersion::V2_0 => "2.0",
        }
    }
}

// ============================================================================
// Validation Issue
// ============================================================================

/// Severity of a validation issue
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IssueSeverity {
    /// Informational message
    Info = 0,
    /// Warning (document may work but doesn't strictly conform)
    Warning = 1,
    /// Error (document violates conformance)
    Error = 2,
    /// Fatal error (document is invalid)
    Fatal = 3,
}

/// A single validation issue
#[derive(Debug, Clone)]
pub struct ValidationIssue {
    /// Issue severity
    pub severity: IssueSeverity,
    /// Issue code (for programmatic handling)
    pub code: String,
    /// Human-readable message
    pub message: String,
    /// Page number (0 = document level, >0 = specific page)
    pub page: i32,
    /// Object number (0 = none)
    pub object_num: i32,
    /// Clause in the standard that is violated
    pub clause: Option<String>,
}

impl ValidationIssue {
    /// Create a new validation issue
    pub fn new(
        severity: IssueSeverity,
        code: impl Into<String>,
        message: impl Into<String>,
    ) -> Self {
        Self {
            severity,
            code: code.into(),
            message: message.into(),
            page: 0,
            object_num: 0,
            clause: None,
        }
    }

    /// Set page number
    pub fn with_page(mut self, page: i32) -> Self {
        self.page = page;
        self
    }

    /// Set object number
    pub fn with_object(mut self, object_num: i32) -> Self {
        self.object_num = object_num;
        self
    }

    /// Set clause reference
    pub fn with_clause(mut self, clause: impl Into<String>) -> Self {
        self.clause = Some(clause.into());
        self
    }
}

// ============================================================================
// Validation Result
// ============================================================================

/// Result of conformance validation
#[derive(Debug, Clone)]
pub struct ValidationResult {
    /// PDF version detected
    pub pdf_version: PdfVersion,
    /// PDF/A level claimed
    pub pdfa_claimed: PdfALevel,
    /// PDF/A level validated
    pub pdfa_valid: PdfALevel,
    /// PDF/X level claimed
    pub pdfx_claimed: PdfXLevel,
    /// PDF/X level validated
    pub pdfx_valid: PdfXLevel,
    /// Is PDF 2.0 compliant
    pub pdf2_compliant: bool,
    /// List of validation issues
    pub issues: Vec<ValidationIssue>,
    /// Number of errors
    pub error_count: usize,
    /// Number of warnings
    pub warning_count: usize,
}

impl ValidationResult {
    /// Create a new empty result
    pub fn new() -> Self {
        Self {
            pdf_version: PdfVersion::Unknown,
            pdfa_claimed: PdfALevel::None,
            pdfa_valid: PdfALevel::None,
            pdfx_claimed: PdfXLevel::None,
            pdfx_valid: PdfXLevel::None,
            pdf2_compliant: false,
            issues: Vec::new(),
            error_count: 0,
            warning_count: 0,
        }
    }

    /// Add an issue
    pub fn add_issue(&mut self, issue: ValidationIssue) {
        match issue.severity {
            IssueSeverity::Error | IssueSeverity::Fatal => self.error_count += 1,
            IssueSeverity::Warning => self.warning_count += 1,
            IssueSeverity::Info => {}
        }
        self.issues.push(issue);
    }

    /// Check if validation passed (no errors)
    pub fn is_valid(&self) -> bool {
        self.error_count == 0
    }

    /// Get total issue count
    pub fn issue_count(&self) -> usize {
        self.issues.len()
    }
}

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

// ============================================================================
// Conformance Validator
// ============================================================================

/// Configuration for conformance validation
#[derive(Debug, Clone)]
pub struct ValidatorConfig {
    /// Check PDF/A conformance
    pub check_pdfa: bool,
    /// Check PDF/X conformance
    pub check_pdfx: bool,
    /// Check PDF 2.0 compliance
    pub check_pdf2: bool,
    /// Stop on first error
    pub stop_on_error: bool,
    /// Maximum issues to report
    pub max_issues: usize,
}

impl Default for ValidatorConfig {
    fn default() -> Self {
        Self {
            check_pdfa: true,
            check_pdfx: true,
            check_pdf2: true,
            stop_on_error: false,
            max_issues: 1000,
        }
    }
}

/// PDF conformance validator
pub struct ConformanceValidator {
    /// Configuration
    config: ValidatorConfig,
    /// Current validation result
    result: ValidationResult,
    /// Raw PDF data to validate against (set via set_document)
    pdf_data: Vec<u8>,
}

impl ConformanceValidator {
    /// Create a new validator
    pub fn new(config: ValidatorConfig) -> Self {
        Self {
            config,
            result: ValidationResult::new(),
            pdf_data: Vec::new(),
        }
    }

    /// Set the raw PDF data to validate against.
    pub fn set_document_data(&mut self, data: Vec<u8>) {
        self.pdf_data = data;
    }

    /// Get the PDF data as a lossy string for text-based scanning.
    fn pdf_text(&self) -> String {
        String::from_utf8_lossy(&self.pdf_data).to_string()
    }

    /// Get configuration
    pub fn config(&self) -> &ValidatorConfig {
        &self.config
    }

    /// Get current result
    pub fn result(&self) -> &ValidationResult {
        &self.result
    }

    /// Reset validation state
    pub fn reset(&mut self) {
        self.result = ValidationResult::new();
    }

    /// Add an issue (respecting max_issues limit)
    fn add_issue(&mut self, issue: ValidationIssue) -> bool {
        if self.result.issues.len() >= self.config.max_issues {
            return false;
        }

        let is_error = matches!(issue.severity, IssueSeverity::Error | IssueSeverity::Fatal);
        self.result.add_issue(issue);

        // Return whether to continue
        !is_error || !self.config.stop_on_error
    }

    // ========================================================================
    // PDF/A Validation
    // ========================================================================

    /// Validate PDF/A conformance
    pub fn validate_pdfa(&mut self) {
        if !self.config.check_pdfa {
            return;
        }

        // First detect if the document claims PDF/A conformance
        self.check_pdfa_metadata();

        // Only run PDF/A-specific checks when a claim is detected
        if self.result.pdfa_claimed != PdfALevel::None {
            self.check_embedded_fonts();
            self.check_transparency();
            self.check_encryption_pdfa();
            self.check_javascript_pdfa();
            self.check_xmp_metadata();
            self.check_colorspaces_pdfa();

            // Determine validated level
            if self.result.error_count == 0 {
                self.result.pdfa_valid = self.result.pdfa_claimed;
            }
        }
    }

    fn check_pdfa_metadata(&mut self) {
        let text = self.pdf_text();

        // Look for XMP metadata containing PDF/A identification
        let has_pdfaid_part = text.contains("pdfaid:part") || text.contains("pdfaid:Part");
        let has_pdfaid_conf =
            text.contains("pdfaid:conformance") || text.contains("pdfaid:Conformance");

        if !has_pdfaid_part && !has_pdfaid_conf {
            // No PDF/A identification found at all; nothing to claim
            return;
        }

        // Try to extract the part number
        if has_pdfaid_part {
            if let Some(part) = Self::extract_xmp_value(&text, "pdfaid:part")
                .or_else(|| Self::extract_xmp_value(&text, "pdfaid:Part"))
            {
                let conformance = Self::extract_xmp_value(&text, "pdfaid:conformance")
                    .or_else(|| Self::extract_xmp_value(&text, "pdfaid:Conformance"))
                    .unwrap_or_default()
                    .to_uppercase();

                self.result.pdfa_claimed = match (part.as_str(), conformance.as_str()) {
                    ("1", "A") => PdfALevel::A1a,
                    ("1", "B") | ("1", _) => PdfALevel::A1b,
                    ("2", "A") => PdfALevel::A2a,
                    ("2", "B") => PdfALevel::A2b,
                    ("2", "U") | ("2", _) => PdfALevel::A2u,
                    ("3", "A") => PdfALevel::A3a,
                    ("3", "B") => PdfALevel::A3b,
                    ("3", "U") | ("3", _) => PdfALevel::A3u,
                    ("4", _) => PdfALevel::A4,
                    _ => PdfALevel::None,
                };
            }
        }

        if !has_pdfaid_conf {
            self.add_issue(
                ValidationIssue::new(
                    IssueSeverity::Warning,
                    "PDFA_CONFORMANCE_MISSING",
                    "PDF/A conformance level (pdfaid:conformance) is missing from XMP metadata",
                )
                .with_clause("6.6.2"),
            );
        }
    }

    fn check_embedded_fonts(&mut self) {
        let text = self.pdf_text();

        // Scan for /Type /Font entries
        let font_pattern = "/Type /Font";
        let mut pos = 0;
        while let Some(found) = text[pos..].find(font_pattern) {
            let abs_pos = pos + found;
            pos = abs_pos + font_pattern.len();

            // Search a window around the font definition for embedding indicators
            let window_start = abs_pos.saturating_sub(200);
            let window_end = (abs_pos + 500).min(text.len());
            let window = &text[window_start..window_end];

            // Check if this font has an embedded program
            let has_fontfile = window.contains("/FontFile")
                || window.contains("/FontFile2")
                || window.contains("/FontFile3");

            // Check if it's a base font reference (Type1 standard 14 fonts are
            // sometimes allowed unembedded, but PDF/A requires all embedding)
            if !has_fontfile {
                // Extract font name if possible
                let font_name = if let Some(bf_pos) = window.find("/BaseFont") {
                    let after = &window[bf_pos + 9..];
                    let trimmed = after.trim_start();
                    if trimmed.starts_with('/') {
                        let end = trimmed[1..]
                            .find(|c: char| c.is_whitespace() || c == '/' || c == '>')
                            .map(|i| i + 1)
                            .unwrap_or(trimmed.len());
                        trimmed[1..end].to_string()
                    } else {
                        "unknown".to_string()
                    }
                } else {
                    "unknown".to_string()
                };

                self.add_issue(
                    ValidationIssue::new(
                        IssueSeverity::Error,
                        "FONT_NOT_EMBEDDED",
                        format!(
                            "Font '{}' is not embedded (missing FontFile/FontFile2/FontFile3)",
                            font_name
                        ),
                    )
                    .with_clause("6.3.5"),
                );
            }
        }
    }

    fn check_transparency(&mut self) {
        let text = self.pdf_text();

        // PDF/A-1 forbids transparency; PDF/A-2+ allows it
        let is_pdfa1 = matches!(self.result.pdfa_claimed, PdfALevel::A1a | PdfALevel::A1b);

        if !is_pdfa1 {
            return;
        }

        // Check for soft masks
        if text.contains("/SMask") {
            // /SMask /None is acceptable
            let has_real_smask = text.contains("/SMask") && {
                // Check if any /SMask is followed by something other than /None
                let mut found_real = false;
                let mut search_pos = 0;
                while let Some(p) = text[search_pos..].find("/SMask") {
                    let after = &text[search_pos + p + 6..];
                    let trimmed = after.trim_start();
                    if !trimmed.starts_with("/None") {
                        found_real = true;
                        break;
                    }
                    search_pos += p + 6;
                }
                found_real
            };
            if has_real_smask {
                self.add_issue(
                    ValidationIssue::new(
                        IssueSeverity::Error,
                        "TRANSPARENCY_SMASK",
                        "Soft mask (/SMask) is forbidden in PDF/A-1",
                    )
                    .with_clause("6.4"),
                );
            }
        }

        // Check for non-Normal blend modes
        let blend_modes = [
            "Multiply",
            "Screen",
            "Overlay",
            "Darken",
            "Lighten",
            "ColorDodge",
            "ColorBurn",
            "HardLight",
            "SoftLight",
            "Difference",
            "Exclusion",
        ];
        for bm in &blend_modes {
            let pattern = format!("/BM /{}", bm);
            if text.contains(&pattern) {
                self.add_issue(
                    ValidationIssue::new(
                        IssueSeverity::Error,
                        "TRANSPARENCY_BLEND_MODE",
                        format!("Non-normal blend mode /BM /{} is forbidden in PDF/A-1", bm),
                    )
                    .with_clause("6.4"),
                );
                break;
            }
        }

        // Check for non-1.0 opacity
        for key in &["/ca ", "/CA "] {
            if let Some(pos) = text.find(key) {
                let after = text[pos + key.len()..].trim_start();
                let end = after
                    .find(|c: char| c.is_whitespace() || c == '/')
                    .unwrap_or(after.len());
                if let Ok(val) = after[..end].parse::<f64>() {
                    if (val - 1.0).abs() > f64::EPSILON {
                        self.add_issue(
                            ValidationIssue::new(
                                IssueSeverity::Error,
                                "TRANSPARENCY_OPACITY",
                                format!(
                                    "Non-opaque {} value ({}) is forbidden in PDF/A-1",
                                    key.trim(),
                                    val
                                ),
                            )
                            .with_clause("6.4"),
                        );
                    }
                }
            }
        }
    }

    fn check_encryption_pdfa(&mut self) {
        let text = self.pdf_text();

        if text.contains("/Encrypt") {
            self.add_issue(
                ValidationIssue::new(
                    IssueSeverity::Error,
                    "ENCRYPTION_FORBIDDEN",
                    "Encryption (/Encrypt) is forbidden in PDF/A",
                )
                .with_clause("6.1.3"),
            );
        }
    }

    fn check_javascript_pdfa(&mut self) {
        let text = self.pdf_text();

        if text.contains("/JS") || text.contains("/JavaScript") {
            self.add_issue(
                ValidationIssue::new(
                    IssueSeverity::Error,
                    "JAVASCRIPT_FORBIDDEN",
                    "JavaScript (/JS or /JavaScript) is forbidden in PDF/A",
                )
                .with_clause("6.6.1"),
            );
        }
    }

    fn check_xmp_metadata(&mut self) {
        let text = self.pdf_text();

        // The catalog must contain a /Metadata entry pointing to an XMP stream
        let has_metadata = text.contains("/Metadata");
        let has_xmp = text.contains("<x:xmpmeta") || text.contains("xpacket");

        if !has_metadata {
            self.add_issue(
                ValidationIssue::new(
                    IssueSeverity::Error,
                    "XMP_METADATA_MISSING",
                    "Document catalog does not contain a /Metadata entry (required for PDF/A)",
                )
                .with_clause("6.6.2"),
            );
        } else if !has_xmp {
            self.add_issue(
                ValidationIssue::new(
                    IssueSeverity::Warning,
                    "XMP_PACKET_MISSING",
                    "No XMP metadata packet found despite /Metadata reference",
                )
                .with_clause("6.6.2"),
            );
        }
    }

    fn check_colorspaces_pdfa(&mut self) {
        let text = self.pdf_text();

        // Device-dependent color spaces without an output intent are problematic
        let has_output_intent = text.contains("/OutputIntents");

        let device_dependent = [
            ("/DeviceRGB", "DeviceRGB"),
            ("/DeviceCMYK", "DeviceCMYK"),
            ("/DeviceGray", "DeviceGray"),
        ];

        if !has_output_intent {
            for (pattern, name) in &device_dependent {
                if text.contains(pattern) {
                    self.add_issue(
                        ValidationIssue::new(
                            IssueSeverity::Error,
                            "COLORSPACE_DEVICE_DEPENDENT",
                            format!(
                                "Device-dependent color space {} used without an output intent",
                                name
                            ),
                        )
                        .with_clause("6.2.3"),
                    );
                }
            }
        }
    }

    /// Extract a value from an XMP element like <pdfaid:part>1</pdfaid:part>
    fn extract_xmp_value(text: &str, tag: &str) -> Option<String> {
        let open = format!("<{}>", tag);
        let close = format!("</{}>", tag);
        if let Some(start) = text.find(&open) {
            let val_start = start + open.len();
            if let Some(end) = text[val_start..].find(&close) {
                let value = text[val_start..val_start + end].trim().to_string();
                if !value.is_empty() {
                    return Some(value);
                }
            }
        }
        // Also check attribute form: pdfaid:part="1"
        let attr_pat = format!("{}=\"", tag);
        if let Some(start) = text.find(&attr_pat) {
            let val_start = start + attr_pat.len();
            if let Some(end) = text[val_start..].find('"') {
                let value = text[val_start..val_start + end].trim().to_string();
                if !value.is_empty() {
                    return Some(value);
                }
            }
        }
        None
    }

    // ========================================================================
    // PDF/X Validation
    // ========================================================================

    /// Validate PDF/X conformance
    pub fn validate_pdfx(&mut self) {
        if !self.config.check_pdfx {
            return;
        }

        // First detect if the document claims PDF/X conformance
        self.check_pdfx_metadata();

        // Only run PDF/X-specific checks when a claim is detected
        if self.result.pdfx_claimed != PdfXLevel::None {
            self.check_output_intent();
            self.check_page_boxes();
            self.check_trapped_key();
            self.check_fonts_pdfx();

            // Determine validated level
            if self.result.error_count == 0 {
                self.result.pdfx_valid = self.result.pdfx_claimed;
            }
        }
    }

    fn check_pdfx_metadata(&mut self) {
        let text = self.pdf_text();

        // Check for GTS_PDFXVersion in Info dictionary or XMP
        if text.contains("GTS_PDFXVersion") {
            // Try to extract the version string
            if text.contains("PDF/X-1a:2001") {
                self.result.pdfx_claimed = PdfXLevel::X1a2001;
            } else if text.contains("PDF/X-1a:2003") {
                self.result.pdfx_claimed = PdfXLevel::X1a2003;
            } else if text.contains("PDF/X-3:2002") {
                self.result.pdfx_claimed = PdfXLevel::X32002;
            } else if text.contains("PDF/X-3:2003") {
                self.result.pdfx_claimed = PdfXLevel::X32003;
            } else if text.contains("PDF/X-4") {
                self.result.pdfx_claimed = PdfXLevel::X4;
            } else {
                self.result.pdfx_claimed = PdfXLevel::X1a2001; // Default to earliest
            }
        } else {
            // No PDF/X identification found
            return;
        }
    }

    fn check_output_intent(&mut self) {
        let text = self.pdf_text();

        if !text.contains("/OutputIntents") {
            self.add_issue(
                ValidationIssue::new(
                    IssueSeverity::Error,
                    "PDFX_OUTPUT_INTENT_MISSING",
                    "PDF/X requires /OutputIntents array in the document catalog",
                )
                .with_clause("6.2.2"),
            );
            return;
        }

        // Check for /GTS_PDFX subtype
        if !text.contains("/GTS_PDFX") {
            self.add_issue(
                ValidationIssue::new(
                    IssueSeverity::Warning,
                    "PDFX_OUTPUT_INTENT_SUBTYPE",
                    "OutputIntents should contain an entry with /S /GTS_PDFX",
                )
                .with_clause("6.2.2"),
            );
        }
    }

    fn check_page_boxes(&mut self) {
        let text = self.pdf_text();

        let has_trimbox = text.contains("/TrimBox");
        let has_artbox = text.contains("/ArtBox");

        if !has_trimbox && !has_artbox {
            self.add_issue(
                ValidationIssue::new(
                    IssueSeverity::Error,
                    "PDFX_PAGE_BOX_MISSING",
                    "PDF/X requires either /TrimBox or /ArtBox on each page",
                )
                .with_clause("6.1.3"),
            );
        }

        if !text.contains("/BleedBox") {
            self.add_issue(
                ValidationIssue::new(
                    IssueSeverity::Info,
                    "PDFX_BLEEDBOX_RECOMMENDED",
                    "/BleedBox is recommended for PDF/X documents",
                )
                .with_clause("6.1.3"),
            );
        }
    }

    fn check_trapped_key(&mut self) {
        let text = self.pdf_text();

        if !text.contains("/Trapped") {
            self.add_issue(
                ValidationIssue::new(
                    IssueSeverity::Error,
                    "PDFX_TRAPPED_MISSING",
                    "/Trapped key is required in the Info dictionary for PDF/X",
                )
                .with_clause("6.1.3"),
            );
        } else {
            // Verify the value is one of /True, /False, /Unknown
            let valid_values = [
                "/Trapped /True",
                "/Trapped /False",
                "/Trapped /Unknown",
                "/Trapped/True",
                "/Trapped/False",
                "/Trapped/Unknown",
            ];
            let has_valid_value = valid_values.iter().any(|v| text.contains(v));
            if !has_valid_value {
                self.add_issue(
                    ValidationIssue::new(
                        IssueSeverity::Warning,
                        "PDFX_TRAPPED_INVALID",
                        "/Trapped key must have value /True, /False, or /Unknown",
                    )
                    .with_clause("6.1.3"),
                );
            }
        }
    }

    fn check_fonts_pdfx(&mut self) {
        // PDF/X has the same font embedding requirement as PDF/A
        self.check_embedded_fonts();
    }

    // ========================================================================
    // PDF 2.0 Validation
    // ========================================================================

    /// Validate PDF 2.0 compliance
    pub fn validate_pdf2(&mut self) {
        if !self.config.check_pdf2 {
            return;
        }

        // First detect the PDF version
        self.check_pdf_version();

        // Only run PDF 2.0 specific checks if the version is 2.0
        if self.result.pdf_version == PdfVersion::V2_0 {
            self.check_deprecated_features();
            self.check_pdf2_features();

            // Determine compliance
            if self.result.error_count == 0 {
                self.result.pdf2_compliant = true;
            }
        }
    }

    fn check_pdf_version(&mut self) {
        let data = &self.pdf_data;

        // Parse %PDF-x.y header (first 20 bytes)
        if data.starts_with(b"%PDF-") {
            let header = String::from_utf8_lossy(&data[..data.len().min(20)]);
            if let Some(ver_str) = header.strip_prefix("%PDF-") {
                let ver_end = ver_str
                    .find(|c: char| !c.is_ascii_digit() && c != '.')
                    .unwrap_or(ver_str.len());
                let ver = &ver_str[..ver_end];
                self.result.pdf_version = match ver {
                    "1.0" => PdfVersion::V1_0,
                    "1.1" => PdfVersion::V1_1,
                    "1.2" => PdfVersion::V1_2,
                    "1.3" => PdfVersion::V1_3,
                    "1.4" => PdfVersion::V1_4,
                    "1.5" => PdfVersion::V1_5,
                    "1.6" => PdfVersion::V1_6,
                    "1.7" => PdfVersion::V1_7,
                    "2.0" => PdfVersion::V2_0,
                    _ => PdfVersion::Unknown,
                };
            }
        }

        // Check for /Version in catalog which takes precedence
        let text = self.pdf_text();
        if let Some(pos) = text.find("/Version") {
            let after = &text[pos + 8..];
            let trimmed = after.trim_start();
            if trimmed.starts_with('/') {
                let end = trimmed[1..]
                    .find(|c: char| c.is_whitespace() || c == '/' || c == '>')
                    .map(|i| i + 1)
                    .unwrap_or(trimmed.len());
                let catalog_ver = &trimmed[1..end];
                let parsed = match catalog_ver {
                    "1.0" => Some(PdfVersion::V1_0),
                    "1.1" => Some(PdfVersion::V1_1),
                    "1.2" => Some(PdfVersion::V1_2),
                    "1.3" => Some(PdfVersion::V1_3),
                    "1.4" => Some(PdfVersion::V1_4),
                    "1.5" => Some(PdfVersion::V1_5),
                    "1.6" => Some(PdfVersion::V1_6),
                    "1.7" => Some(PdfVersion::V1_7),
                    "2.0" => Some(PdfVersion::V2_0),
                    _ => None,
                };
                if let Some(ver) = parsed {
                    self.result.pdf_version = ver;
                }
            }
        }
    }

    fn check_deprecated_features(&mut self) {
        let text = self.pdf_text();

        if text.contains("/LZWDecode") {
            self.add_issue(
                ValidationIssue::new(
                    IssueSeverity::Warning,
                    "DEPRECATED_LZWDECODE",
                    "LZWDecode filter is deprecated in PDF 2.0; use FlateDecode instead",
                )
                .with_clause("7.3.4"),
            );
        }

        if text.contains("/ASCII85Decode") {
            self.add_issue(
                ValidationIssue::new(
                    IssueSeverity::Warning,
                    "DEPRECATED_ASCII85",
                    "ASCII85Decode filter is deprecated in PDF 2.0",
                )
                .with_clause("7.3.4"),
            );
        }

        if text.contains("/XFA") {
            self.add_issue(
                ValidationIssue::new(
                    IssueSeverity::Warning,
                    "DEPRECATED_XFA",
                    "XFA forms are deprecated in PDF 2.0",
                )
                .with_clause("12.7.8"),
            );
        }
    }

    fn check_pdf2_features(&mut self) {
        let text = self.pdf_text();

        // Check for AES-256 encryption (PDF 2.0 feature)
        if text.contains("/AESV3") || text.contains("/CFM /AESV3") {
            // AES-256 is a PDF 2.0 feature; note its presence
            self.add_issue(
                ValidationIssue::new(
                    IssueSeverity::Info,
                    "PDF2_AES256",
                    "Document uses AES-256 encryption (PDF 2.0 feature)",
                )
                .with_clause("7.6.2"),
            );
        }

        // Check for page-level output intents
        // In PDF 2.0, individual pages can have their own OutputIntents
        // Look for /OutputIntents inside /Type /Page objects
        let page_pattern = "/Type /Page";
        let mut pos = 0;
        while let Some(found) = text[pos..].find(page_pattern) {
            let abs_pos = pos + found;
            pos = abs_pos + page_pattern.len();

            // Make sure it's not /Type /Pages
            if text.get(abs_pos + page_pattern.len()..abs_pos + page_pattern.len() + 1) == Some("s")
            {
                continue;
            }

            // Check a window after this page definition for page-level OutputIntents
            let window_end = (abs_pos + 1000).min(text.len());
            let window = &text[abs_pos..window_end];
            if window.contains("/OutputIntents") {
                self.add_issue(
                    ValidationIssue::new(
                        IssueSeverity::Info,
                        "PDF2_PAGE_OUTPUT_INTENT",
                        "Document uses page-level output intents (PDF 2.0 feature)",
                    )
                    .with_clause("14.11.5"),
                );
                break;
            }
        }
    }
}

// ============================================================================
// FFI Functions
// ============================================================================

/// Create a new conformance validator
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_conformance_validator(
    _ctx: Handle,
    check_pdfa: c_int,
    check_pdfx: c_int,
    check_pdf2: c_int,
) -> Handle {
    let config = ValidatorConfig {
        check_pdfa: check_pdfa != 0,
        check_pdfx: check_pdfx != 0,
        check_pdf2: check_pdf2 != 0,
        ..Default::default()
    };
    let validator = ConformanceValidator::new(config);
    VALIDATORS.insert(validator)
}

/// Drop a conformance validator
#[unsafe(no_mangle)]
pub extern "C" fn fz_drop_conformance_validator(_ctx: Handle, validator: Handle) {
    VALIDATORS.remove(validator);
}

/// Set the document data for a validator (from a document handle).
///
/// The validator copies the raw PDF bytes so subsequent validation
/// methods can inspect the document structure.
#[unsafe(no_mangle)]
pub extern "C" fn fz_conformance_set_document(_ctx: Handle, validator: Handle, doc: Handle) {
    if let Some(doc_arc) = crate::ffi::DOCUMENTS.get(doc) {
        if let Ok(doc_guard) = doc_arc.lock() {
            let data = doc_guard.data().to_vec();
            if let Some(varc) = VALIDATORS.get(validator) {
                if let Ok(mut v) = varc.lock() {
                    v.set_document_data(data);
                }
            }
        }
    }
}

/// Set raw PDF bytes directly on a validator.
///
/// # Safety
/// Caller must ensure `data` points to valid memory of at least `len` bytes.
#[unsafe(no_mangle)]
pub extern "C" fn fz_conformance_set_data(
    _ctx: Handle,
    validator: Handle,
    data: *const u8,
    len: usize,
) {
    if data.is_null() || len == 0 {
        return;
    }
    let bytes = unsafe { std::slice::from_raw_parts(data, len) }.to_vec();
    if let Some(varc) = VALIDATORS.get(validator) {
        if let Ok(mut v) = varc.lock() {
            v.set_document_data(bytes);
        }
    }
}

/// Reset validator state
#[unsafe(no_mangle)]
pub extern "C" fn fz_conformance_validator_reset(_ctx: Handle, validator: Handle) {
    if let Some(arc) = VALIDATORS.get(validator) {
        if let Ok(mut v) = arc.lock() {
            v.reset();
        }
    }
}

/// Run PDF/A validation
#[unsafe(no_mangle)]
pub extern "C" fn fz_validate_pdfa(_ctx: Handle, validator: Handle) {
    if let Some(arc) = VALIDATORS.get(validator) {
        if let Ok(mut v) = arc.lock() {
            v.validate_pdfa();
        }
    }
}

/// Run PDF/X validation
#[unsafe(no_mangle)]
pub extern "C" fn fz_validate_pdfx(_ctx: Handle, validator: Handle) {
    if let Some(arc) = VALIDATORS.get(validator) {
        if let Ok(mut v) = arc.lock() {
            v.validate_pdfx();
        }
    }
}

/// Run PDF 2.0 validation
#[unsafe(no_mangle)]
pub extern "C" fn fz_validate_pdf2(_ctx: Handle, validator: Handle) {
    if let Some(arc) = VALIDATORS.get(validator) {
        if let Ok(mut v) = arc.lock() {
            v.validate_pdf2();
        }
    }
}

/// Check if validation passed
#[unsafe(no_mangle)]
pub extern "C" fn fz_conformance_is_valid(_ctx: Handle, validator: Handle) -> c_int {
    if let Some(arc) = VALIDATORS.get(validator) {
        if let Ok(v) = arc.lock() {
            return if v.result().is_valid() { 1 } else { 0 };
        }
    }
    0
}

/// Get error count
#[unsafe(no_mangle)]
pub extern "C" fn fz_conformance_error_count(_ctx: Handle, validator: Handle) -> c_int {
    if let Some(arc) = VALIDATORS.get(validator) {
        if let Ok(v) = arc.lock() {
            return v.result().error_count as c_int;
        }
    }
    0
}

/// Get warning count
#[unsafe(no_mangle)]
pub extern "C" fn fz_conformance_warning_count(_ctx: Handle, validator: Handle) -> c_int {
    if let Some(arc) = VALIDATORS.get(validator) {
        if let Ok(v) = arc.lock() {
            return v.result().warning_count as c_int;
        }
    }
    0
}

/// Get total issue count
#[unsafe(no_mangle)]
pub extern "C" fn fz_conformance_issue_count(_ctx: Handle, validator: Handle) -> c_int {
    if let Some(arc) = VALIDATORS.get(validator) {
        if let Ok(v) = arc.lock() {
            return v.result().issue_count() as c_int;
        }
    }
    0
}

/// Get PDF/A claimed level
#[unsafe(no_mangle)]
pub extern "C" fn fz_conformance_pdfa_claimed(_ctx: Handle, validator: Handle) -> c_int {
    if let Some(arc) = VALIDATORS.get(validator) {
        if let Ok(v) = arc.lock() {
            return v.result().pdfa_claimed as c_int;
        }
    }
    0
}

/// Get PDF/A validated level
#[unsafe(no_mangle)]
pub extern "C" fn fz_conformance_pdfa_valid(_ctx: Handle, validator: Handle) -> c_int {
    if let Some(arc) = VALIDATORS.get(validator) {
        if let Ok(v) = arc.lock() {
            return v.result().pdfa_valid as c_int;
        }
    }
    0
}

/// Get PDF/X claimed level
#[unsafe(no_mangle)]
pub extern "C" fn fz_conformance_pdfx_claimed(_ctx: Handle, validator: Handle) -> c_int {
    if let Some(arc) = VALIDATORS.get(validator) {
        if let Ok(v) = arc.lock() {
            return v.result().pdfx_claimed as c_int;
        }
    }
    0
}

/// Get PDF/X validated level
#[unsafe(no_mangle)]
pub extern "C" fn fz_conformance_pdfx_valid(_ctx: Handle, validator: Handle) -> c_int {
    if let Some(arc) = VALIDATORS.get(validator) {
        if let Ok(v) = arc.lock() {
            return v.result().pdfx_valid as c_int;
        }
    }
    0
}

/// Check if PDF 2.0 compliant
#[unsafe(no_mangle)]
pub extern "C" fn fz_conformance_pdf2_compliant(_ctx: Handle, validator: Handle) -> c_int {
    if let Some(arc) = VALIDATORS.get(validator) {
        if let Ok(v) = arc.lock() {
            return if v.result().pdf2_compliant { 1 } else { 0 };
        }
    }
    0
}

/// Get PDF version
#[unsafe(no_mangle)]
pub extern "C" fn fz_conformance_pdf_version(_ctx: Handle, validator: Handle) -> c_int {
    if let Some(arc) = VALIDATORS.get(validator) {
        if let Ok(v) = arc.lock() {
            return v.result().pdf_version as c_int;
        }
    }
    0
}

/// Create a new validation result
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_validation_result(_ctx: Handle) -> Handle {
    let result = ValidationResult::new();
    VALIDATION_RESULTS.insert(result)
}

/// Drop a validation result
#[unsafe(no_mangle)]
pub extern "C" fn fz_drop_validation_result(_ctx: Handle, result: Handle) {
    VALIDATION_RESULTS.remove(result);
}

/// Get issue message (returns allocated string)
#[unsafe(no_mangle)]
pub extern "C" fn fz_validation_issue_message(
    _ctx: Handle,
    validator: Handle,
    index: c_int,
) -> *mut c_char {
    if let Some(arc) = VALIDATORS.get(validator) {
        if let Ok(v) = arc.lock() {
            if let Some(issue) = v.result().issues.get(index as usize) {
                if let Ok(s) = CString::new(issue.message.as_str()) {
                    return s.into_raw();
                }
            }
        }
    }
    std::ptr::null_mut()
}

/// Get issue code (returns allocated string)
#[unsafe(no_mangle)]
pub extern "C" fn fz_validation_issue_code(
    _ctx: Handle,
    validator: Handle,
    index: c_int,
) -> *mut c_char {
    if let Some(arc) = VALIDATORS.get(validator) {
        if let Ok(v) = arc.lock() {
            if let Some(issue) = v.result().issues.get(index as usize) {
                if let Ok(s) = CString::new(issue.code.as_str()) {
                    return s.into_raw();
                }
            }
        }
    }
    std::ptr::null_mut()
}

/// Get issue severity
#[unsafe(no_mangle)]
pub extern "C" fn fz_validation_issue_severity(
    _ctx: Handle,
    validator: Handle,
    index: c_int,
) -> c_int {
    if let Some(arc) = VALIDATORS.get(validator) {
        if let Ok(v) = arc.lock() {
            if let Some(issue) = v.result().issues.get(index as usize) {
                return issue.severity as c_int;
            }
        }
    }
    -1
}

/// Free a validation string
#[unsafe(no_mangle)]
pub extern "C" fn fz_free_validation_string(_ctx: Handle, s: *mut c_char) {
    if !s.is_null() {
        unsafe {
            drop(CString::from_raw(s));
        }
    }
}

/// Get PDF/A level name (returns static string)
#[unsafe(no_mangle)]
pub extern "C" fn fz_pdfa_level_name(level: c_int) -> *const c_char {
    let level = match level {
        0 => PdfALevel::None,
        1 => PdfALevel::A1a,
        2 => PdfALevel::A1b,
        3 => PdfALevel::A2a,
        4 => PdfALevel::A2b,
        5 => PdfALevel::A2u,
        6 => PdfALevel::A3a,
        7 => PdfALevel::A3b,
        8 => PdfALevel::A3u,
        9 => PdfALevel::A4,
        10 => PdfALevel::A4e,
        11 => PdfALevel::A4f,
        _ => PdfALevel::None,
    };
    level.short_name().as_ptr() as *const c_char
}

/// Get PDF/X level name (returns static string)
#[unsafe(no_mangle)]
pub extern "C" fn fz_pdfx_level_name(level: c_int) -> *const c_char {
    let level = match level {
        0 => PdfXLevel::None,
        1 => PdfXLevel::X1a2001,
        2 => PdfXLevel::X1a2003,
        3 => PdfXLevel::X32002,
        4 => PdfXLevel::X32003,
        5 => PdfXLevel::X4,
        6 => PdfXLevel::X4p,
        7 => PdfXLevel::X5g,
        8 => PdfXLevel::X5n,
        9 => PdfXLevel::X5pg,
        10 => PdfXLevel::X6,
        11 => PdfXLevel::X6n,
        12 => PdfXLevel::X6p,
        _ => PdfXLevel::None,
    };
    level.short_name().as_ptr() as *const c_char
}

// ============================================================================
// Tests
// ============================================================================

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

    #[test]
    fn test_pdfa_levels() {
        assert_eq!(PdfALevel::A1a.short_name(), "PDF/A-1a");
        assert_eq!(PdfALevel::A1a.iso_standard(), "ISO 19005-1");
        assert_eq!(PdfALevel::A2b.short_name(), "PDF/A-2b");
        assert_eq!(PdfALevel::A2b.iso_standard(), "ISO 19005-2");
        assert_eq!(PdfALevel::A4.short_name(), "PDF/A-4");
        assert_eq!(PdfALevel::A4.iso_standard(), "ISO 19005-4");
    }

    #[test]
    fn test_pdfx_levels() {
        assert_eq!(PdfXLevel::X1a2001.short_name(), "PDF/X-1a:2001");
        assert_eq!(PdfXLevel::X1a2001.iso_standard(), "ISO 15930-1");
        assert_eq!(PdfXLevel::X4.short_name(), "PDF/X-4");
        assert_eq!(PdfXLevel::X4.iso_standard(), "ISO 15930-7");
    }

    #[test]
    fn test_pdf_versions() {
        assert_eq!(PdfVersion::V1_7.version_string(), "1.7");
        assert_eq!(PdfVersion::V2_0.version_string(), "2.0");
    }

    #[test]
    fn test_validation_issue() {
        let issue = ValidationIssue::new(IssueSeverity::Error, "TEST_ERROR", "Test error message")
            .with_page(1)
            .with_object(42)
            .with_clause("6.1.2");

        assert_eq!(issue.severity, IssueSeverity::Error);
        assert_eq!(issue.code, "TEST_ERROR");
        assert_eq!(issue.page, 1);
        assert_eq!(issue.object_num, 42);
        assert_eq!(issue.clause, Some("6.1.2".to_string()));
    }

    #[test]
    fn test_validation_result() {
        let mut result = ValidationResult::new();
        assert!(result.is_valid());
        assert_eq!(result.error_count, 0);

        result.add_issue(ValidationIssue::new(
            IssueSeverity::Warning,
            "WARN1",
            "Warning",
        ));
        assert!(result.is_valid());
        assert_eq!(result.warning_count, 1);

        result.add_issue(ValidationIssue::new(IssueSeverity::Error, "ERR1", "Error"));
        assert!(!result.is_valid());
        assert_eq!(result.error_count, 1);
    }

    #[test]
    fn test_validator_config() {
        let config = ValidatorConfig::default();
        assert!(config.check_pdfa);
        assert!(config.check_pdfx);
        assert!(config.check_pdf2);
        assert!(!config.stop_on_error);
    }

    #[test]
    fn test_conformance_validator() {
        let config = ValidatorConfig::default();
        let mut validator = ConformanceValidator::new(config);

        validator.validate_pdfa();
        validator.validate_pdfx();
        validator.validate_pdf2();

        // Since we don't have a real document, validation should pass
        assert!(validator.result().is_valid());
    }

    #[test]
    fn test_validator_ffi() {
        let handle = fz_new_conformance_validator(0, 1, 1, 1);
        assert!(handle != 0);

        fz_validate_pdfa(0, handle);
        fz_validate_pdfx(0, handle);
        fz_validate_pdf2(0, handle);

        let is_valid = fz_conformance_is_valid(0, handle);
        assert_eq!(is_valid, 1);

        let error_count = fz_conformance_error_count(0, handle);
        assert_eq!(error_count, 0);

        fz_drop_conformance_validator(0, handle);
    }

    #[test]
    fn test_validator_reset() {
        let handle = fz_new_conformance_validator(0, 1, 1, 1);

        fz_validate_pdfa(0, handle);
        fz_conformance_validator_reset(0, handle);

        let issue_count = fz_conformance_issue_count(0, handle);
        assert_eq!(issue_count, 0);

        fz_drop_conformance_validator(0, handle);
    }
}
