
using DocumentFormat.OpenXml.Vml;
using FruitBank.Common;
using FruitBank.Common.Dtos;
using FruitBank.Common.Entities;
using FruitBank.Common.Interfaces;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Nop.Core;
using Nop.Core.Domain.Catalog;
using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer;
using Nop.Plugin.Misc.FruitBankPlugin.Helpers;
using Nop.Plugin.Misc.FruitBankPlugin.Services;
using Nop.Plugin.Misc.FruitBankPlugin.Services.FileStorage;
using Nop.Services.Catalog;
using Nop.Services.Common;
using Nop.Services.Security;
using Nop.Web.Framework;
using Nop.Web.Framework.Controllers;
using Nop.Web.Framework.Mvc.Filters;
using PDFtoImage;
using SkiaSharp;


namespace Nop.Plugin.Misc.FruitBank.Controllers
{
    [AuthorizeAdmin]
    [Area(AreaNames.ADMIN)]
    [AutoValidateAntiforgeryToken]
    public class FileManagerController : BasePluginController
    {
        private readonly IPermissionService _permissionService;
        private readonly OpenAIApiService _aiApiService;
        private readonly CerebrasAPIService _cerebrasApiService;
        private readonly AICalculationService _aiCalculationService;
        private readonly IProductService _productService;
        private readonly FruitBankDbContext _dbContext;
        private readonly PdfToImageService _pdfToImageService;
        private readonly IWorkContext _workContext;
        private readonly FileStorageService _fileStorageService;
        private readonly FruitBankAttributeService _fruitBankAttributeService;
        private readonly IStoreContext _storeContext;
        private readonly PreOrderConversionService _preorderConversionService;

        public FileManagerController(
            IPermissionService permissionService,
            OpenAIApiService aiApiService,
            CerebrasAPIService cerebrasApiService,
            AICalculationService aiCalculationService,
            IProductService productService,
            FruitBankDbContext fruitBankDbContext,
            PdfToImageService pdfToImageService,
            IWorkContext workContext,
            FileStorageService fileStorageService,
            FruitBankAttributeService fruitBankAttributeService,
            IStoreContext storeContext,
            PreOrderConversionService preorderConversionService)
        {
            _permissionService = permissionService;
            _aiApiService = aiApiService;
            _cerebrasApiService = cerebrasApiService;
            _aiCalculationService = aiCalculationService;
            _productService = productService;
            _dbContext = fruitBankDbContext;
            _pdfToImageService = pdfToImageService;
            _workContext = workContext;
            _fileStorageService = fileStorageService;
            _fruitBankAttributeService = fruitBankAttributeService;
            _storeContext = storeContext;
            _preorderConversionService = preorderConversionService;
        }

        /// <summary>
        /// Display the image text extraction page
        /// </summary>
        public async Task<IActionResult> ImageTextExtraction()
        {
            if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
                return AccessDeniedView();

            return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Extras/ImageTextExtraction.cshtml");
        }

