//! PDF compliance validation and transparency flattening
//!
//! This module provides validation for PDF/A, PDF/X, and PDF/UA standards,
//! plus a stub for transparency flattening (which requires a full rendering pipeline).

use crate::fitz::error::{Error, Result};

// ============================================================================
// PdfStandard
// ============================================================================

/// PDF standard that a document may conform to.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PdfStandard {
    /// PDF/A-1a (Level A conformance, ISO 19005-1)
    PdfA1a,
    /// PDF/A-1b (Level B conformance, ISO 19005-1)
    PdfA1b,
    /// PDF/A-2a (Level A conformance, ISO 19005-2)
    PdfA2a,
    /// PDF/A-2b (Level B conformance, ISO 19005-2)
    PdfA2b,
    /// PDF/A-2u (Level U conformance, ISO 19005-2)
    PdfA2u,
    /// PDF/A-3a (Level A conformance, ISO 19005-3)
    PdfA3a,
    /// PDF/A-3b (Level B conformance, ISO 19005-3)
    PdfA3b,
    /// PDF/A-3u (Level U conformance, ISO 19005-3)
    PdfA3u,
    /// PDF/X-1a (ISO 15930-1)
    PdfX1a,
    /// PDF/X-3 (ISO 15930-3)
    PdfX3,
    /// PDF/X-4 (ISO 15930-7)
    PdfX4,
    /// PDF/UA-1 (ISO 14289-1)
    PdfUa1,
}

impl PdfStandard {
    /// Short display name (e.g., "PDF/A-1b")
    pub fn short_name(&self) -> &'static str {
        match self {
            PdfStandard::PdfA1a => "PDF/A-1a",
            PdfStandard::PdfA1b => "PDF/A-1b",
            PdfStandard::PdfA2a => "PDF/A-2a",
            PdfStandard::PdfA2b => "PDF/A-2b",
            PdfStandard::PdfA2u => "PDF/A-2u",
            PdfStandard::PdfA3a => "PDF/A-3a",
            PdfStandard::PdfA3b => "PDF/A-3b",
            PdfStandard::PdfA3u => "PDF/A-3u",
            PdfStandard::PdfX1a => "PDF/X-1a",
            PdfStandard::PdfX3 => "PDF/X-3",
            PdfStandard::PdfX4 => "PDF/X-4",
            PdfStandard::PdfUa1 => "PDF/UA-1",
        }
    }

    /// Whether this standard allows transparency (PDF/A-2+ does).
    pub fn allows_transparency(&self) -> bool {
        matches!(
            self,
            PdfStandard::PdfA2a
                | PdfStandard::PdfA2b
                | PdfStandard::PdfA2u
                | PdfStandard::PdfA3a
                | PdfStandard::PdfA3b
                | PdfStandard::PdfA3u
        )
    }

    /// Whether this standard allows JPEG2000 (PDF/A-2+ does).
    pub fn allows_jpeg2000(&self) -> bool {
        self.allows_transparency()
    }
}

// ============================================================================
// ViolationSeverity
// ============================================================================

/// Severity of a compliance violation.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ViolationSeverity {
    /// Error: document is not compliant.
    Error,
    /// Warning: may cause issues in some viewers or workflows.
    Warning,
}

// ============================================================================
// ComplianceViolation
// ============================================================================

/// A single compliance violation.
#[derive(Debug, Clone)]
pub struct ComplianceViolation {
    /// The standard that was being validated.
    pub standard: PdfStandard,
    /// Rule identifier (e.g., "PDFA1-001").
    pub rule_id: String,
    /// Severity of the violation.
    pub severity: ViolationSeverity,
    /// Human-readable message.
    pub message: String,
    /// Location (page number, object reference, or "Document").
    pub location: String,
}

impl ComplianceViolation {
    pub fn new(
        standard: PdfStandard,
        rule_id: &str,
        severity: ViolationSeverity,
        message: &str,
        location: &str,
    ) -> Self {
        Self {
            standard,
            rule_id: rule_id.to_string(),
            severity,
            message: message.to_string(),
            location: location.to_string(),
        }
    }

