using DevExpress.Pdf;
using DevExpress.Drawing;
using System.Drawing;
using DocumentOperator.Application.Common.DTOs;
using DocumentOperator.Application.Common.Interfaces;
using DocumentOperator.Domain.Common.Exceptions;

namespace DocumentOperator.Infrastructure.Services.PdfProcessing;

/// <summary>
/// PDF processor implementation using DevExpress.Pdf library.
/// Handles PDF validation, metadata extraction, and attachment operations.
/// </summary>
public class DevExpressPdfProcessor : IPdfProcessor
{
    #region PDF Validation
    /// <summary>
    /// Validates a PDF document and returns metadata.
    /// </summary>
    /// <param name="pdfStream">PDF content as stream (caller is responsible for disposal)</param>
    /// <returns>PDF metadata (page count, file size, version, etc.)</returns>
    /// <exception cref="BadRequestException">Thrown when stream is empty or invalid</exception>
    public async Task<Application.Common.DTOs.PdfMetadata> ValidateAsync(Stream pdfStream)
    {
        // 1. Input Validation
        ArgumentNullException.ThrowIfNull(pdfStream, nameof(pdfStream));
        
        if (pdfStream.Length == 0)
        {
            throw new BadRequestException("PDF stream cannot be empty");
        }

        // Defensive validation: Seekable streams must be at Position = 0
        if (pdfStream.CanSeek && pdfStream.Position != 0)
        {
            throw new BadRequestException("PDF stream must be positioned at the beginning (Position = 0).");
        }

        // 2. Read stream to byte array for raw data analysis
        // (DevExpress needs byte[] for some operations like attachment detection)
        byte[] pdfBytes;
        if (pdfStream is MemoryStream ms && ms.TryGetBuffer(out var buffer))
        {
            // Fast path: reuse MemoryStream buffer
            pdfBytes = buffer.Array!;
        }
        else
        {
            // Slow path: copy stream to byte array
            using var memoryStream = new MemoryStream();
            await pdfStream.CopyToAsync(memoryStream);
            pdfBytes = memoryStream.ToArray();
        }

        // 3. Load PDF with DevExpress Document API
        // Reset position for DevExpress (seekable streams only)
        if (pdfStream.CanSeek)
        {
            pdfStream.Position = 0;
        }
        
        using var processor = new PdfDocumentProcessor();
        processor.LoadDocument(pdfStream);

        // 4. Extract metadata
        var document = processor.Document;

        int pageCount = document.Pages.Count;
        string pdfVersion = document.Version.ToString(); // e.g., "1.4", "1.7"

        // Attachments (embedded files)
        // DevExpress PdfDocument API doesn't expose EmbeddedFiles directly in old API.
        // We scan PDF raw data for "/EmbeddedFiles" and parse the name tree to get count.
        var (hasAttachments, attachmentCount) = DetectEmbeddedFiles(pdfBytes);

        // 5. Create and return PdfMetadata DTO
        return new Application.Common.DTOs.PdfMetadata(
            pageCount: pageCount,
            fileSizeBytes: pdfBytes.Length,
            pdfVersion: pdfVersion,
            hasAttachments: hasAttachments,
            attachmentCount: attachmentCount
        );
    }

    /// <summary>
    /// Validates a PDF/A document and checks conformance level.
    /// </summary>
    /// <param name="pdfStream">PDF content as stream (caller is responsible for disposal)</param>
    /// <returns>PDF/A metadata including conformance level and validation errors/warnings</returns>
    /// <exception cref="BadRequestException">Thrown when stream is empty or invalid</exception>
    public async Task<PdfAMetadata> ValidatePdfAAsync(Stream pdfStream)
    {
        // 1. Input Validation
        ArgumentNullException.ThrowIfNull(pdfStream, nameof(pdfStream));
        
        if (pdfStream.Length == 0)
        {
            throw new BadRequestException("PDF stream cannot be empty");
        }

        // Defensive validation: Seekable streams must be at Position = 0
        if (pdfStream.CanSeek && pdfStream.Position != 0)
        {
            throw new BadRequestException("PDF stream must be positioned at the beginning (Position = 0).");
        }

        // 2. Read stream to byte array for raw data analysis
        byte[] pdfBytes;
        if (pdfStream is MemoryStream ms && ms.TryGetBuffer(out var buffer))
        {
            pdfBytes = buffer.Array!;
        }
        else
        {
            using var memoryStream = new MemoryStream();
            await pdfStream.CopyToAsync(memoryStream);
            pdfBytes = memoryStream.ToArray();
        }

        // 3. Load PDF with DevExpress Document API
        // Reset position for DevExpress (seekable streams only)
        if (pdfStream.CanSeek)
        {
            pdfStream.Position = 0;
        }
        
        using var processor = new PdfDocumentProcessor();
        processor.LoadDocument(pdfStream);

        var document = processor.Document;

        // 4. Extract basic metadata
        int pageCount = document.Pages.Count;
        string pdfVersion = document.Version.ToString();
        
        // 5. Check encryption (scan PDF raw data for /Encrypt keyword)
        bool encrypted = DetectEncryption(pdfBytes);

        // 6. Check PDF/A conformance (scan PDF raw data for PDF/A identifier)
        var (isPdfACompliant, pdfaVersion) = DetectPdfAConformance(pdfBytes);

        // 7. Collect errors and warnings
        var errors = new List<string>();
        var warnings = new List<string>();

        // If encrypted, PDF/A compliance is not possible
        if (encrypted && isPdfACompliant)
        {
            errors.Add("PDF/A documents cannot be encrypted");
            isPdfACompliant = false;
        }

        // Basic PDF/A validation checks
        if (isPdfACompliant)
        {
            // Add generic warning for manual verification
            warnings.Add("Manual verification recommended: All fonts must be embedded");
            warnings.Add("Manual verification recommended: No JavaScript or multimedia content");
        }

        // 8. Determine overall validity
        bool isValid = errors.Count == 0;

        // 9. Create and return PdfAMetadata DTO
        return new PdfAMetadata(
            isValid: isValid,
            pdfVersion: pdfVersion,
            pageCount: pageCount,
            fileSizeBytes: pdfBytes.Length,
            encrypted: encrypted,
            pdfaVersion: pdfaVersion,
            pdfaCompliant: isPdfACompliant,
            errors: errors,
            warnings: warnings
        );
    }