        /// <summary>
        /// Endpoint to extract text from uploaded image with duplicate detection
        /// FIXED: Removed EF Core .Include() dependency
        /// </summary>
        [HttpPost]
        public async Task<IActionResult> ExtractTextFromImage(IFormFile imageFile, string customPrompt = null)
        {
            if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
                return Json(new { success = false, message = "Access denied" });

            if (imageFile == null || imageFile.Length == 0)
            {
                return Json(new { success = false, message = "No file received" });
            }

            // Validate file type - now including PDF
            var extension = System.IO.Path.GetExtension(imageFile.FileName).ToLowerInvariant();
            if (extension != ".jpg" && extension != ".jpeg" && extension != ".png" &&
                extension != ".gif" && extension != ".webp" && extension != ".pdf")
            {
                return Json(new { success = false, message = "Invalid file type. Please upload JPG, PNG, GIF, WebP, or PDF." });
            }

            try
            {
                // ✅ STEP 1: Calculate file hash FIRST to check for duplicates
                string fileHash;
                using (var hashStream = imageFile.OpenReadStream())
                {
                    using (var sha256 = System.Security.Cryptography.SHA256.Create())
                    {
                        var hashBytes = await Task.Run(() => sha256.ComputeHash(hashStream));
                        fileHash = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
                    }
                }

                Console.WriteLine($"📝 Uploaded file hash: {fileHash}");

                // ✅ STEP 2: Check if this file has already been processed
                var existingFile = await _dbContext.Files
                    .GetAll()
                    .FirstOrDefaultAsync(f => f.FileHash == fileHash);

                if (existingFile != null)
                {
                    Console.WriteLine($"♻️ Duplicate file detected! File ID: {existingFile.Id}");

                    // Find the shipping document(s) associated with this file
                    var existingMappingList = await _dbContext.ShippingDocumentToFiles
                        .GetAll()
                        .Where(m => m.FilesId == existingFile.Id)
                        .OrderByDescending(m => m.Created).ToListAsync();
                    var existingMapping = existingMappingList.FirstOrDefault();

                    if (existingMapping != null)
                    {
                        // ✅ Load the existing shipping document (without .Include())
                        var existingShippingDoc = await _dbContext.ShippingDocuments
                            .GetByIdAsync(existingMapping.ShippingDocumentId);

                        if (existingShippingDoc != null)
                        {
                            Console.WriteLine($"✅ Found existing shipping document - ID: {existingShippingDoc.Id}");

                            // ✅ Manually load related data
                            var shippingItems = await _dbContext.ShippingItems
                                .GetAll()
                                .Where(si => si.ShippingDocumentId == existingShippingDoc.Id)
                                .ToListAsync();

                            Partner matchedPartner = null;
                            if (existingShippingDoc.PartnerId > 0)
                            {
                                matchedPartner = await _dbContext.Partners.GetByIdAsync(existingShippingDoc.PartnerId);
                            }

                            // Return the existing data instead of re-processing
                            return Json(new
                            {
                                success = true,
                                isDuplicate = true, // ✅ Flag so frontend can show warning
                                message = $"This document was already processed on {existingShippingDoc.ShippingDate.ToString("yyyy-MM-dd HH:mm")}. Loaded existing data.",
                                existingDocumentId = existingShippingDoc.Id,
                                shippingDocument = new
                                {
                                    documentIdNumber = existingShippingDoc.DocumentIdNumber,
                                    partnerId = existingShippingDoc.PartnerId,
                                    partnerName = matchedPartner?.Name,
                                    partnerTaxId = matchedPartner?.TaxId,
                                    pdfFileName = existingShippingDoc.PdfFileName,
                                    totalPallets = existingShippingDoc.TotalPallets,
                                    shippingItems = shippingItems?.Select(item => new
                                    {
                                        name = item.Name,
                                        hungarianName = item.HungarianName,
                                        nameOnDocument = item.NameOnDocument,
                                        productId = item.ProductId,
                                        palletsOnDocument = item.PalletsOnDocument,
                                        quantityOnDocument = item.QuantityOnDocument,
                                        netWeightOnDocument = item.NetWeightOnDocument,
                                        grossWeightOnDocument = item.GrossWeightOnDocument,
                                        unitPriceOnDocument = item.UnitPriceOnDocument
                                    }).ToList(),
                                    extractedText = existingFile.RawText
                                },
                                fileName = existingFile.FileName + existingFile.FileExtension,
                                fileSize = imageFile.Length,
                                wasConverted = extension == ".pdf"
                            });
                        }
                    }

                    // If we found the file but no shipping document, continue with extraction
                    Console.WriteLine("⚠️ File exists but no shipping document found. Continuing with extraction...");
                }

                // ✅ STEP 3: File is new or has no associated document - proceed with extraction
                Console.WriteLine("🆕 New file detected. Starting AI extraction...");

                ShippingDocument shippingDocument = new ShippingDocument();
                shippingDocument.ShippingItems = new List<ShippingItem>();

                // Define the uploads folder
                var uploadsFolder = System.IO.Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "uploads", "ocr");

                // Create directory if it doesn't exist
                if (!Directory.Exists(uploadsFolder))
                {
                    Directory.CreateDirectory(uploadsFolder);
                }

                string processedFilePath;
                string processedFileName;

                // Handle PDF conversion
                if (extension == ".pdf")
                {
                    // Save the PDF temporarily
                    var tempPdfFileName = $"temp_pdf_{DateTime.Now:yyyyMMdd_HHmmss}.pdf";
                    var tempPdfPath = System.IO.Path.Combine(uploadsFolder, tempPdfFileName);

                    using (var stream = new FileStream(tempPdfPath, FileMode.Create))
                    {
                        await imageFile.CopyToAsync(stream);
                    }

                    // Convert PDF to JPG using our service                    
                    var convertedImages = await _pdfToImageService.ConvertPdfToJpgAsync(tempPdfPath, uploadsFolder);

                    if (convertedImages == null || convertedImages.Count == 0)
                    {
                        // Clean up temp PDF
                        if (System.IO.File.Exists(tempPdfPath))
                            System.IO.File.Delete(tempPdfPath);

                        return Json(new { success = false, message = "Failed to convert PDF or PDF is empty" });
                    }

                    // Use the first page
                    processedFilePath = convertedImages[0];
                    processedFileName = System.IO.Path.GetFileName(processedFilePath);

                    // Clean up temp PDF
                    if (System.IO.File.Exists(tempPdfPath))
                        System.IO.File.Delete(tempPdfPath);
                }
                else
                {
                    // Handle regular image files
                    processedFileName = $"ocr_image_{DateTime.Now:yyyyMMdd_HHmmss}{extension}";
                    processedFilePath = System.IO.Path.Combine(uploadsFolder, processedFileName);

                    using (var stream = new FileStream(processedFilePath, FileMode.Create))
                    {
                        await imageFile.CopyToAsync(stream);
                    }
                }

                // Extract text from the processed image using OpenAI Vision API
                string extractedText;
                using (var imageStream = new FileStream(processedFilePath, FileMode.Open, FileAccess.Read))
                {
                    extractedText = await _aiApiService.ExtractTextFromImageAsync(
                        imageStream,
                        processedFileName,
                        customPrompt
                    );
                }

                if (string.IsNullOrEmpty(extractedText))
                {
                    return Json(new
                    {
                        success = false,
                        message = "Failed to extract text. The API may have returned an empty response."
                    });
                }

                OpenaiImageResponse deserializedContent = new();

                var result = TextHelper.FixJsonWithoutAI(extractedText);

                var options = new System.Text.Json.JsonSerializerOptions
                {
                    PropertyNameCaseInsensitive = true,
                    IncludeFields = true
                };

                try
                {
                    deserializedContent = System.Text.Json.JsonSerializer.Deserialize<OpenaiImageResponse>(result, options);

                    if (deserializedContent == null || deserializedContent.extractedData == null)
                    {
                        Console.Error.WriteLine($"Deserialization returned null. JSON was: {result}");
                    }
                }
                catch (Exception ex)
                {
                    Console.Error.WriteLine($"Error deserializing extracted text: {ex}");
                    Console.Error.WriteLine($"JSON content: {result}");
                }

                Console.WriteLine($"Document number analysis Result: {deserializedContent.extractedData.documentId}");

                shippingDocument.DocumentIdNumber = deserializedContent.extractedData.documentId;
                string partnerAnalysis = await ExtractPartnerName(extractedText);

                int? dbPartnerName = await DeterminePartner(partnerAnalysis);
                if (dbPartnerName != null)
                {
                    shippingDocument.PartnerId = (int)dbPartnerName;
                    Console.WriteLine($"Determined Partner ID: {dbPartnerName}");
                }
                else
                {
                    Console.WriteLine("No matching partner found in the database.");
                }

                Console.WriteLine($"Product analysis Result: {deserializedContent.extractedData.products}");

                // Identify products from database
                var allProducts = await _dbContext.ProductDtos.GetAll(true).ToListAsync();
                var historicalProducts = await _dbContext.ShippingItems.GetAll().ToListAsync();

                ProductReferenceResponse deserializedProducts = new ProductReferenceResponse();
                deserializedProducts.products = deserializedContent.extractedData.products;
                Console.WriteLine($"Serialized Products: {deserializedProducts.products.Count}");

                List<ShippingItem> matchedProducts = new List<ShippingItem>();
                matchedProducts = await DetermineProducts(allProducts, historicalProducts, deserializedProducts);

                shippingDocument.ShippingItems = matchedProducts;

                if (matchedProducts.Count > 0)
                {
                    Console.WriteLine($"Matched Products Count: {matchedProducts.Count}");
                    foreach (var matchedProduct in matchedProducts)
                    {
                        Console.WriteLine($"Matched Product: {matchedProduct.Name}");
                    }
                }
                else
                {
                    Console.WriteLine("No products matched from the database.");
                }

                shippingDocument.PdfFileName = processedFileName;

                // Calculate total pallets from shipping items
                shippingDocument.TotalPallets = shippingDocument.ShippingItems?.Sum(item => item.PalletsOnDocument) ?? 0;

                // Get partner details if matched
                Partner matchedPartner2 = null;
                if (shippingDocument.PartnerId > 0)
                {
                    matchedPartner2 = await _dbContext.Partners.GetByIdAsync(shippingDocument.PartnerId);
                }

                return Json(new
                {
                    success = true,
                    isDuplicate = false, // ✅ Flag indicating this is new extraction
                    message = extension == ".pdf"
                        ? "PDF converted and text extracted successfully"
                        : "Text extracted successfully",
                    shippingDocument = new
                    {
                        documentIdNumber = shippingDocument.DocumentIdNumber,
                        partnerId = shippingDocument.PartnerId,
                        partnerName = matchedPartner2?.Name,
                        partnerTaxId = matchedPartner2?.TaxId,
                        pdfFileName = shippingDocument.PdfFileName,
                        totalPallets = shippingDocument.TotalPallets,
                        shippingItems = shippingDocument.ShippingItems?.Select(item => new
                        {
                            name = item.Name,
                            hungarianName = item.HungarianName,
                            nameOnDocument = item.NameOnDocument,
                            productId = item.ProductId,
                            palletsOnDocument = item.PalletsOnDocument,
                            quantityOnDocument = item.QuantityOnDocument,
                            netWeightOnDocument = item.NetWeightOnDocument,
                            grossWeightOnDocument = item.GrossWeightOnDocument,
                            unitPriceOnDocument = item.UnitPriceOnDocument
                        }).ToList(),
                        extractedText = deserializedContent.extractedData.fullText
                    },
                    fileName = processedFileName,
                    filePath = processedFilePath,
                    fileSize = imageFile.Length,
                    wasConverted = extension == ".pdf"
                });
            }
            catch (Exception ex)
            {
                Console.Error.WriteLine($"Error in ExtractTextFromImage: {ex}");
                return Json(new
                {
                    success = false,
                    message = $"Error processing file: {ex.Message}"
                });
            }
        }