    pub fn error(standard: PdfStandard, rule_id: &str, message: &str, location: &str) -> Self {
        Self::new(
            standard,
            rule_id,
            ViolationSeverity::Error,
            message,
            location,
        )
    }

    pub fn warning(standard: PdfStandard, rule_id: &str, message: &str, location: &str) -> Self {
        Self::new(
            standard,
            rule_id,
            ViolationSeverity::Warning,
            message,
            location,
        )
    }
}

// ============================================================================
// ComplianceReport
// ============================================================================

/// Result of compliance validation.
#[derive(Debug, Clone)]
pub struct ComplianceReport {
    /// The standard that was validated against.
    pub standard: PdfStandard,
    /// All violations found.
    pub violations: Vec<ComplianceViolation>,
    /// Whether the document is compliant (no errors).
    pub is_compliant: bool,
}

impl ComplianceReport {
    pub fn new(standard: PdfStandard, violations: Vec<ComplianceViolation>) -> Self {
        let has_errors = violations
            .iter()
            .any(|v| v.severity == ViolationSeverity::Error);
        Self {
            standard,
            violations,
            is_compliant: !has_errors,
        }
    }

    /// Number of error-level violations.
    pub fn error_count(&self) -> usize {
        self.violations
            .iter()
            .filter(|v| v.severity == ViolationSeverity::Error)
            .count()
    }

    /// Number of warning-level violations.
    pub fn warning_count(&self) -> usize {
        self.violations
            .iter()
            .filter(|v| v.severity == ViolationSeverity::Warning)
            .count()
    }
}

// ============================================================================
// validate_pdfa (PDF/A-1b)
// ============================================================================

/// Validate PDF/A-1b compliance.
///
/// Checks:
/// - No encryption
/// - No JavaScript/actions
/// - All fonts embedded
/// - ICC output intent present
/// - No transparency (PDF/A-1)
/// - XMP metadata present with correct conformance level
/// - No LZW compression
pub fn validate_pdfa(data: &[u8]) -> ComplianceReport {
    let content = String::from_utf8_lossy(data);
    let mut violations = Vec::new();
    let standard = PdfStandard::PdfA1b;

    // 1. No encryption
    if content.contains("/Encrypt") {
        violations.push(ComplianceViolation::error(
            standard,
            "PDFA1-001",
            "PDF/A-1b does not allow encryption",
            "Document",
        ));
    }

    // 2. No JavaScript/actions
    if content.contains("/JS") || content.contains("/JavaScript") {
        violations.push(ComplianceViolation::error(
            standard,
            "PDFA1-002",
            "PDF/A-1b does not allow JavaScript or actions",
            "Document",
        ));
    }

    // 3. All fonts embedded
    // Simple heuristic: if document uses fonts but has no FontFile/FontFile2/FontFile3,
    // fonts may not be embedded. Full validation would require parsing each font resource.
    if content.contains("/Font")
        && !content.contains("/FontFile")
        && !content.contains("/FontFile2")
        && !content.contains("/FontFile3")
    {
        violations.push(ComplianceViolation::error(
            standard,
            "PDFA1-003",
            "PDF/A-1b requires all fonts to be embedded; no embedded font data found",
            "Fonts",
        ));
    }

    // 4. ICC output intent present
    if !content.contains("/OutputIntent") && !content.contains("/OutputIntents") {
        violations.push(ComplianceViolation::error(
            standard,
            "PDFA1-004",
            "PDF/A-1b requires an ICC output intent for color management",
            "Document",
        ));
    }

    // 5. No transparency (PDF/A-1)
    if content.contains("/SMask") || content.contains("/Mask") {
        violations.push(ComplianceViolation::error(
            standard,
            "PDFA1-005",
            "PDF/A-1 does not allow transparency (SMask or Mask)",
            "Document",
        ));
    }
    if content.contains("/ExtGState") && content.contains("/ca ") {
        violations.push(ComplianceViolation::error(
            standard,
            "PDFA1-006",
            "PDF/A-1 does not allow transparency (ExtGState ca)",
            "Graphics State",
        ));
    }

    // 6. XMP metadata present with correct conformance level
    if !content.contains("/Metadata") {
        violations.push(ComplianceViolation::error(
            standard,
            "PDFA1-007",
            "PDF/A-1b requires XMP metadata in document catalog",
            "Metadata",
        ));
    }
    if !content.contains("pdfaid:part") && !content.contains("pdfa:part") {
        violations.push(ComplianceViolation::error(
            standard,
            "PDFA1-008",
            "XMP metadata must contain PDF/A identification (pdfaid:part)",
            "XMP Metadata",
        ));
    }

    // 7. No LZW compression
    if content.contains("/LZWDecode") || content.contains("/LZW ") {
        violations.push(ComplianceViolation::error(
            standard,
            "PDFA1-009",
            "PDF/A-1b does not allow LZW compression; use FlateDecode",
            "Stream Filters",
        ));
    }

    ComplianceReport::new(standard, violations)
}