    #endregion

    #region Attachment Operations

    /// <summary>
    /// Checks for embedded files (attachments) in a PDF document and returns detailed metadata.
    /// Uses DevExpress PdfDocument.FileAttachments collection to retrieve attachment details.
    /// </summary>
    /// <param name="pdfStream">PDF content as stream (caller is responsible for disposal)</param>
    /// <returns>Attachment information including count, file names, MIME types, and sizes</returns>
    /// <exception cref="BadRequestException">Thrown when stream is empty</exception>
    public async Task<AttachmentInfo> CheckAttachmentsAsync(Stream pdfStream)
    {
        // 1. Input Validation
        ArgumentNullException.ThrowIfNull(pdfStream, nameof(pdfStream));
        
        if (pdfStream.Length == 0)
        {
            throw new BadRequestException("PDF stream cannot be empty");
        }

        // Defensive validation: Seekable streams must be at Position = 0
        if (pdfStream.CanSeek && pdfStream.Position != 0)
        {
            throw new BadRequestException("PDF stream must be positioned at the beginning (Position = 0).");
        }

        // 2. Load PDF with DevExpress Document API
        // DevExpress LoadDocument may throw exceptions for corrupted PDFs - let them propagate naturally
        // Middleware will catch and convert to 500 Internal Server Error
        using var processor = new PdfDocumentProcessor();
        processor.LoadDocument(pdfStream);

        var document = processor.Document;

        // 3. Extract attachment details using DevExpress FileAttachments collection
        var fileAttachments = document.FileAttachments;

        // 4. No attachments case (FileAttachments is IEnumerable<PdfFileAttachment>)
        if (fileAttachments == null || !fileAttachments.Any())
        {
            return AttachmentInfo.Empty;
        }

        // 5. Map DevExpress PdfFileAttachment to our DTO AttachmentMetadata
        var attachments = fileAttachments.Select(devExpressAttachment =>
            new AttachmentMetadata(
                fileName: devExpressAttachment.FileName ?? "unnamed",
                mimeType: devExpressAttachment.MimeType ?? "application/octet-stream",
                sizeBytes: devExpressAttachment.Size
            )).ToList();

        // 6. Return AttachmentInfo DTO
        return new AttachmentInfo(
            hasAttachments: true,
            attachmentCount: attachments.Count,
            attachments: attachments.AsReadOnly()
        );
    }

    /// <summary>
    /// Extracts all embedded files from a PDF document and returns them as a ZIP archive.
    /// Uses DevExpress PdfDocument.FileAttachments to retrieve attachment data.
    /// </summary>
    /// <param name="pdfStream">PDF content as stream (caller is responsible for disposal)</param>
    /// <returns>ZIP archive byte array containing all extracted attachments</returns>
    /// <exception cref="BadRequestException">Thrown when stream is empty</exception>
    /// <exception cref="NotFoundException">Thrown when PDF contains no attachments</exception>
    public async Task<byte[]> ExtractAttachmentsAsync(Stream pdfStream)
    {
        // 1. Input Validation
        ArgumentNullException.ThrowIfNull(pdfStream, nameof(pdfStream));
        
        if (pdfStream.Length == 0)
        {
            throw new BadRequestException("PDF stream cannot be empty");
        }

        // Defensive validation: Seekable streams must be at Position = 0
        if (pdfStream.CanSeek && pdfStream.Position != 0)
        {
            throw new BadRequestException("PDF stream must be positioned at the beginning (Position = 0).");
        }

        // 2. Load PDF with DevExpress Document API
        using var processor = new PdfDocumentProcessor();
        processor.LoadDocument(pdfStream);

        var document = processor.Document;

        // 3. Extract attachment data using DevExpress FileAttachments collection
        var fileAttachments = document.FileAttachments;

        // 4. No attachments case
        if (fileAttachments == null || !fileAttachments.Any())
        {
            throw new NotFoundException("PDF does not contain any attachments");
        }

        // 5. Create ZIP archive in memory
        using var zipStream = new MemoryStream();
        using (var zipArchive = new System.IO.Compression.ZipArchive(zipStream, System.IO.Compression.ZipArchiveMode.Create, leaveOpen: true))
        {
            foreach (var attachment in fileAttachments)
            {
                // Get attachment metadata
                string fileName = attachment.FileName ?? "unnamed";
                byte[] fileData = attachment.Data;

                // Create entry in ZIP
                var entry = zipArchive.CreateEntry(fileName, System.IO.Compression.CompressionLevel.Optimal);
                
                // Write attachment data to ZIP entry
                using var entryStream = entry.Open();
                await entryStream.WriteAsync(fileData, 0, fileData.Length);
            }
        }

        // 6. Return ZIP byte array
        return zipStream.ToArray();
    }