        private async Task<List<ShippingItem>> DetermineProducts(List<ProductDto> allProducts, List<ShippingItem> historicalProducts, ProductReferenceResponse deserializedProducts)
        {
            List<ShippingItem> finalMatchedProducts = new List<ShippingItem>();
            //List<ProductDto> allProductDtos = await _dbContext.ProductDtos.GetAll(true).ToListAsync();
            // Load all shipping items once
            var allShippingItems = await _dbContext.ShippingItems.GetAll(true).ToListAsync();

            foreach (var deserializedProduct in deserializedProducts.products)
            {
                ShippingItem matchedItem = null;

                // Step 1: Try exact historical match
                var historicalProduct = historicalProducts
                    .Where(hp => !string.IsNullOrEmpty(hp.NameOnDocument))
                    .FirstOrDefault(p => p.NameOnDocument.Equals(deserializedProduct.name, StringComparison.OrdinalIgnoreCase));

                if (historicalProduct != null)
                {
                    Console.WriteLine($"Historical product found: {historicalProduct.Name}");
                    var productDto = allProducts.FirstOrDefault(p => p.Id == historicalProduct.ProductId);

                    if (productDto != null)
                    {
                        matchedItem = CreateShippingItem(productDto, deserializedProduct);
                        Console.WriteLine($"Matched product from historical data: {productDto.Name}");
                    }
                }

                // Step 2: If no exact match, try AI matching with similar names
                if (matchedItem == null)
                {
                    // Get similar products (safe substring)
                    int substringLength = Math.Min(6, deserializedProduct.name?.Length ?? 0);

                    if (substringLength > 0)
                    {
                        var similarNameProducts = historicalProducts
                            .Where(p => !string.IsNullOrEmpty(p.NameOnDocument) &&
                                       p.NameOnDocument.Contains(deserializedProduct.name.Substring(0, substringLength), StringComparison.OrdinalIgnoreCase))
                            .ToList();

                        Console.WriteLine($"Similar products found for {deserializedProduct.name}: {similarNameProducts.Count}");

                        // Try AI match with similar historical products
                        if (similarNameProducts.Any())
                        {
                            //var aiMatchPrompt = $"You are an agent of Fruitbank to analyze product names and match them to existing products in the Fruitbank product catalog. " +
                            //    $"Given the following product catalog: {string.Join(", ", similarNameProducts.Select(p => p.NameOnDocument))}, " +
                            //    $"which product from the catalog best matches this product name: {deserializedProduct.name}. " +
                            //    $"Reply with NOTHING ELSE THAN the exact product name from the catalog, if no match found, reply with 'NONE'.";

                            var systemPrompt = "You are a product name matching specialist for FruitBank, a wholesale fruit and vegetable company.\n\n" +
    "Your task: Match a product name from a shipping document to the most similar product in our historical catalog.\n\n" +
    "MATCHING RULES:\n" +
    "1. Match based on ALL details including:\n" +
    "   - Product type (apples, bananas, oranges)\n" +
    "   - Variety (Golden Delicious, Cavendish, Valencia)\n" +
    "   - Quality grade (Class I, Class II, Extra, Premium)\n" +
    "   - Size markers (60+, 70+, 80+, Large, Small)\n" +
    "   - Packaging type if mentioned (Carton, Box, Loose)\n" +
    "2. Consider language variations:\n" +
    "   - Spanish: Manzanas = Apples, Plátanos = Bananas, Naranjas = Oranges\n" +
    "   - Hungarian: Alma = Apples, Banán = Bananas, Narancs = Oranges\n" +
    "   - Plural/singular: 'Bananas' = 'Banana'\n" +
    "3. Match as specifically as possible:\n" +
    "   - 'APPLES CLASS I 70+' should match 'APPLES CLASS I 70+' (not just 'APPLES')\n" +
    "   - 'ORANGES 60+' is different from 'ORANGES 70+'\n" +
    "   - 'TOMATOES EXTRA' is different from 'TOMATOES CLASS I'\n" +
    "4. Abbreviations to recognize:\n" +
    "   - 'GOLDEN DEL' = 'GOLDEN DELICIOUS'\n" +
    "   - 'CAT I' = 'CLASS I' = 'CATEGORY I'\n" +
    "   - 'CAT II' = 'CLASS II' = 'CATEGORY II'\n" +
    "   - 'BIO' = 'ORGANIC'\n\n" +
    "OUTPUT:\n" +
    "Return ONLY the exact product name from the catalog that best matches ALL the details.\n" +
    "If no good match exists (less than 70% similarity including grade/size), return 'NONE'.\n\n" +
    "Examples:\n" +
    "Document: 'GOLDEN DEL APPLES CAT I 70+' | Catalog: ['GOLDEN DELICIOUS APPLES CLASS I 70+', 'GOLDEN DELICIOUS APPLES CLASS II 70+'] → GOLDEN DELICIOUS APPLES CLASS I 70+\n" +
    "Document: 'PLATANOS CAVENDISH 70+' | Catalog: ['BANANAS CAVENDISH 60+', 'BANANAS CAVENDISH 70+', 'BANANAS CAVENDISH 80+'] → BANANAS CAVENDISH 70+\n" +
    "Document: 'MANZANAS ROJAS EXTRA' | Catalog: ['RED APPLES CLASS I', 'RED APPLES EXTRA', 'RED APPLES CLASS II'] → RED APPLES EXTRA\n" +
    "Document: 'SWEET PEPPERS' | Catalog: ['TOMATOES', 'CUCUMBERS', 'CARROTS'] → NONE";

                            var userPrompt = "HISTORICAL PRODUCT CATALOG:\n" +
                                string.Join("\n", similarNameProducts.Select(p => $"- {p.NameOnDocument}")) + "\n\n" +
                                "---\n\n" +
                                "PRODUCT NAME FROM DOCUMENT:\n" +
                                deserializedProduct.name + "\n\n" +
                                "Return the best matching product name from the catalog above (matching ALL details including size/grade), or 'NONE' if no good match exists.";

                            //var aiMatchedProductName = await _aiApiService.GetSimpleResponseAsync(systemPrompt, userPrompt);
                            var aiMatchedProductName = await _cerebrasApiService.GetSimpleResponseAsync(systemPrompt, userPrompt);

                            //var aiMatchedProductName = await _aiApiService.GetSimpleResponseAsync(aiMatchPrompt, deserializedProduct.name);
                            Console.WriteLine($"AI matched product name for {deserializedProduct.name}: {aiMatchedProductName}");

                            if (!string.IsNullOrEmpty(aiMatchedProductName) && aiMatchedProductName != "NONE")
                            {
                                var matchingShippingItem = allShippingItems.FirstOrDefault(x =>
                                    x.NameOnDocument != null &&
                                    x.NameOnDocument.Equals(aiMatchedProductName, StringComparison.OrdinalIgnoreCase));

                                if (matchingShippingItem?.ProductDto != null)
                                {
                                    matchedItem = CreateShippingItem(matchingShippingItem.ProductDto, deserializedProduct);
                                    Console.WriteLine($"AI Matched product from historical: {matchingShippingItem.ProductDto.Name}");
                                }
                            }
                        }

                        // Step 3: If still no match, try AI with full product catalog
                        if (matchedItem == null)
                        {
                            // HYBRID APPROACH: Combine recent products + fuzzy matched products

                            // Get recent products (50 newest - most likely to be in current shipments)
                            var recentProducts = allProducts
                                .OrderByDescending(p => p.Id) // Use Id as proxy for CreatedDate if CreatedOnUtc not available
                                .Take(50)
                                .ToList();

                            // Get products that fuzzy match the search term (similar names)
                            int fuzzySearchLength = Math.Min(4, deserializedProduct.name?.Length ?? 0);
                            var fuzzyMatches = fuzzySearchLength > 0
                                ? allProducts
                                    .Where(p => p.Name.Contains(
                                        deserializedProduct.name.Substring(0, fuzzySearchLength),
                                        StringComparison.OrdinalIgnoreCase))
                                    .Take(30)
                                    .ToList()
                                : new List<ProductDto>();

                            // Combine and deduplicate
                            var combinedProducts = recentProducts
                                .Union(fuzzyMatches)
                                .GroupBy(p => p.Id)
                                .Select(g => g.First())
                                .Take(100)
                                .ToList();

                            Console.WriteLine($"Hybrid search: {combinedProducts.Count} products ({recentProducts.Count} recent + {fuzzyMatches.Count} fuzzy matched) for: {deserializedProduct.name}");

                            var systemPrompt2 = "You are a product name matching specialist for FruitBank wholesale company.\n\n" +
    "Your task: Match a product name from a shipping document to our product catalog.\n\n" +
    "MATCHING RULES:\n" +
    "1. Match based on ALL product details:\n" +
    "   - Product type and variety\n" +
    "   - Quality grades: 'Extra', 'Class I', 'Class II', 'Premium', 'Category I/II'\n" +
    "   - Size markers: '60+', '70+', '80+', 'Large', 'Small', 'Medium'\n" +
    "   - Packaging: 'Carton', 'Box', 'Loose', 'Packed' (if it's part of product ID)\n" +
    "   - Origin country: 'Spanish', 'Italian', 'Dutch', 'Turkish' (if tracked separately)\n" +
    "2. Language variations:\n" +
    "   SPANISH → ENGLISH → HUNGARIAN\n" +
    "   - Manzanas → Apples → Alma\n" +
    "   - Plátanos → Bananas → Banán\n" +
    "   - Naranjas → Oranges → Narancs\n" +
    "   - Tomates → Tomatoes → Paradicsom\n" +
    "   - Pimientos → Peppers → Paprika\n" +
    "   - Uvas → Grapes → Szőlő\n" +
    "   - Limones → Lemons → Citrom\n" +
    "3. Quality grade abbreviations:\n" +
    "   - 'CAT I' / 'CAT. I' / 'CATEGORY I' = 'CLASS I'\n" +
    "   - 'CAT II' / 'CAT. II' / 'CATEGORY II' = 'CLASS II'\n" +
    "   - '1' = 'CLASS I', '2' = 'CLASS II'\n" +
    "4. Be specific:\n" +
    "   - 'APPLES 70+' is NOT the same as 'APPLES 80+'\n" +
    "   - 'TOMATOES CLASS I' is NOT the same as 'TOMATOES CLASS II'\n" +
    "   - 'ORANGES SPANISH' may be different from 'ORANGES ITALIAN'\n\n" +
    "OUTPUT:\n" +
    "Return ONLY the exact product name from the catalog that matches ALL the details.\n" +
    "If no close match exists (below 70% similarity), return 'NONE'.\n\n" +
    "Examples:\n" +
    "Document: 'MANZANAS GOLDEN CAT I 70+' | Best match: 'GOLDEN DELICIOUS APPLES CLASS I 70+'\n" +
    "Document: 'BIO BANANEN 80+' | Best match: 'ORGANIC BANANAS 80+' (NOT just 'BANANAS')\n" +
    "Document: 'POMODORI CILIEGINI EXTRA' | Best match: 'CHERRY TOMATOES EXTRA' (NOT 'CHERRY TOMATOES CLASS I')\n" +
    "Document: 'NARANJAS 60+' | Best match: 'ORANGES 60+' (NOT 'ORANGES 70+')\n" +
    "Document: 'RARE EXOTIC FRUIT' | No match: 'NONE'";

                            var userPrompt2 = "PRODUCT CATALOG (recent products + similar names):\n" +
                                string.Join("\n", combinedProducts.Select(p => $"- {p.Name}")) + "\n\n" +
                                "---\n\n" +
                                "PRODUCT NAME FROM DOCUMENT:\n" +
                                deserializedProduct.name + "\n\n" +
                                "Return the best matching product name from the catalog above that matches ALL details (size, grade, quality), or 'NONE' if no confident match exists.";

                            //var aiMatchedProductName2 = await _aiApiService.GetSimpleResponseAsync(systemPrompt2, userPrompt2);
                            var aiMatchedProductName2 = await _cerebrasApiService.GetSimpleResponseAsync(systemPrompt2, userPrompt2);
                            Console.WriteLine($"AI matched product name from hybrid catalog for {deserializedProduct.name}: {aiMatchedProductName2}");

                            if (!string.IsNullOrEmpty(aiMatchedProductName2) && aiMatchedProductName2 != "NONE")
                            {
                                // Clean the AI response
                                aiMatchedProductName2 = CleanProductName(aiMatchedProductName2);

                                var matchingProduct = combinedProducts.FirstOrDefault(x =>
                                    x.Name.Equals(aiMatchedProductName2, StringComparison.OrdinalIgnoreCase));

                                if (matchingProduct != null)
                                {
                                    matchedItem = CreateShippingItem(matchingProduct, deserializedProduct);
                                    Console.WriteLine($"AI Matched product from hybrid catalog: {matchingProduct.Name}");
                                }
                            }
                        }
                    }
                }

                // Step 4: Add matched or unmatched item
                if (matchedItem != null)
                {
                    finalMatchedProducts.Add(matchedItem);
                }
                else
                {
                    // Create unmatched item
                    finalMatchedProducts.Add(new ShippingItem
                    {
                        Name = "",
                        HungarianName = "",
                        PalletsOnDocument = 1,
                        IsMeasurable = false,
                        QuantityOnDocument = deserializedProduct.quantity ?? 0,
                        NetWeightOnDocument = deserializedProduct.netWeight ?? 0,
                        GrossWeightOnDocument = deserializedProduct.grossWeight ?? 0,
                        ProductId = null,
                        NameOnDocument = deserializedProduct.name
                    });
                    Console.WriteLine($"No match found for: {deserializedProduct.name}");
                }
            }

            Console.WriteLine($"Total matched products: {finalMatchedProducts.Count(x => x.ProductId != null)}");
            Console.WriteLine($"Total unmatched products: {finalMatchedProducts.Count(x => x.ProductId == null)}");

            return finalMatchedProducts;
        }