// ============================================================================
// validate_pdfa2 (PDF/A-2b)
// ============================================================================

/// Validate PDF/A-2b compliance.
///
/// Additional PDF/A-2b checks beyond PDF/A-1b:
/// - Transparency allowed (unlike PDF/A-1)
/// - Optional content groups allowed
/// - JPEG2000 allowed
pub fn validate_pdfa2(data: &[u8]) -> ComplianceReport {
    let content = String::from_utf8_lossy(data);
    let mut violations = Vec::new();
    let standard = PdfStandard::PdfA2b;

    // Run PDF/A-1b base checks first (but skip transparency - PDF/A-2 allows it)
    let pdfa1 = validate_pdfa(data);

    for v in pdfa1.violations {
        // Skip PDF/A-1 transparency violations in PDF/A-2
        if v.rule_id == "PDFA1-005" || v.rule_id == "PDFA1-006" {
            continue;
        }
        violations.push(v);
    }

    // PDF/A-2 specific: XMP must indicate part 2
    if content.contains("pdfaid:part") {
        if content.contains("pdfaid:part>1<") {
            violations.push(ComplianceViolation::error(
                standard,
                "PDFA2-001",
                "XMP indicates PDF/A-1; PDF/A-2 requires pdfaid:part=2",
                "XMP Metadata",
            ));
        }
    }

    // Optional content groups and JPEG2000 are allowed in PDF/A-2
    // No additional prohibitions to check

    ComplianceReport::new(standard, violations)
}

// ============================================================================
// validate_pdfx
// ============================================================================

/// Validate PDF/X compliance
///
/// Checks:
/// - TrimBox or ArtBox required
/// - Output intent required
/// - No RGB colors without ICC (for PDF/X-1a)
pub fn validate_pdfx(data: &[u8]) -> ComplianceReport {
    let content = String::from_utf8_lossy(data);
    let mut violations = Vec::new();
    let standard = PdfStandard::PdfX1a; // Default to PDF/X-1a for strictest checks

    // 1. TrimBox or ArtBox required
    let has_trim_or_art = content.contains("/TrimBox") || content.contains("/ArtBox");
    if !has_trim_or_art {
        violations.push(ComplianceViolation::error(
            standard,
            "PDFX-001",
            "PDF/X requires TrimBox or ArtBox on pages",
            "Pages",
        ));
    }

    // 2. Output intent required
    if !content.contains("/OutputIntent") && !content.contains("/OutputIntents") {
        violations.push(ComplianceViolation::error(
            standard,
            "PDFX-002",
            "PDF/X requires an output intent in the document catalog",
            "Document",
        ));
    }

    // 3. For PDF/X-1a: no RGB without ICC
    // DeviceRGB/DeviceCMYK/DeviceGray without explicit ICC in output intent
    // is a simplified check - full validation would need ICC profile parsing
    if content.contains("/DeviceRGB") && !content.contains("/OutputIntent") {
        violations.push(ComplianceViolation::error(
            standard,
            "PDFX-003",
            "PDF/X-1a: RGB colors require ICC output intent",
            "Color",
        ));
    }

    ComplianceReport::new(standard, violations)
}