    #endregion

    #region PDF Merge Operations

    public async Task<byte[]> MergePdfsAsync(IReadOnlyList<Stream> pdfStreams, IReadOnlyList<string?>? pageRanges = null)
    {
        // 1. Validate input: minimum 2 PDFs required
        if (pdfStreams == null || pdfStreams.Count < 2)
            throw new BadRequestException("At least 2 PDF files are required for merging");

        // 2. Validate page ranges length (if provided)
        if (pageRanges != null && pageRanges.Count != pdfStreams.Count)
            throw new BadRequestException($"Page ranges count ({pageRanges.Count}) must match PDF files count ({pdfStreams.Count})");

        // 3. Defensive validation: all streams must be at Position = 0
        for (int i = 0; i < pdfStreams.Count; i++)
        {
            var stream = pdfStreams[i];
            
            if (stream == null)
                throw new BadRequestException($"PDF stream at index {i} is null");

            if (stream.Length == 0)
                throw new BadRequestException($"PDF stream at index {i} is empty");

            if (stream.CanSeek && stream.Position != 0)
                throw new BadRequestException(null, new ArgumentException(
                    $"PDF stream at index {i} must be positioned at the beginning (Position = 0).",
                    nameof(pdfStreams)));
        }

        // 4. Create merged PDF using DevExpress
        using var mergedProcessor = new PdfDocumentProcessor();
        
        // Load first PDF as base document
        mergedProcessor.LoadDocument(pdfStreams[0]);

        // Apply page range to first PDF if specified
        if (pageRanges != null && !string.IsNullOrWhiteSpace(pageRanges[0]))
        {
            var pageIndices = ParsePageRange(pageRanges[0]!, mergedProcessor.Document.Pages.Count);
            // Remove pages not in range (process in reverse to maintain indices)
            for (int i = mergedProcessor.Document.Pages.Count - 1; i >= 0; i--)
            {
                if (!pageIndices.Contains(i))
                    mergedProcessor.Document.Pages.RemoveAt(i);
            }
        }

        // Append remaining PDFs
        for (int i = 1; i < pdfStreams.Count; i++)
        {
            string? pageRange = pageRanges?[i];

            if (string.IsNullOrWhiteSpace(pageRange))
            {
                // Append all pages
                mergedProcessor.AppendDocument(pdfStreams[i]);
            }
            else
            {
                // Parse page range and append selected pages
                // Note: We need to load the document first to validate page range
                using var tempProcessor = new PdfDocumentProcessor();
                tempProcessor.LoadDocument(pdfStreams[i]);
                
                var pageIndices = ParsePageRange(pageRange, tempProcessor.Document.Pages.Count);

                // DevExpress AppendDocument doesn't support arbitrary page selection
                // Workaround: Create temp PDF with selected pages, then append
                using var tempStream = new MemoryStream();
                
                // Remove unwanted pages from temp document (in reverse order)
                for (int j = tempProcessor.Document.Pages.Count - 1; j >= 0; j--)
                {
                    if (!pageIndices.Contains(j))
                        tempProcessor.Document.Pages.RemoveAt(j);
                }
                
                tempProcessor.SaveDocument(tempStream);
                tempStream.Position = 0;
                mergedProcessor.AppendDocument(tempStream);
            }
        }

        // 5. Save merged PDF to byte array
        using var outputStream = new MemoryStream();
        mergedProcessor.SaveDocument(outputStream);
        
        return await Task.FromResult(outputStream.ToArray());
    }

    #endregion

    #region PDF Annotation