        private ShippingItem CreateShippingItem(ProductDto productDto, ProductReference deserializedProduct)
        {
            return new ShippingItem
            {
                Name = productDto.Name,
                HungarianName = productDto.Name,
                PalletsOnDocument = 1,
                QuantityOnDocument = deserializedProduct.quantity ?? 0,
                NetWeightOnDocument = deserializedProduct.netWeight ?? 0,
                GrossWeightOnDocument = deserializedProduct.grossWeight ?? 0,
                ProductId = productDto.Id,
                NameOnDocument = deserializedProduct.name,
                IsMeasurable = productDto.IsMeasurable,
                UnitPriceOnDocument = deserializedProduct.unitCost
            };
        }

        /// <summary>
        /// Cleans and normalizes product name returned from AI
        /// </summary>
        private string CleanProductName(string rawProductName)
        {
            if (string.IsNullOrWhiteSpace(rawProductName))
                return string.Empty;

            var cleaned = rawProductName.Trim();

            // Remove common prefixes that AI might add
            var prefixesToRemove = new[]
            {
        "Product name:",
        "Match:",
        "Best match:",
        "The product is",
        "Answer:",
        "-"
    };

            foreach (var prefix in prefixesToRemove)
            {
                if (cleaned.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
                {
                    cleaned = cleaned.Substring(prefix.Length).Trim();
                }
            }

            // Remove quotes if present
            cleaned = cleaned.Trim('\"', '\'', '\'', '«', '»');

            // Remove trailing punctuation
            cleaned = cleaned.TrimEnd('.', ',', ';', ':');

            return cleaned;
        }

        private async Task<int> DeterminePartner(string partnerAnalysis)
        {
            // Clean the input first
            partnerAnalysis = CleanPartnerName(partnerAnalysis);

            if (string.IsNullOrWhiteSpace(partnerAnalysis))
            {
                Console.WriteLine("Partner analysis is empty after cleaning.");
                return 0;
            }

            var possiblePartners = await _dbContext.Partners.GetAll().ToListAsync();

            // STEP 1: Try exact match first (fast, free, no AI needed!)
            var exactMatch = possiblePartners.FirstOrDefault(p =>
                p.Name.Trim().Equals(partnerAnalysis.Trim(), StringComparison.OrdinalIgnoreCase));

            if (exactMatch != null)
            {
                Console.WriteLine($"✓ Exact partner match found: {exactMatch.Name} (ID: {exactMatch.Id})");
                return exactMatch.Id;
            }

            Console.WriteLine($"No exact match found for '{partnerAnalysis}'. Trying AI matching...");

            // STEP 2: No exact match? Use AI with IDs (handles fuzzy matching)
            var partnerListWithIds = string.Join("\n", possiblePartners.Select(p => $"ID: {p.Id} | Name: {p.Name}"));

            var systemPrompt = "You are a partner matching specialist for FruitBank.\n\n" +
                "Your task: Match a partner name to the correct partner from our database.\n\n" +
                "MATCHING RULES:\n" +
                "1. Ignore minor differences:\n" +
                "   - Trailing/leading spaces\n" +
                "   - Periods and punctuation\n" +
                "   - Case differences (B.V. vs BV vs b.v.)\n" +
                "   - Legal entity suffixes (B.V., S.L., S.R.L., Kft., Ltd.)\n" +
                "2. Match based on core company name\n" +
                "3. Be flexible with abbreviations\n\n" +
                "OUTPUT:\n" +
                "Return ONLY the numeric ID of the matching partner.\n" +
                "If no match found, return '0'.\n\n" +
                "Examples:\n" +
                "Input: 'SFI Rotterdam' | Database: 'ID: 42 | Name: SFI Rotterdam B.V.' → 42\n" +
                "Input: 'Frutas Sanchez SL' | Database: 'ID: 15 | Name: FRUTAS SÁNCHEZ S.L.' → 15\n" +
                "Input: 'Van den Berg' | Database: 'ID: 8 | Name: Van den Berg B.V.' → 8\n" +
                "Input: 'Unknown Company' | No match in database → 0";

            var userPrompt = "PARTNER DATABASE:\n" +
                partnerListWithIds + "\n\n" +
                "---\n\n" +
                "PARTNER TO MATCH:\n" +
                partnerAnalysis + "\n\n" +
                "Return ONLY the numeric ID of the matching partner, or '0' if no match found.";

            //var aiResponse = await _aiApiService.GetSimpleResponseAsync(systemPrompt, userPrompt);
            var aiResponse = await _cerebrasApiService.GetSimpleResponseAsync(systemPrompt, userPrompt);
            Console.WriteLine($"AI Partner Match Response: {aiResponse}");

            // Parse the ID
            if (int.TryParse(aiResponse.Trim(), out int partnerId))
            {
                if (partnerId == 0)
                {
                    Console.WriteLine("AI found no matching partner.");
                    return 0;
                }

                // Verify the ID exists in our list
                var matchedPartner = possiblePartners.FirstOrDefault(p => p.Id == partnerId);
                if (matchedPartner != null)
                {
                    Console.WriteLine($"✓ AI matched partner: {matchedPartner.Name} (ID: {matchedPartner.Id})");
                    return partnerId;
                }
                else
                {
                    Console.WriteLine($"⚠ AI returned invalid partner ID: {partnerId}");
                    return 0;
                }
            }
            else
            {
                Console.WriteLine($"⚠ AI returned non-numeric response: {aiResponse}");
                return 0;
            }
        }

        private async Task<string> ExtractPartnerName(string extractedText)
        {
            var availablePartners = await _dbContext.Partners.GetAll().ToListAsync();
            Console.WriteLine($"Available partners count: {availablePartners.Count}");

            string partnerListForAI = "";
            foreach (var partner in availablePartners)
            {
                //let's make a string list of available partners for AI

                partnerListForAI += $"- {partner.Name}\n";

            }

            // Enhanced system prompt with explicit instructions and examples
            var systemPrompt = "You are a specialized data extraction agent for FruitBank, a Hungarian fruit and vegetable wholesale company.\n\n" +
                "Your task: Extract the SUPPLIER/SENDER company name from shipping documents (CMR, delivery notes, invoices).\n\n" +
                "CRITICAL RULES:\n" +
                "1. FruitBank (Gyümölcsbank Kft.) is the RECEIVER - NEVER return FruitBank as the partner\n" +
                "2. Look for these indicators of the SUPPLIER:\n" +
                "   - 'Sender' / 'Expediteur' / 'Feladó' / 'Absender' section\n" +
                "   - 'From' / 'De' / 'Kitől' field\n" +
                "   - Company name at TOP of document (usually sender)\n" +
                "   - Tax ID / VAT number paired with company name\n" +
                "   - EORI number holder (if present)\n" +
                "3. The supplier is typically:\n" +
                "   - A farm, cooperative, or wholesaler\n" +
                "   - Located in Spain, Italy, Netherlands, Poland, Germany, Greece, Turkey, or other EU countries\n" +
                "   - NOT FruitBank and NOT the transport company\n\n" +
                "Document structure hints:\n" +
                "- CMR documents: Sender is box 1-2, Receiver is box 3-4\n" +
                "- Invoices: Look for 'Seller' / 'Eladó' / 'Vendedor' (NOT Buyer)\n" +
                "- Delivery notes: Sender/Origin section at top\n\n" +
                "OUTPUT FORMAT:\n" +
                "Return ONLY the exact company name as it appears in the document.\n" +
                "Do not include:\n" +
                "- Tax IDs\n" +
                "- Addresses\n" +
                "- Country codes\n" +
                "- Legal entity types (unless part of official name)\n\n" +
                "Examples:\n" +
                "[CORRECT] FRUTAS SÁNCHEZ S.L.\n" +
                "[CORRECT] Van den Berg B.V.\n" +
                "[CORRECT] Agricola Romana SRL\n" +
                "[WRONG] FruitBank (this is us!)\n" +
                "[WRONG] DHL Supply Chain (transport company)\n" +
                "[WRONG] FRUTAS SÁNCHEZ S.L. - ES12345678 (no tax ID)";

            // Enhanced user prompt with context and structure
            var userPrompt = "DOCUMENT TEXT:\n" +
                extractedText + "\n\n" +
                "---\n\n" +
                "INSTRUCTIONS:\n" +
                "1. Identify the SENDER/SUPPLIER company name\n" +
                "2. Ignore FruitBank (Gyümölcsbank) - that's the receiver\n" +
                "3. Ignore transport companies (DHL, Transporeon, etc.)\n" +
                "4. Return ONLY the company name, nothing else\n\n" +
                "If uncertain, return the most prominent non-FruitBank company name from the document.";

            //var partnerAnalysis = await _aiApiService.GetSimpleResponseAsync(systemPrompt, userPrompt);
            var partnerAnalysis = await _cerebrasApiService.GetSimpleResponseAsync(systemPrompt, userPrompt);

            // Clean up the response
            var cleanedPartnerName = CleanPartnerName(partnerAnalysis);

            Console.WriteLine($"Partner analysis Result: {cleanedPartnerName}");
            return cleanedPartnerName;
        }

        /// <summary>
        /// Cleans and normalizes partner name from AI response
        /// </summary>
        private string CleanPartnerName(string rawPartnerName)
        {
            if (string.IsNullOrWhiteSpace(rawPartnerName))
                return string.Empty;

            var cleaned = rawPartnerName.Trim();

            // Remove common prefixes that AI might add
            var prefixesToRemove = new[]
            {
                "Company name:",
                "Sender:",
                "Supplier:",
                "Partner:",
                "The partner is",
                "The company is",
                "Feladó:",
                "Expediteur:"
            };

            foreach (var prefix in prefixesToRemove)
            {
                if (cleaned.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
                {
                    cleaned = cleaned.Substring(prefix.Length).Trim();
                    break;
                }
            }

            // Remove quotes if present
            cleaned = cleaned.Trim('\"', '\'', '\'', '«', '»');

            // Remove trailing punctuation
            cleaned = cleaned.TrimEnd('.', ',', ';');

            // Remove tax IDs that might have slipped through (pattern: letters followed by 8+ digits)
            var taxIdPattern = new System.Text.RegularExpressions.Regex(@"\s*-?\s*[A-Z]{2}\d{8,}.*$");
            cleaned = taxIdPattern.Replace(cleaned, string.Empty).Trim();

            // If AI returned 'NONE' or similar, return empty
            if (cleaned.Equals("NONE", StringComparison.OrdinalIgnoreCase) ||
                cleaned.Equals("N/A", StringComparison.OrdinalIgnoreCase) ||
                cleaned.Equals("NOT FOUND", StringComparison.OrdinalIgnoreCase))
            {
                return string.Empty;
            }

            // Check if accidentally returned FruitBank
            if (cleaned.Contains("FruitBank", StringComparison.OrdinalIgnoreCase) ||
                cleaned.Contains("Gyümölcsbank", StringComparison.OrdinalIgnoreCase))
            {
                Console.WriteLine($"WARNING: AI returned FruitBank as partner. Returning empty.");
                return string.Empty;
            }

            return cleaned;
        }

        //private async Task<string> ExtractDocumentId(string extractedText)
        //{
        //    //analyze the text for document number or identifiers
        //    return await _aiApiService.GetSimpleResponseAsync("You are an agent of Fruitbank to analyize text extracted frem a pfd document, and find the document number or identifier. IMPORTANT: reply only with the number, do not add further explanation.", $"What is the document identifier of this document: {extractedText}");
        //}

        /// <summary>
        /// Partner search autocomplete endpoint for finding partners by name or tax ID
        /// </summary>
        [HttpGet]
        public async Task<IActionResult> PartnerSearchAutoComplete(string term)
        {
            if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
                return Json(new { success = false, message = "Access denied" });

            if (string.IsNullOrWhiteSpace(term) || term.Length < 2)
                return Json(new List<object>());

            try
            {
                const int maxResults = 20;

                // Get all partners and filter in-memory (or create a custom search method)
                var allPartners = await _dbContext.Partners.GetAll().ToListAsync();

                var matchedPartners = allPartners
                    .Where(p =>
                        (!string.IsNullOrEmpty(p.Name) && p.Name.Contains(term, StringComparison.OrdinalIgnoreCase)) ||
                        (!string.IsNullOrEmpty(p.TaxId) && p.TaxId.Contains(term, StringComparison.OrdinalIgnoreCase))
                    )
                    .Take(maxResults)
                    .ToList();

                var result = matchedPartners.Select(partner => new
                {
                    label = $"{partner.Name} - Tax ID: {partner.TaxId ?? "N/A"}",
                    value = partner.Id,
                    name = partner.Name,
                    taxId = partner.TaxId,
                    city = partner.City,
                    country = partner.Country
                }).ToList();

                return Json(result);
            }
            catch (Exception ex)
            {
                Console.Error.WriteLine($"Error in PartnerSearchAutoComplete: {ex}");
                return Json(new List<object>());
            }
        }

        /// <summary>
        /// Save a shipping document with its items AND the original uploaded file
        /// </summary>
        [HttpPost]
        public async Task<IActionResult> SaveShippingDocument(
            [FromForm] string documentData,
            [FromForm] IFormFile originalFile,
            [FromForm] string extractedText)
        {
            if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
                return Json(new { success = false, message = "Access denied" });

            if (string.IsNullOrEmpty(documentData))
            {
                return Json(new { success = false, message = "Invalid request data" });
            }

            try
            {
                // ✅ Configure JSON options to handle camelCase from JavaScript
                var jsonOptions = new System.Text.Json.JsonSerializerOptions
                {
                    PropertyNameCaseInsensitive = true // This handles camelCase <-> PascalCase
                };

                // Deserialize the shipping document data
                var request = System.Text.Json.JsonSerializer.Deserialize<SaveShippingDocumentRequest>(documentData, jsonOptions);

                if (request == null)
                {
                    return Json(new { success = false, message = "Failed to parse document data" });
                }

                // Validate required fields
                if (string.IsNullOrEmpty(request.DocumentIdNumber))
                {
                    return Json(new { success = false, message = "Document ID is required" });
                }

                if (!request.PartnerId.HasValue || request.PartnerId <= 0)
                {
                    return Json(new { success = false, message = "Valid Partner ID is required" });
                }

                if (request.ShippingItems == null || !request.ShippingItems.Any())
                {
                    return Json(new { success = false, message = "At least one shipping item is required" });
                }

                var partner = await _dbContext.Partners.GetByIdAsync(request.PartnerId.Value);

                // Create the shipping document entity
                var shippingDocument = new ShippingDocument
                {
                    DocumentIdNumber = request.DocumentIdNumber,
                    PartnerId = request.PartnerId.Value,
                    Country = partner?.Country,
                    TotalPallets = request.TotalPallets,
                    PdfFileName = originalFile.FileName,
                    ShippingDate = DateTime.UtcNow,
                    ShippingItems = new List<ShippingItem>()
                };

                // Convert DTOs to entities
                foreach (var itemDto in request.ShippingItems)
                {
                    var productDto = await _dbContext.ProductDtos.GetByIdAsync(itemDto.ProductId ?? 0, true);
                    if (productDto != null && !string.IsNullOrEmpty(itemDto.Name))
                    {
                        itemDto.IsMeasurable = productDto.IsMeasurable;
                    }
                    var shippingItem = new ShippingItem
                    {
                        Name = itemDto.Name,
                        HungarianName = itemDto.HungarianName,
                        NameOnDocument = itemDto.NameOnDocument,
                        ProductId = itemDto.ProductId,
                        PalletsOnDocument = itemDto.PalletsOnDocument,
                        QuantityOnDocument = itemDto.QuantityOnDocument,
                        NetWeightOnDocument = itemDto.NetWeightOnDocument,
                        GrossWeightOnDocument = itemDto.GrossWeightOnDocument,
                        UnitPriceOnDocument = itemDto.UnitPriceOnDocument,
                        IsMeasurable = itemDto.IsMeasurable
                    };

                    shippingDocument.ShippingItems.Add(shippingItem);
                }

                // ✅ STEP 1: Save shipping document to database FIRST
                await _dbContext.ShippingDocuments.InsertAsync(shippingDocument);
                foreach (var sItem in shippingDocument.ShippingItems)
                {
                    sItem.ShippingDocumentId = shippingDocument.Id; // Set FK
                    await _dbContext.ShippingItems.InsertAsync(sItem);
                }

                Console.WriteLine($"✓ Shipping document saved - ID: {shippingDocument.Id}, Document ID: {shippingDocument.DocumentIdNumber}");

                // ✅ STEP 2: NOW save the original file (only if document saved successfully)
                if (originalFile != null && originalFile.Length > 0)
                {
                    var currentUser = await _workContext.GetCurrentCustomerAsync();

                    using (var fileStream = originalFile.OpenReadStream())
                    {
                        var savedFile = await _fileStorageService.SaveFileAsync(
                            fileStream: fileStream,
                            fileName: originalFile.FileName,
                            userId: currentUser.Id,
                            featureName: "ShippingDocumentProcessing",
                            entityType: "ShippingDocuments",
                            entityId: shippingDocument.Id, // ✅ Now we have the real ID!
                            rawText: extractedText,
                            checkForDuplicates: true
                        );

                        // Create mapping between ShippingDocument and File
                        var mapping = new ShippingDocumentToFiles
                        {
                            FilesId = savedFile.Id,
                            ShippingDocumentId = shippingDocument.Id,
                            DocumentTypeId = (int)DocumentType.ShippingDocument,
                            Created = DateTime.UtcNow,
                            Modified = DateTime.UtcNow
                        };

                        await _dbContext.ShippingDocumentToFiles.InsertAsync(mapping);


                        Console.WriteLine($"✓ Original file saved - File ID: {savedFile.Id}, Filename: {savedFile.FileName}");
                    }
                }
                else
                {
                    Console.WriteLine("⚠ No original file provided - skipping file save");
                }

                //everything done, let's update the genericattribute "IncomingQuantity" of the product
                foreach (var item in shippingDocument.ShippingItems.Where(x => x.ProductId != null))
                {
                    //var stockWeight = double.Round(await _fruitBankAttributeService.GetGenericAttributeValueAsync<Product, double>(product.Id, nameof(IMeasuringNetWeight.NetWeight), storeId), 1);
                    var alreadyIncomingQuantityAttribute = await _fruitBankAttributeService.GetGenericAttributeValueAsync<Product, int>(item.ProductId.Value, nameof(IIncomingQuantity.IncomingQuantity), _storeContext.GetCurrentStore().Id); 
                    int alreadyIncomingQuantity = 0;
                    if (alreadyIncomingQuantityAttribute != null) 
                    {
                        Console.WriteLine($"Existing IncomingQuantity for Product ID {item.ProductId.Value}: {alreadyIncomingQuantityAttribute}");
                        alreadyIncomingQuantity = Convert.ToInt32(alreadyIncomingQuantityAttribute);
                    }
                    var newIncomingQuantity = alreadyIncomingQuantity + item.QuantityOnDocument;

                    await _fruitBankAttributeService.InsertOrUpdateGenericAttributeAsync<Product, int>(
                        item.ProductId.Value,
                        "IncomingQuantity",
                        newIncomingQuantity, _storeContext.GetCurrentStore().Id
                    );
                }

                // ── Step 3: Convert pending preorders that cover these products ──────────
                var productIdsWithIncoming = shippingDocument.ShippingItems
                    .Where(x => x.ProductId != null)
                    .Select(x => x.ProductId!.Value)
                    .Distinct()
                    .ToList();

                if (productIdsWithIncoming.Any())
                {
                    // Fire-and-forget with error isolation so a conversion failure
                    // never blocks the shipping document save response
                    _ = Task.Run(async () =>
                    {
                        try
                        {
                            await _preorderConversionService
                                .ConvertPreOrdersForProductsAsync(productIdsWithIncoming, shippingDocument.Id);
                        }
                        catch (Exception convEx)
                        {
                            Console.Error.WriteLine(
                                $"[PreOrderConversion] Error during conversion for document #{shippingDocument.Id}: {convEx.Message}");
                        }
                    });
                }
                    

                return Json(new
                {
                    success = true,
                    message = "Shipping document and file saved successfully",
                    shippingDocumentId = shippingDocument.Id,
                    documentId = shippingDocument.DocumentIdNumber,
                    itemCount = shippingDocument.ShippingItems.Count
                });
            }
            catch (Exception ex)
            {
                Console.Error.WriteLine($"Error saving shipping document: {ex}");
                return Json(new
                {
                    success = false,
                    message = $"Error saving document: {ex.Message}"
                });
            }
        }

    }



    public class ProductReference
    {
        public string? name { get; set; }
        public int? quantity { get; set; }
        public double? netWeight { get; set; }
        public double? grossWeight { get; set; }
        public int? productId { get; set; }
        public double unitCost { get; set; }
    }

    public class ProductReferenceResponse
    {
        public List<ProductReference> products { get; set; }
    }

    public class SaveShippingDocumentRequest
    {
        public string DocumentIdNumber { get; set; }
        public int? PartnerId { get; set; }
        public int TotalPallets { get; set; }
        public string PdfFileName { get; set; }
        public List<SaveShippingItemDto> ShippingItems { get; set; }
    }

    public class SaveShippingItemDto
    {
        public string Name { get; set; }
        public string HungarianName { get; set; }
        public string NameOnDocument { get; set; }
        public int? ProductId { get; set; }
        public int PalletsOnDocument { get; set; }
        public int QuantityOnDocument { get; set; }
        public double NetWeightOnDocument { get; set; }
        public double GrossWeightOnDocument { get; set; }
        public double UnitPriceOnDocument { get; set; }
        public bool IsMeasurable { get; set; }
    }

}