// ============================================================================
// detect_standard
// ============================================================================

/// Auto-detect which PDF standard a document claims to conform to from XMP metadata.
///
/// Returns `None` if no standard is found or XMP is missing.
pub fn detect_standard(data: &[u8]) -> Option<PdfStandard> {
    let content = String::from_utf8_lossy(data);

    // Check for PDF/A in XMP
    if content.contains("pdfaid:part") || content.contains("pdfa:part") {
        if content.contains("pdfaid:conformance>A") || content.contains("pdfa:conformance>A") {
            if content.contains("pdfaid:part>1<") || content.contains("pdfa:part>1<") {
                return Some(PdfStandard::PdfA1a);
            }
            if content.contains("pdfaid:part>2<") || content.contains("pdfa:part>2<") {
                return Some(PdfStandard::PdfA2a);
            }
            if content.contains("pdfaid:part>3<") || content.contains("pdfa:part>3<") {
                return Some(PdfStandard::PdfA3a);
            }
        }
        if content.contains("pdfaid:part>1<") || content.contains("pdfa:part>1<") {
            return Some(PdfStandard::PdfA1b);
        }
        if content.contains("pdfaid:part>2<") || content.contains("pdfa:part>2<") {
            if content.contains("pdfaid:conformance>U") || content.contains("pdfa:conformance>U") {
                return Some(PdfStandard::PdfA2u);
            }
            return Some(PdfStandard::PdfA2b);
        }
        if content.contains("pdfaid:part>3<") || content.contains("pdfa:part>3<") {
            if content.contains("pdfaid:conformance>U") || content.contains("pdfa:conformance>U") {
                return Some(PdfStandard::PdfA3u);
            }
            return Some(PdfStandard::PdfA3b);
        }
    }

    // Check for PDF/X in GTS_PDFXVersion or XMP
    if content.contains("GTS_PDFXVersion") || content.contains("GTS_PDFX") {
        if content.contains("PDF/X-4") {
            return Some(PdfStandard::PdfX4);
        }
        if content.contains("PDF/X-3") {
            return Some(PdfStandard::PdfX3);
        }
        if content.contains("PDF/X-1a") {
            return Some(PdfStandard::PdfX1a);
        }
    }

    // Check for PDF/UA
    if content.contains("pdfuaid:part") || content.contains("pdf/ua") {
        return Some(PdfStandard::PdfUa1);
    }

    None
}

// ============================================================================
// TransparencyFlattener (stub)
// ============================================================================

/// Stub for transparency flattening.
///
/// **Transparency flattening requires a full rendering pipeline.** To properly flatten
/// transparency in a PDF:
///
/// 1. Parse the PDF content streams
/// 2. Interpret each page's drawing commands
/// 3. Render to a raster device (or vector equivalent) with transparency
///    compositing (e.g., alpha blending, soft masks)
/// 4. Re-encode the flattened result as opaque PDF content
///
/// This cannot be done without:
/// - A complete PDF interpreter (content stream parsing)
/// - A rendering engine (e.g., MuPDF's draw device, or similar)
/// - Proper handling of blend modes, soft masks, and transparency groups
///
/// Use `flatten_transparency()` which returns an error explaining this.
#[derive(Debug, Clone)]
pub struct TransparencyFlattener {
    /// Resolution for rasterization (DPI) when using raster-based flattening.
    /// Not used in stub.
    _resolution: Option<u32>,
}

impl TransparencyFlattener {
    pub fn new() -> Self {
        Self { _resolution: None }
    }