    /// <summary>
    /// Adds an annotation to a PDF document at the specified page and rectangle.
    /// </summary>
    public async Task<byte[]> AddAnnotationAsync(
        Stream pdfStream,
        Domain.Models.ValueObjects.AnnotationType annotationType,
        int pageNumber,
        (double X1, double Y1, double X2, double Y2) rectangle,
        string? content = null,
        string? author = null,
        string? color = null,
        Domain.Models.ValueObjects.TextMarkupStyle? textMarkupStyle = null,
        Domain.Models.ValueObjects.AnnotationOrigin origin = Domain.Models.ValueObjects.AnnotationOrigin.BottomLeft)
    {
        // 1. Input validation
        ArgumentNullException.ThrowIfNull(pdfStream, nameof(pdfStream));
        
        if (pdfStream.Length == 0)
            throw new BadRequestException("PDF stream cannot be empty");

        if (pdfStream.Position != 0)
            throw new BadRequestException($"PDF stream must be at position 0 (current position: {pdfStream.Position})");

        if (pageNumber < 1)
            throw new BadRequestException($"Page number must be >= 1 (provided: {pageNumber})");

        // Validate content requirement
        if (annotationType is Domain.Models.ValueObjects.AnnotationType.FreeText 
            or Domain.Models.ValueObjects.AnnotationType.StickyNote)
        {
            if (string.IsNullOrWhiteSpace(content))
                throw new BadRequestException($"{annotationType} annotation requires content");
        }

        // Validate TextMarkup style requirement
        if (annotationType == Domain.Models.ValueObjects.AnnotationType.TextMarkup && textMarkupStyle == null)
            throw new BadRequestException("TextMarkup annotation requires textMarkupStyle parameter");

        byte[] annotatedPdfBytes;

        try
        {
            // 2. Load PDF
            using var processor = new PdfDocumentProcessor();
            processor.LoadDocument(pdfStream);

            // 3. Validate page number
            int pageCount = processor.Document.Pages.Count;
            if (pageNumber > pageCount)
                throw new BadRequestException($"Page number {pageNumber} exceeds document page count ({pageCount})");

            // 4. Convert coordinates if origin is TopLeft
            var pdfRectangle = rectangle;
            if (origin == Domain.Models.ValueObjects.AnnotationOrigin.TopLeft)
            {
                var page = processor.Document.Pages[pageNumber - 1];
                double pageHeight = page.CropBox.Height;
                
                // Convert Y coordinates: TopLeft → BottomLeft
                // TopLeft Y=0 → BottomLeft Y=pageHeight
                // TopLeft Y=pageHeight → BottomLeft Y=0
                pdfRectangle = (
                    rectangle.X1,
                    pageHeight - rectangle.Y2, // Y2 becomes Y1 (top → bottom)
                    rectangle.X2,
                    pageHeight - rectangle.Y1  // Y1 becomes Y2 (bottom → top)
                );
            }
            
            // 5. Get page facade (zero-based index)
            var pageFacade = processor.DocumentFacade.Pages[pageNumber - 1];

            // 6. Create annotation rectangle from converted coordinates
            var pdfRect = new PdfRectangle(pdfRectangle.X1, pdfRectangle.Y1, pdfRectangle.X2, pdfRectangle.Y2);

            // 7. Parse color (default to yellow for highlights, red for others)
            PdfRGBColor annotationColor = ParseColor(color) ?? (annotationType == Domain.Models.ValueObjects.AnnotationType.TextMarkup 
                ? new PdfRGBColor(1.0, 1.0, 0) // Yellow
                : new PdfRGBColor(1.0, 0, 0)); // Red

            // 8. Add annotation based on type
            switch (annotationType)
            {
                case Domain.Models.ValueObjects.AnnotationType.TextMarkup:
                    AddTextMarkupAnnotation(pageFacade, pdfRect, textMarkupStyle!.Value, content, author, annotationColor);
                    break;

                case Domain.Models.ValueObjects.AnnotationType.FreeText:
                    AddFreeTextAnnotation(pageFacade, pdfRect, content!, author, annotationColor);
                    break;

                case Domain.Models.ValueObjects.AnnotationType.StickyNote:
                    AddStickyNoteAnnotation(pageFacade, pdfRect, content!, author, annotationColor);
                    break;

                case Domain.Models.ValueObjects.AnnotationType.Circle:
                    AddCircleAnnotation(pageFacade, pdfRect, content, author, annotationColor);
                    break;

                case Domain.Models.ValueObjects.AnnotationType.Square:
                    AddSquareAnnotation(pageFacade, pdfRect, content, author, annotationColor);
                    break;

                default:
                    throw new BadRequestException($"Unsupported annotation type: {annotationType}");
            }

            // 8. Save annotated PDF
            using var outputStream = new MemoryStream();
            processor.SaveDocument(outputStream);
            annotatedPdfBytes = outputStream.ToArray();
        }
        catch (BadRequestException)
        {
            throw; // Re-throw our own exceptions
        }
        catch (Exception ex)
        {
            throw new BadRequestException($"Failed to add annotation: {ex.Message}");
        }

        return await Task.FromResult(annotatedPdfBytes);
    }

    private void AddTextMarkupAnnotation(
        PdfPageFacade pageFacade,
        PdfRectangle rectangle,
        Domain.Models.ValueObjects.TextMarkupStyle style,
        string? content,
        string? author,
        PdfRGBColor color)
    {
        // Map our enum to DevExpress enum
        var devExpressStyle = style switch
        {
            Domain.Models.ValueObjects.TextMarkupStyle.Highlight => PdfTextMarkupAnnotationType.Highlight,
            Domain.Models.ValueObjects.TextMarkupStyle.Underline => PdfTextMarkupAnnotationType.Underline,
            Domain.Models.ValueObjects.TextMarkupStyle.Strikeout => PdfTextMarkupAnnotationType.StrikeOut,
            _ => throw new BadRequestException($"Unsupported text markup style: {style}")
        };

        var annotation = pageFacade.AddTextMarkupAnnotation(rectangle, devExpressStyle);
        
        if (annotation != null)
        {
            annotation.Color = color;
            if (!string.IsNullOrWhiteSpace(author))
                annotation.Author = author;
            if (!string.IsNullOrWhiteSpace(content))
                annotation.Contents = content;
        }
    }

