using Codecrete.SwissQRBill.Generator;
using DevExpress.Drawing;
using DevExpress.Pdf;
using DocumentOperator.Application.Common.Interfaces;
using DocumentOperator.Domain.Common.Exceptions;
using SkiaSharp;
using SkiaSharp.QrCode;
using System.Collections.Concurrent;

namespace DocumentOperator.Infrastructure.Services.QrCodeProcessing;

/// <summary>
/// Swiss QR Code processor using DevExpress PDF API for image extraction,
/// SkiaSharp.QrCode for QR decoding, and Codecrete.SwissQRBill.Generator for Swiss QR parsing.
/// 
/// Strategy:
/// 1. Extract all embedded images from PDF using GetDXImages()
/// 2. Try to decode QR code from each image using SkiaSharp.QrCode
/// 3. Parse Swiss QR format with Codecrete library
/// 4. Return both parsed Bill and raw QR text lines
/// </summary>
public sealed class DevExpressSwissQrCodeProcessor : ISwissQrCodeProcessor
{

    /// <inheritdoc />
    public async Task<(Bill Bill, string[] RawLines)> ExtractSwissQrCodeAsync(
        Stream pdfStream,
        int[]? pageNumbers = null,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(pdfStream, nameof(pdfStream));
        
        if (pdfStream.Length == 0)
            throw new ArgumentException("PDF stream is empty.", nameof(pdfStream));

        // Defensive validation: Seekable streams must be at Position = 0
        // Non-seekable streams (e.g., NetworkStream) are not checked
        if (pdfStream.CanSeek && pdfStream.Position != 0)
            throw new BadRequestException(null, new ArgumentException(
                "PDF stream must be positioned at the beginning (Position = 0).",
                nameof(pdfStream)));

        using var pdfDocument = new PdfDocumentProcessor();
        pdfDocument.LoadDocument(pdfStream);

        if (pdfDocument.Document.Pages.Count == 0)
            throw new ArgumentException("PDF document contains no pages.", nameof(pdfStream));

        // Determine which pages to scan
        int[] pagesToScan = DeterminePageNumbers(pdfDocument.Document.Pages.Count, pageNumbers);

        // Extract all images from specified pages (no pre-filtering)
        var allImages = new ConcurrentBag<(int pageNumber, DXBitmap image)>();

        foreach (int pageNumber in pagesToScan)
        {
            cancellationToken.ThrowIfCancellationRequested();

            // Define area: entire page
            var page = pdfDocument.Document.Pages[pageNumber - 1];
            var area = new PdfDocumentArea(pageNumber,
                new PdfRectangle(0, 0, page.CropBox.Width, page.CropBox.Height));

            // Extract images from this page
            var images = pdfDocument.GetDXImages(area);

            foreach (var image in images)
            {
                // Collect ALL images - QRCodeDecoder will determine if it's a QR code
                allImages.Add((pageNumber, image));
            }
        }

        if (allImages.IsEmpty)
            throw new NotFoundException($"No images found in pages: {string.Join(", ", pagesToScan)}");

        // Parallel scan all images
        var qrCodeTasks = allImages.Select(imageData =>
            Task.Run(() =>
            {
                using (imageData.image)
                {
                    string? qrText = DecodeQrCodeFromImage(imageData.image);
                    if (!string.IsNullOrEmpty(qrText))
                    {
                        try
                        {
                            // Parse with Codecrete
                            var bill = QRBill.DecodeQrCodeText(qrText);

                            // Split raw text into lines (handle both \r\n and \n)
                            // Remove leading/trailing \r and \n from each line
                            var rawLines = qrText
                                .Split(["\r\n", "\n"], StringSplitOptions.None)
                                .Select(line => line.Trim('\r', '\n'))
                                .ToArray();

                            return (success: true, bill, rawLines, imageData.pageNumber);
                        }
                        catch
                        {
                            // Not a valid Swiss QR Bill, ignore
                            return (success: false, bill: (Bill?)null, rawLines: (string[]?)null, pageNumber: 0);
                        }
                    }
                    return (success: false, bill: (Bill?)null, rawLines: (string[]?)null, pageNumber: 0);
                }
            }, cancellationToken)
        ).ToList();

        // Wait for all tasks and find first valid Swiss QR
        var results = await Task.WhenAll(qrCodeTasks);
        var validResult = results.FirstOrDefault(r => r.success);

        if (validResult.success && validResult.bill != null)
        {
            return (validResult.bill, validResult.rawLines!);
        }

        throw new NotFoundException(
            $"No valid Swiss QR Code found in {allImages.Count} images across pages: {string.Join(", ", pagesToScan)}.");
    }

    /// <summary>
    /// Determines which pages to scan based on optional page numbers parameter.
    /// Default: Last page first, then all pages in reverse order.
    /// </summary>
    private static int[] DeterminePageNumbers(int totalPages, int[]? pageNumbers)
    {
        if (pageNumbers != null && pageNumbers.Length > 0)
        {
            // Validate page numbers
            foreach (int pageNum in pageNumbers)
                if (pageNum < 1 || pageNum > totalPages)
                    throw new ArgumentException(
                        $"Invalid page number {pageNum}. Document has {totalPages} pages.",
                        nameof(pageNumbers));
            return pageNumbers;
        }

        // Default: Scan all pages, last page first (Swiss QR standard)
        return [.. Enumerable.Range(1, totalPages).OrderByDescending(p => p)];
    }

    /// <summary>
    /// Decodes QR code from a DXBitmap image using SkiaSharp.QrCode.
    /// Converts DXBitmap to SKBitmap and attempts decoding.
    /// </summary>
    private static string? DecodeQrCodeFromImage(DXBitmap dxImage)
    {
        // Convert DXBitmap to SKBitmap via MemoryStream (PNG format)
        using var ms = new MemoryStream();
        dxImage.Save(ms, DXImageFormat.Png);
        ms.Position = 0;

        // Decode using SkiaSharp.QrCode
        using var skBitmap = SKBitmap.Decode(ms);
        if (skBitmap == null)
            return null;

        // TryDecode returns true if QR code found and decoded successfully
        bool success = QRCodeDecoder.TryDecode(skBitmap, out var text, out _);
        return success ? text : null;
    }
}