    pub fn with_resolution(mut self, dpi: u32) -> Self {
        self._resolution = Some(dpi);
        self
    }
}

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

/// Attempt to flatten transparency in a PDF.
///
/// **This is a stub.** Transparency flattening requires a full rendering pipeline:
/// content stream interpretation, graphics state (blend modes, soft masks),
/// and re-encoding to opaque content. This operation is not implemented.
///
/// Returns an error explaining the complexity.
pub fn flatten_transparency(_data: &[u8]) -> Result<Vec<u8>> {
    Err(Error::Unsupported(
        "Transparency flattening requires a full PDF rendering pipeline: content stream \
         interpretation, graphics state handling (blend modes, soft masks, transparency \
         groups), and re-encoding to opaque content. This operation is not yet implemented. \
         Consider using an external tool (e.g., Ghostscript, Acrobat) for flattening."
            .to_string(),
    ))
}

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

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

    #[test]
    fn test_pdf_standard_short_name() {
        assert_eq!(PdfStandard::PdfA1b.short_name(), "PDF/A-1b");
        assert_eq!(PdfStandard::PdfA2b.short_name(), "PDF/A-2b");
        assert_eq!(PdfStandard::PdfX1a.short_name(), "PDF/X-1a");
    }

    #[test]
    fn test_pdf_standard_allows_transparency() {
        assert!(!PdfStandard::PdfA1b.allows_transparency());
        assert!(PdfStandard::PdfA2b.allows_transparency());
    }

    #[test]
    fn test_compliance_violation_constructors() {
        let v = ComplianceViolation::error(
            PdfStandard::PdfA1b,
            "PDFA1-001",
            "No encryption",
            "Document",
        );
        assert_eq!(v.severity, ViolationSeverity::Error);
        assert_eq!(v.rule_id, "PDFA1-001");

        let w =
            ComplianceViolation::warning(PdfStandard::PdfA1b, "PDFA1-W001", "Optional", "Metadata");
        assert_eq!(w.severity, ViolationSeverity::Warning);
    }

    #[test]
    fn test_compliance_report() {
        let violations = vec![ComplianceViolation::error(
            PdfStandard::PdfA1b,
            "PDFA1-001",
            "Encryption",
            "Doc",
        )];
        let report = ComplianceReport::new(PdfStandard::PdfA1b, violations);
        assert!(!report.is_compliant);
        assert_eq!(report.error_count(), 1);
        assert_eq!(report.warning_count(), 0);
    }

    #[test]
    fn test_validate_pdfa_encryption() {
        let pdf = b"%PDF-1.4
trailer << /Size 1 /Root 1 0 R /Encrypt 2 0 R >>
startxref 0
%%EOF";
        let report = validate_pdfa(pdf);
        assert!(!report.is_compliant);
        assert!(report.violations.iter().any(|v| v.rule_id == "PDFA1-001"));
    }

    #[test]
    fn test_validate_pdfa_javascript() {
        let pdf = b"%PDF-1.4
1 0 obj << /Type /Catalog /Pages 2 0 R /OpenAction << /S /JavaScript /JS (alert(1)) >> >> endobj
trailer << /Size 2 /Root 1 0 R >>
startxref 0
%%EOF";
        let report = validate_pdfa(pdf);
        assert!(!report.is_compliant);
        assert!(report.violations.iter().any(|v| v.rule_id == "PDFA1-002"));
    }

    #[test]
    fn test_validate_pdfa_lzw() {
        let pdf = b"%PDF-1.4
5 0 obj << /Length 10 /Filter /LZWDecode >> stream
\x78\x9c\x00\x00\x00\x01endstream endobj
trailer << /Size 6 /Root 1 0 R >>
startxref 0
%%EOF";
        let report = validate_pdfa(pdf);
        assert!(!report.is_compliant);
        assert!(report.violations.iter().any(|v| v.rule_id == "PDFA1-009"));
    }

    #[test]
    fn test_validate_pdfa_transparency() {
        let pdf = b"%PDF-1.4
4 0 obj << /Type /Page /Resources << /ExtGState << /GS1 << /ca 0.5 >> >> >> >> endobj
trailer << /Size 5 /Root 1 0 R >>
startxref 0
%%EOF";
        let report = validate_pdfa(pdf);
        assert!(!report.is_compliant);
        assert!(report.violations.iter().any(|v| v.rule_id == "PDFA1-006"));
    }

    #[test]
    fn test_validate_pdfa2_allows_transparency() {
        let pdf = b"%PDF-1.4
4 0 obj << /Type /Page /Resources << /ExtGState << /GS1 << /ca 0.5 >> >> >> >> endobj
trailer << /Size 5 /Root 1 0 R >>
startxref 0
%%EOF";
        let report = validate_pdfa2(pdf);
        // PDF/A-2 allows transparency; PDFA1-005/006 should be skipped
        assert!(
            !report
                .violations
                .iter()
                .any(|v| v.rule_id == "PDFA1-005" || v.rule_id == "PDFA1-006")
        );
    }

    #[test]
    fn test_validate_pdfx_no_output_intent() {
        let pdf = b"%PDF-1.4
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
2 0 obj << /Type /Pages /Count 1 /Kids [3 0 R] >> endobj
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >> endobj
trailer << /Size 4 /Root 1 0 R >>
startxref 0
%%EOF";
        let report = validate_pdfx(pdf);
        assert!(!report.is_compliant);
        assert!(report.violations.iter().any(|v| v.rule_id == "PDFX-002"));
    }

    #[test]
    fn test_detect_standard_pdfa1b() {
        let pdf = b"%PDF-1.4
3 0 obj << /Type /Metadata >> stream
<?xpacket?><x:xmpmeta><rdf:RDF><rdf:Description xmlns:pdfaid='http://www.aiim.org/pdfa/ns/id/'>
<pdfaid:part>1</pdfaid:part>
<pdfaid:conformance>B</pdfaid:conformance>
</rdf:Description></rdf:RDF></x:xmpmeta>
endstream endobj
trailer << /Size 4 /Root 1 0 R >>
%%EOF";
        let detected = detect_standard(pdf);
        assert_eq!(detected, Some(PdfStandard::PdfA1b));
    }

    #[test]
    fn test_detect_standard_pdfa2b() {
        let pdf = b"%PDF-1.4
3 0 obj << /Type /Metadata >> stream
<?xpacket?><x:xmpmeta><rdf:RDF><rdf:Description xmlns:pdfaid='http://www.aiim.org/pdfa/ns/id/'>
<pdfaid:part>2</pdfaid:part>
<pdfaid:conformance>B</pdfaid:conformance>
</rdf:Description></rdf:RDF></x:xmpmeta>
endstream endobj
trailer << /Size 4 /Root 1 0 R >>
%%EOF";
        let detected = detect_standard(pdf);
        assert_eq!(detected, Some(PdfStandard::PdfA2b));
    }

    #[test]
    fn test_detect_standard_none() {
        let pdf = b"%PDF-1.4
trailer << /Size 1 /Root 1 0 R >>
startxref 0
%%EOF";
        let detected = detect_standard(pdf);
        assert_eq!(detected, None);
    }

    #[test]
    fn test_flatten_transparency_returns_error() {
        let pdf = b"%PDF-1.4
trailer << /Size 1 /Root 1 0 R >>
%%EOF";
        let result = flatten_transparency(pdf);
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(format!("{}", err).contains("Transparency flattening"));
        assert!(format!("{}", err).contains("not yet implemented"));
    }

    #[test]
    fn test_transparency_flattener_stub() {
        let flattener = TransparencyFlattener::new();
        let _ = flattener.with_resolution(300);
        // Stub has no methods; just ensure it compiles and default works
        let _default = TransparencyFlattener::default();
    }
}