    private void AddFreeTextAnnotation(
        PdfPageFacade pageFacade,
        PdfRectangle rectangle,
        string content,
        string? author,
        PdfRGBColor color)
    {
        var annotation = pageFacade.AddFreeTextAnnotation(rectangle, content);
        
        if (annotation != null)
        {
            annotation.Color = color;
            if (!string.IsNullOrWhiteSpace(author))
                annotation.Author = author;
        }
    }

    private void AddStickyNoteAnnotation(
        PdfPageFacade pageFacade,
        PdfRectangle rectangle,
        string content,
        string? author,
        PdfRGBColor color)
    {
        // Sticky note uses a point (top-left corner of rectangle)
        var point = new PdfPoint(rectangle.Left, rectangle.Top);
        var annotation = pageFacade.AddTextAnnotation(point);
        
        if (annotation != null)
        {
            annotation.Color = color;
            annotation.Contents = content;
            if (!string.IsNullOrWhiteSpace(author))
                annotation.Author = author;
        }
    }

    private void AddCircleAnnotation(
        PdfPageFacade pageFacade,
        PdfRectangle rectangle,
        string? content,
        string? author,
        PdfRGBColor color)
    {
        var annotation = pageFacade.AddCircleAnnotation(rectangle);
        
        if (annotation != null)
        {
            annotation.Color = color;
            if (!string.IsNullOrWhiteSpace(author))
                annotation.Author = author;
            if (!string.IsNullOrWhiteSpace(content))
                annotation.Contents = content;
        }
    }

    private void AddSquareAnnotation(
        PdfPageFacade pageFacade,
        PdfRectangle rectangle,
        string? content,
        string? author,
        PdfRGBColor color)
    {
        var annotation = pageFacade.AddSquareAnnotation(rectangle);
        
        if (annotation != null)
        {
            annotation.Color = color;
            if (!string.IsNullOrWhiteSpace(author))
                annotation.Author = author;
            if (!string.IsNullOrWhiteSpace(content))
                annotation.Contents = content;
        }
    }

    /// <summary>
    /// Parses hex color string (e.g., "FF0000" for red) to PdfRGBColor
    /// </summary>
    private PdfRGBColor? ParseColor(string? hexColor)
    {
        if (string.IsNullOrWhiteSpace(hexColor))
            return null;

        try
        {
            // Remove '#' if present
            hexColor = hexColor.TrimStart('#');

            if (hexColor.Length != 6)
                throw new BadRequestException($"Color must be 6-digit hex (e.g., 'FF0000'), got: '{hexColor}'");

            int r = Convert.ToInt32(hexColor.Substring(0, 2), 16);
            int g = Convert.ToInt32(hexColor.Substring(2, 2), 16);
            int b = Convert.ToInt32(hexColor.Substring(4, 2), 16);

            return new PdfRGBColor(r / 255.0, g / 255.0, b / 255.0);
        }
        catch (Exception ex)
        {
            throw new BadRequestException($"Invalid color format: '{hexColor}'. Expected 6-digit hex (e.g., 'FF0000'). Error: {ex.Message}");
        }
    }

    #endregion

    #region Private Helpers

    /// <summary>
    /// Detects if PDF is encrypted by scanning for /Encrypt keyword.
    /// </summary>
    private static bool DetectEncryption(byte[] pdfBytes)
    {
        string pdfText = System.Text.Encoding.ASCII.GetString(pdfBytes);
        return pdfText.Contains("/Encrypt", StringComparison.Ordinal);
    }

    /// <summary>
    /// Detects PDF/A conformance level by scanning PDF metadata.
    /// PDF/A documents contain an XMP metadata stream with pdfaid:conformance and pdfaid:part.
    /// </summary>
    private static (bool isPdfACompliant, string? pdfaVersion) DetectPdfAConformance(byte[] pdfBytes)
    {
        string pdfText = System.Text.Encoding.ASCII.GetString(pdfBytes);

        // Look for PDF/A identifier in XMP metadata
        // Example: <pdfaid:part>1</pdfaid:part><pdfaid:conformance>B</pdfaid:conformance>
        if (pdfText.Contains("pdfaid:part", StringComparison.Ordinal))
        {
            // Try to extract part and conformance level
            var partMatch = System.Text.RegularExpressions.Regex.Match(pdfText, @"pdfaid:part>(\d+)</pdfaid:part");
            var conformanceMatch = System.Text.RegularExpressions.Regex.Match(pdfText, @"pdfaid:conformance>([ABU])</pdfaid:conformance");

            if (partMatch.Success && conformanceMatch.Success)
            {
                string part = partMatch.Groups[1].Value; // "1", "2", "3"
                string conformance = conformanceMatch.Groups[1].Value; // "A", "B", "U"
                string pdfaVersion = $"PDF/A-{part}{conformance.ToLower()}";
                return (true, pdfaVersion);
            }

            // Found pdfaid:part but couldn't parse details
            return (true, "PDF/A (unknown level)");
        }

        return (false, null);
    }

