using DevExpress.Pdf;
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 and metadata extraction.
/// </summary>
public class DevExpressPdfProcessor : IPdfProcessor
{
    /// <summary>
    /// Validates a PDF document and returns metadata.
    /// </summary>
    /// <param name="pdfBytes">PDF content as byte array</param>
    /// <returns>PDF metadata (page count, file size, version, etc.)</returns>
    /// <exception cref="PdfProcessingException">Thrown when PDF is invalid or null</exception>
    public async Task<DocumentOperator.Domain.Models.ValueObjects.PdfMetadata> ValidateAsync(byte[] pdfBytes)
    {
        // 1. Input Validation (Defensive Programming)
        if (pdfBytes == null)
        {
            throw new PdfProcessingException("PDF bytes cannot be null");
        }

        if (pdfBytes.Length == 0)
        {
            throw new PdfProcessingException("PDF bytes cannot be empty");
        }

        try
        {
            // 2. Load PDF with DevExpress Document API (PdfDocumentProcessor)
            using var processor = new PdfDocumentProcessor();
            processor.LoadDocument(new MemoryStream(pdfBytes));

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

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

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

            // 4. Create and return PdfMetadata Value Object (fully qualified name!)
            return new DocumentOperator.Domain.Models.ValueObjects.PdfMetadata(
                pageCount: pageCount,
                fileSizeBytes: pdfBytes.Length,
                pdfVersion: pdfVersion,
                hasAttachments: hasAttachments,
                attachmentCount: attachmentCount
            );
        }
        catch (Exception ex) when (ex is not PdfProcessingException)
        {
            // Wrap DevExpress exceptions in our domain exception
            throw new PdfProcessingException(
                $"Failed to validate PDF: {ex.Message}",
                ex);
        }
    }

    /// <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;
        }
    }
}