    /// <summary>
    /// Detects embedded files in PDF by scanning raw PDF data for /EmbeddedFiles keyword
    /// and parsing the name tree to count attachments.
    /// This is a pragmatic approach as DevExpress PdfDocument API doesn't expose EmbeddedFiles directly.
    /// </summary>
    /// <param name="pdfBytes">PDF raw bytes</param>
    /// <returns>Tuple: (hasAttachments, attachmentCount)</returns>
    private static (bool hasAttachments, int attachmentCount) DetectEmbeddedFiles(byte[] pdfBytes)
    {
        // PDF embedded files are declared in the document catalog:
        // /Names << /EmbeddedFiles << /Names [...] >> >>
        // The /Names array contains pairs: [name1, filespec1, name2, filespec2, ...]

        string pdfText = System.Text.Encoding.ASCII.GetString(pdfBytes);

        // Search for /EmbeddedFiles in the context of /Names dictionary
        // Must appear after a /Names keyword to be valid
        int searchStart = 0;

        while (true)
        {
            // Find next occurrence of /EmbeddedFiles
            int embeddedFilesIndex = pdfText.IndexOf("/EmbeddedFiles", searchStart, StringComparison.Ordinal);

            if (embeddedFilesIndex == -1)
                return (false, 0); // Not found

            // Check if there's a /Names keyword BEFORE this /EmbeddedFiles
            // within a reasonable distance (e.g., within the same PDF object, max 5000 chars back)
            int contextStart = Math.Max(0, embeddedFilesIndex - 5000);
            string contextBefore = pdfText.Substring(contextStart, embeddedFilesIndex - contextStart);

            // Look for /Names in the context before /EmbeddedFiles
            int lastNamesIndex = contextBefore.LastIndexOf("/Names", StringComparison.Ordinal);

            if (lastNamesIndex != -1)
            {
                // Found /Names before /EmbeddedFiles - this is likely a valid embedded files declaration
                // Now try to parse the /Names array
                int namesArrayStart = pdfText.IndexOf("/Names", embeddedFilesIndex, StringComparison.Ordinal);
                if (namesArrayStart == -1)
                    return (true, 0); // Has EmbeddedFiles but can't count

                int arrayStart = pdfText.IndexOf('[', namesArrayStart);
                if (arrayStart == -1)
                    return (true, 0); // Has EmbeddedFiles but can't count

                int arrayEnd = pdfText.IndexOf(']', arrayStart);
                if (arrayEnd == -1)
                    return (true, 0); // Has EmbeddedFiles but can't count

                // Extract array content and count entries
                string arrayContent = pdfText.Substring(arrayStart + 1, arrayEnd - arrayStart - 1);

                // Count object references in array
                // The /Names array contains pairs: (filename) objectReference (filename) objectReference ...
                // Each object reference (pattern: "123 0 R") points to one embedded file
                // So the number of object references = number of attachments
                int objectCount = System.Text.RegularExpressions.Regex.Matches(arrayContent, @"\d+ \d+ R").Count;

                return (true, Math.Max(1, objectCount)); // At least 1 if EmbeddedFiles found
            }

            // This /EmbeddedFiles was not in the right context, search for next occurrence
            searchStart = embeddedFilesIndex + 1;
        }
    }

    /// <summary>
    /// Parses page range string into list of zero-based page indices.
    /// </summary>
    /// <param name="pageRange">Page range string (e.g., "1-3,5" or "1,3,5")</param>
    /// <param name="totalPages">Total page count in PDF (for validation)</param>
    /// <returns>List of zero-based page indices</returns>
    /// <exception cref="BadRequestException">Invalid format or page number out of range</exception>
    private static List<int> ParsePageRange(string pageRange, int totalPages)
    {
        var pageIndices = new HashSet<int>(); // Use HashSet to avoid duplicates

        try
        {
            // Split by comma
            string[] parts = pageRange.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);

            foreach (string part in parts)
            {
                if (part.Contains('-'))
                {
                    // Range format: "1-3"
                    string[] rangeParts = part.Split('-', StringSplitOptions.TrimEntries);
                    if (rangeParts.Length != 2)
                        throw new BadRequestException($"Invalid page range format: '{part}'. Expected format: '1-3'");

                    if (!int.TryParse(rangeParts[0], out int start) || !int.TryParse(rangeParts[1], out int end))
                        throw new BadRequestException($"Invalid page numbers in range: '{part}'");

                    if (start < 1 || end < 1)
                        throw new BadRequestException($"Page numbers must be >= 1 in range: '{part}'");

                    if (start > end)
                        throw new BadRequestException($"Start page must be <= end page in range: '{part}'");

                    if (start > totalPages || end > totalPages)
                        throw new BadRequestException($"Page range '{part}' exceeds document page count ({totalPages})");

                    // Add pages (convert to zero-based indices)
                    for (int i = start; i <= end; i++)
                        pageIndices.Add(i - 1);
                }
                else
                {
                    // Single page: "5"
                    if (!int.TryParse(part, out int pageNum))
                        throw new BadRequestException($"Invalid page number: '{part}'");

                    if (pageNum < 1)
                        throw new BadRequestException($"Page number must be >= 1: '{part}'");

                    if (pageNum > totalPages)
                        throw new BadRequestException($"Page number {pageNum} exceeds document page count ({totalPages})");

                    pageIndices.Add(pageNum - 1); // Convert to zero-based index
                }
            }
        }
        catch (BadRequestException)
        {
            throw; // Re-throw BadRequestException as-is
        }
        catch (Exception ex)
        {
            throw new BadRequestException($"Invalid page range format: '{pageRange}'. Error: {ex.Message}");
        }

        if (pageIndices.Count == 0)
            throw new BadRequestException($"Page range '{pageRange}' resulted in no pages");

        return pageIndices.OrderBy(x => x).ToList(); // Return sorted list
    }

    #endregion

    #region AddStampAsync

    public async Task<byte[]> AddStampAsync(
        Stream pdfStream,
        Domain.Models.ValueObjects.StampType stampType,
        int[]? pageNumbers,
        (double X, double Y) position,
        (double Width, double Height)? size = null,
        Domain.Models.ValueObjects.AnnotationOrigin origin = Domain.Models.ValueObjects.AnnotationOrigin.BottomLeft,
        string? text = null,
        string? fontName = null,
        double? fontSize = null,
        string? color = null,
        double? opacity = null,
        double? rotation = null,
        Domain.Models.ValueObjects.StampPlacement placement = Domain.Models.ValueObjects.StampPlacement.Foreground,
        byte[]? imageBytes = null,
        Domain.Models.ValueObjects.PredefinedStampType? predefinedType = null)
    {
        // 1. Validate stream
        if (pdfStream == null || pdfStream.Length == 0)
            throw new BadRequestException("PDF stream cannot be null or empty");

        if (pdfStream.Position != 0)
            throw new BadRequestException("PDF stream position must be 0");

        // 2. Validate stamp type requirements
        ValidateStampParameters(stampType, text, imageBytes, predefinedType);

        // 3. Validate optional parameters
        if (opacity.HasValue && (opacity.Value < 0.0 || opacity.Value > 1.0))
            throw new BadRequestException("Opacity must be between 0.0 and 1.0");

        if (rotation.HasValue && (rotation.Value < 0 || rotation.Value > 360))
            throw new BadRequestException("Rotation must be between 0 and 360 degrees");

        if (fontSize.HasValue && fontSize.Value <= 0)
            throw new BadRequestException("Font size must be positive");

        // 4. Load PDF and calculate target pages
        using var processor = new PdfDocumentProcessor();
        try
        {
            processor.LoadDocument(pdfStream);
        }
        catch (Exception ex)
        {
            throw new BadRequestException($"Failed to load PDF document: {ex.Message}");
        }

        int totalPages = processor.Document.Pages.Count;
        int[] targetPages = CalculateTargetPages(totalPages, pageNumbers);

        // 5. Apply stamp to each target page
        foreach (int pageIndex in targetPages)
        {
            double pageHeight = processor.Document.Pages[pageIndex].CropBox.Height;

            // Convert position if origin is TopLeft
            var stampPosition = origin == Domain.Models.ValueObjects.AnnotationOrigin.TopLeft
                ? (position.X, pageHeight - position.Y)
                : position;

            // Apply stamp based on type
            switch (stampType)
            {
                case Domain.Models.ValueObjects.StampType.Text:
                    AddTextStamp(processor, pageIndex, stampPosition, text!, fontName, fontSize, color, opacity, rotation, placement, size);
                    break;

                case Domain.Models.ValueObjects.StampType.Image:
                    AddImageStamp(processor, pageIndex, stampPosition, imageBytes!, opacity, rotation, placement, size);
                    break;

                case Domain.Models.ValueObjects.StampType.Predefined:
                    AddPredefinedStamp(processor, pageIndex, stampPosition, predefinedType!.Value, opacity, rotation, placement, size);
                    break;
            }
        }

        // 6. Save to byte array
        using var outputStream = new MemoryStream();
        processor.SaveDocument(outputStream);
        return await Task.FromResult(outputStream.ToArray());
    }

    private void ValidateStampParameters(
        Domain.Models.ValueObjects.StampType stampType,
        string? text,
        byte[]? imageBytes,
        Domain.Models.ValueObjects.PredefinedStampType? predefinedType)
    {
        switch (stampType)
        {
            case Domain.Models.ValueObjects.StampType.Text:
                if (string.IsNullOrWhiteSpace(text))
                    throw new BadRequestException("Text is required for Text stamp type");
                break;

            case Domain.Models.ValueObjects.StampType.Image:
                if (imageBytes == null || imageBytes.Length == 0)
                    throw new BadRequestException("Image bytes are required for Image stamp type");
                break;

            case Domain.Models.ValueObjects.StampType.Predefined:
                if (!predefinedType.HasValue)
                    throw new BadRequestException("Predefined type is required for Predefined stamp type");
                break;
        }
    }

    private int[] CalculateTargetPages(int totalPages, int[]? pageNumbers)
    {
        // null = all pages
        if (pageNumbers == null)
            return Enumerable.Range(0, totalPages).ToArray();

        // Validate page numbers (1-based)
        foreach (int pageNum in pageNumbers)
        {
            if (pageNum < 1 || pageNum > totalPages)
                throw new BadRequestException($"Page number {pageNum} is out of range (1-{totalPages})");
        }

        // Convert to zero-based indices
        return pageNumbers.Select(p => p - 1).Distinct().OrderBy(p => p).ToArray();
    }

    private void AddTextStamp(
        PdfDocumentProcessor processor,
        int pageIndex,
        (double X, double Y) position,
        string text,
        string? fontName,
        double? fontSize,
        string? color,
        double? opacity,
        double? rotation,
        Domain.Models.ValueObjects.StampPlacement placement,
        (double Width, double Height)? size)
    {
        using var graphics = processor.CreateGraphicsPageSystem();

        // Get page object
        PdfPage page = processor.Document.Pages[pageIndex];

        // Parse color (default: black)
        var pdfColor = ParseColor(color) ?? new PdfRGBColor(0, 0, 0);

        // Apply opacity (default: 0.5) by creating color with alpha channel
        double alpha = opacity ?? 0.5;
        Color drawColor = Color.FromArgb((int)(alpha * 255), (int)(pdfColor.R * 255), (int)(pdfColor.G * 255), (int)(pdfColor.B * 255));

        // Create font (default: Arial, 12pt)
        var font = new DXFont(fontName ?? "Arial", (float)(fontSize ?? 12));

        // Calculate bounds
        var bounds = size.HasValue
            ? new RectangleF((float)position.X, (float)position.Y, (float)size.Value.Width, (float)size.Value.Height)
            : new RectangleF((float)position.X, (float)position.Y, 200, 50); // Default size

        // Apply rotation if specified (around origin, not center point)
        if (rotation.HasValue && rotation.Value > 0)
        {
            // Translate to position, rotate, translate back
            graphics.TranslateTransform((float)position.X, (float)position.Y);
            graphics.RotateTransform((float)rotation.Value);
            graphics.TranslateTransform(-(float)position.X, -(float)position.Y);
        }

        // Draw text
        graphics.DrawString(text, font, new DXSolidBrush(drawColor), bounds);

        // Add graphics to page (foreground or background)
        if (placement == Domain.Models.ValueObjects.StampPlacement.Foreground)
            graphics.AddToPageForeground(page);
        else
            graphics.AddToPageBackground(page);
    }

    private void AddImageStamp(
        PdfDocumentProcessor processor,
        int pageIndex,
        (double X, double Y) position,
        byte[] imageBytes,
        double? opacity,
        double? rotation,
        Domain.Models.ValueObjects.StampPlacement placement,
        (double Width, double Height)? size)
    {
        using var graphics = processor.CreateGraphicsPageSystem();

        // Get page object
        PdfPage page = processor.Document.Pages[pageIndex];

        // Draw image directly from byte array
        try
        {
            PointF point = new PointF((float)position.X, (float)position.Y);

            // Apply rotation if specified
            if (rotation.HasValue && rotation.Value > 0)
            {
                graphics.TranslateTransform((float)position.X, (float)position.Y);
                graphics.RotateTransform((float)rotation.Value);
                graphics.TranslateTransform(-(float)position.X, -(float)position.Y);
            }

            // Draw image (DevExpress.Pdf.PdfGraphics.DrawImage accepts byte[] directly)
            // Note: Size parameter is ignored for now (DrawImage auto-sizes based on image dimensions)
            // If size is needed, we'd need to use DXImage.FromStream and resize
            graphics.DrawImage(imageBytes, point);

            // Add graphics to page
            if (placement == Domain.Models.ValueObjects.StampPlacement.Foreground)
                graphics.AddToPageForeground(page);
            else
                graphics.AddToPageBackground(page);
        }
        catch (Exception ex)
        {
            throw new BadRequestException($"Invalid image format or failed to draw image: {ex.Message}");
        }
    }

    private void AddPredefinedStamp(
        PdfDocumentProcessor processor,
        int pageIndex,
        (double X, double Y) position,
        Domain.Models.ValueObjects.PredefinedStampType predefinedType,
        double? opacity,
        double? rotation,
        Domain.Models.ValueObjects.StampPlacement placement,
        (double Width, double Height)? size)
    {
        // Get predefined stamp configuration
        var (text, color, fontSize, fontStyle) = GetPredefinedStampConfig(predefinedType);

        // Delegate to AddTextStamp with predefined parameters
        AddTextStamp(processor, pageIndex, position, text, "Arial", fontSize, color, opacity, rotation, placement, size);
    }

    private (string Text, string Color, double FontSize, string FontStyle) GetPredefinedStampConfig(
        Domain.Models.ValueObjects.PredefinedStampType predefinedType)
    {
        return predefinedType switch
        {
            Domain.Models.ValueObjects.PredefinedStampType.Confidential => ("CONFIDENTIAL", "FF0000", 24, "Bold"),
            Domain.Models.ValueObjects.PredefinedStampType.Approved => ("APPROVED", "00AA00", 24, "Bold"),
            Domain.Models.ValueObjects.PredefinedStampType.Draft => ("DRAFT", "808080", 24, "Italic"),
            Domain.Models.ValueObjects.PredefinedStampType.Void => ("VOID", "FF0000", 32, "Bold"),
            Domain.Models.ValueObjects.PredefinedStampType.ForReview => ("FOR REVIEW", "FFA500", 20, "Bold"),
            _ => throw new BadRequestException($"Unsupported predefined stamp type: {predefinedType}")
        };
    }

    #endregion
}
