using AutoMapper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using PowderCoating.Application.DTOs.GiftCertificate;
using PowderCoating.Application.Interfaces;
using PowderCoating.Core.Entities;
using PowderCoating.Core.Enums;
using PowderCoating.Core.Interfaces;
using PowderCoating.Shared.Constants;
using PowderCoating.Web.Helpers;
using AccountSubTypeEnum = PowderCoating.Core.Enums.AccountSubType;

namespace PowderCoating.Web.Controllers;

/// <summary>
/// Manages gift certificate issuance, redemption tracking, voiding, and PDF generation.
/// Gift certificates progress through five statuses: Active → PartiallyRedeemed → FullyRedeemed
/// (automatic via invoice redemption), or Active/PartiallyRedeemed → Voided (admin action), or
/// Active → Expired (time-based, detected lazily on Index load). Certificate codes use the
/// GC-YYMM-#### format scoped per company, matching the INV- and DEP- numbering conventions
/// across the billing module. Redemptions are recorded separately as child records so the full
/// redemption history survives even if the parent certificate is voided.
/// </summary>
[Authorize(Policy = AppConstants.Policies.CanManageInvoices)]
public class GiftCertificatesController : Controller
{
    private readonly IUnitOfWork _unitOfWork;
    private readonly IMapper _mapper;
    private readonly ILogger<GiftCertificatesController> _logger;
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly IPdfService _pdfService;
    private readonly ICompanyLogoService _logoService;
    private readonly IAccountBalanceService _accountBalanceService;

    public GiftCertificatesController(
        IUnitOfWork unitOfWork,
        IMapper mapper,
        ILogger<GiftCertificatesController> logger,
        UserManager<ApplicationUser> userManager,
        IPdfService pdfService,
        ICompanyLogoService logoService,
        IAccountBalanceService accountBalanceService)
    {
        _unitOfWork = unitOfWork;
        _mapper = mapper;
        _logger = logger;
        _userManager = userManager;
        _pdfService = pdfService;
        _logoService = logoService;
        _accountBalanceService = accountBalanceService;
    }

    /// <summary>
    /// Displays the gift certificate list with optional search and status filtering, and lazily
    /// transitions any Active certificates that have passed their expiry date to Expired status.
    /// Lazy expiration is used rather than a background job or DB trigger because the certificate
    /// volume is low and the mutation is idempotent; this ensures the list is always self-consistent
    /// without requiring a separate scheduler. Stats show only certificates with spendable balances
    /// (Active or PartiallyRedeemed) so the total outstanding liability is immediately visible.
    /// </summary>
    public async Task<IActionResult> Index(string? searchTerm, string? statusFilter)
    {
        var companyId = (await _userManager.GetUserAsync(User))?.CompanyId ?? 0;
        var certs = await _unitOfWork.GiftCertificates.FindAsync(
            gc => gc.CompanyId == companyId, false,
            gc => gc.RecipientCustomer,
            gc => gc.PurchasingCustomer);

        // Expire any Active certs past their expiry date
        var now = DateTime.UtcNow;
        foreach (var cert in certs.Where(c => c.Status == GiftCertificateStatus.Active
                                            && c.ExpiryDate.HasValue && c.ExpiryDate.Value < now))
        {
            cert.Status = GiftCertificateStatus.Expired;
            await _unitOfWork.GiftCertificates.UpdateAsync(cert);
        }
        await _unitOfWork.CompleteAsync();

        if (!string.IsNullOrWhiteSpace(searchTerm))
        {
            var term = searchTerm.Trim().ToLower();
            certs = certs.Where(gc =>
                gc.CertificateCode.ToLower().Contains(term) ||
                (gc.RecipientName?.ToLower().Contains(term) ?? false) ||
                (gc.RecipientCustomer?.CompanyName?.ToLower().Contains(term) ?? false) ||
                (gc.RecipientEmail?.ToLower().Contains(term) ?? false)
            ).ToList();
        }

        if (!string.IsNullOrWhiteSpace(statusFilter) && Enum.TryParse<GiftCertificateStatus>(statusFilter, out var parsedStatus))
            certs = certs.Where(gc => gc.Status == parsedStatus).ToList();

        var dtos = certs
            .OrderByDescending(gc => gc.IssueDate)
            .Select(gc => new GiftCertificateListDto
            {
                Id = gc.Id,
                CertificateCode = gc.CertificateCode,
                OriginalAmount = gc.OriginalAmount,
                RedeemedAmount = gc.RedeemedAmount,
                RemainingBalance = gc.RemainingBalance,
                RecipientName = gc.RecipientCustomer != null
                    ? (gc.RecipientCustomer.CompanyName ?? $"{gc.RecipientCustomer.ContactFirstName} {gc.RecipientCustomer.ContactLastName}".Trim())
                    : gc.RecipientName,
                RecipientEmail = gc.RecipientEmail,
                IssuedReason = gc.IssuedReason,
                Status = gc.Status,
                IssueDate = gc.IssueDate,
                ExpiryDate = gc.ExpiryDate,
                BatchId = gc.BatchId
            })
            .ToList();

        ViewBag.SearchTerm = searchTerm;
        ViewBag.StatusFilter = statusFilter;
        ViewBag.TotalActive = dtos.Count(d => d.Status == GiftCertificateStatus.Active || d.Status == GiftCertificateStatus.PartiallyRedeemed);
        ViewBag.TotalValue = dtos.Where(d => d.Status == GiftCertificateStatus.Active || d.Status == GiftCertificateStatus.PartiallyRedeemed)
                                  .Sum(d => d.RemainingBalance);

        return View(dtos);
    }

    /// <summary>
    /// Shows full certificate detail including all non-deleted redemption records, each annotated with
    /// the human-readable invoice number. Invoice numbers are resolved in a loop rather than a join
    /// because the redemption DTO is constructed manually (not via AutoMapper) to avoid over-fetching
    /// invoice data; the loop is bounded by the number of redemptions which is typically very small.
    /// Recipient name falls back to the free-text <c>RecipientName</c> field when no customer record
    /// is linked, supporting gift certificates issued to non-customers (e.g. promotional give-aways).
    /// </summary>
    public async Task<IActionResult> Details(int id)
    {
        var cert = await _unitOfWork.GiftCertificates.GetByIdAsync(id, false,
            gc => gc.RecipientCustomer,
            gc => gc.PurchasingCustomer,
            gc => gc.IssuedBy,
            gc => gc.Redemptions);

        if (cert == null) return NotFound();

        var dto = new GiftCertificateDto
        {
            Id = cert.Id,
            CertificateCode = cert.CertificateCode,
            OriginalAmount = cert.OriginalAmount,
            RedeemedAmount = cert.RedeemedAmount,
            RemainingBalance = cert.RemainingBalance,
            RecipientCustomerId = cert.RecipientCustomerId,
            RecipientName = cert.RecipientCustomer != null
                ? (cert.RecipientCustomer.CompanyName ?? $"{cert.RecipientCustomer.ContactFirstName} {cert.RecipientCustomer.ContactLastName}".Trim())
                : cert.RecipientName,
            RecipientEmail = cert.RecipientEmail,
            IssuedReason = cert.IssuedReason,
            PurchasePrice = cert.PurchasePrice,
            PurchasingCustomerId = cert.PurchasingCustomerId,
            PurchasingCustomerName = cert.PurchasingCustomer != null
                ? (cert.PurchasingCustomer.CompanyName ?? $"{cert.PurchasingCustomer.ContactFirstName} {cert.PurchasingCustomer.ContactLastName}".Trim())
                : null,
            Status = cert.Status,
            IssueDate = cert.IssueDate,
            ExpiryDate = cert.ExpiryDate,
            Notes = cert.Notes,
            IssuedByName = cert.IssuedBy != null
                ? $"{cert.IssuedBy.FirstName} {cert.IssuedBy.LastName}".Trim()
                : null
        };

        dto.Redemptions = cert.Redemptions
            .Where(r => !r.IsDeleted)
            .OrderByDescending(r => r.RedeemedDate)
            .Select(r => new GiftCertificateRedemptionDto
            {
                Id = r.Id,
                GiftCertificateId = r.GiftCertificateId,
                InvoiceId = r.InvoiceId,
                AmountRedeemed = r.AmountRedeemed,
                RedeemedDate = r.RedeemedDate
            }).ToList();

        foreach (var redemption in dto.Redemptions)
        {
            var inv = await _unitOfWork.Invoices.GetByIdAsync(redemption.InvoiceId);
            if (inv != null) redemption.InvoiceNumber = inv.InvoiceNumber;
        }

        return View(dto);
    }

    /// <summary>
    /// Renders the Create form with the customer dropdown pre-populated for optional recipient and
    /// purchaser selection.
    /// </summary>
    public async Task<IActionResult> Create()
    {
        await PopulateCustomersAsync();
        return View(new CreateGiftCertificateDto());
    }

    /// <summary>
    /// Issues a new gift certificate with an auto-generated GC-YYMM-#### code and sets its initial
    /// status to Active. Purchase price and purchasing customer are only recorded when the issued
    /// reason is <c>Sold</c>; for goodwill, promotional, and refund certificates there is no
    /// associated sale transaction, so those fields are explicitly nulled to prevent misleading
    /// financial data. The certificate is always created with <c>RedeemedAmount = 0</c> — redemptions
    /// are recorded separately via invoice payment flow.
    /// </summary>
    [HttpPost, ValidateAntiForgeryToken]
    public async Task<IActionResult> Create(CreateGiftCertificateDto dto)
    {
        if (!ModelState.IsValid)
        {
            await PopulateCustomersAsync();
            return View(dto);
        }

        try
        {
            var currentUser = await _userManager.GetUserAsync(User);
            var companyId = currentUser?.CompanyId ?? 0;

            var code = await GenerateCertificateCodeAsync(companyId);

            var cert = new GiftCertificate
            {
                CertificateCode = code,
                OriginalAmount = dto.Amount,
                RedeemedAmount = 0,
                RecipientCustomerId = dto.RecipientCustomerId,
                RecipientName = dto.RecipientName,
                RecipientEmail = dto.RecipientEmail,
                IssuedReason = dto.IssuedReason,
                PurchasePrice = dto.IssuedReason == GiftCertificateIssuedReason.Sold ? dto.PurchasePrice : null,
                PurchasingCustomerId = dto.IssuedReason == GiftCertificateIssuedReason.Sold ? dto.PurchasingCustomerId : null,
                Status = GiftCertificateStatus.Active,
                IssueDate = DateTime.UtcNow,
                ExpiryDate = dto.ExpiryDate,
                Notes = dto.Notes,
                IssuedById = currentUser?.Id,
                CompanyId = companyId,
                CreatedAt = DateTime.UtcNow,
                CreatedBy = currentUser?.Email
            };

            await _unitOfWork.GiftCertificates.AddAsync(cert);
            await _unitOfWork.CompleteAsync();

            // GL: CR Gift Certificate Liability (2500) for the face value.
            // Debit side varies by reason:
            //   Sold   → DR Checking (received cash outside invoice flow)
            //   Others → DR Sales Discounts 4950 (promotional/goodwill cost)
            var gcLiabilityAcctId = await GetGcLiabilityAccountIdAsync(companyId);
            await _accountBalanceService.CreditAsync(gcLiabilityAcctId, cert.OriginalAmount);
            if (dto.IssuedReason == GiftCertificateIssuedReason.Sold)
            {
                var checkingAcctId = await _unitOfWork.Accounts.FirstOrDefaultAsync(
                    a => a.CompanyId == companyId && a.IsActive && (a.AccountSubType == AccountSubTypeEnum.Checking
                                     || a.AccountSubType == AccountSubTypeEnum.Cash));
                await _accountBalanceService.DebitAsync(checkingAcctId?.Id, cert.OriginalAmount);
            }
            else
            {
                var discountAcctId = await _unitOfWork.Accounts.FirstOrDefaultAsync(
                    a => a.CompanyId == companyId && a.IsActive && a.AccountNumber == "4950");
                await _accountBalanceService.DebitAsync(discountAcctId?.Id, cert.OriginalAmount);
            }

            TempData["Success"] = $"Gift certificate {code} for {dto.Amount:C} created successfully.";
            return RedirectToAction(nameof(Details), new { id = cert.Id });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error creating gift certificate");
            this.ToastError("An error occurred creating the gift certificate.");
            await PopulateCustomersAsync();
            return View(dto);
        }
    }

    /// <summary>
    /// Voids a gift certificate, permanently cancelling any remaining balance. Requires
    /// <c>CompanyAdminOnly</c> because voiding has revenue implications — it eliminates a liability
    /// the company owes to the certificate holder. Voiding a FullyRedeemed certificate is blocked
    /// because it would have no practical effect (the balance is already zero) and could create
    /// confusion in audit logs.
    /// </summary>
    [HttpPost, ValidateAntiForgeryToken]
    [Authorize(Policy = AppConstants.Policies.CompanyAdminOnly)]
    public async Task<IActionResult> Void(int id)
    {
        var cert = await _unitOfWork.GiftCertificates.GetByIdAsync(id);
        if (cert == null) return NotFound();

        if (cert.Status == GiftCertificateStatus.FullyRedeemed)
        {
            TempData["Error"] = "Cannot void a fully redeemed gift certificate.";
            return RedirectToAction(nameof(Details), new { id });
        }

        var remaining = cert.RemainingBalance;
        cert.Status = GiftCertificateStatus.Voided;
        cert.UpdatedAt = DateTime.UtcNow;
        await _unitOfWork.GiftCertificates.UpdateAsync(cert);
        await _unitOfWork.CompleteAsync();

        // GL: DR GC Liability / CR Other Income (breakage — the company keeps the unredeemed amount)
        if (remaining > 0)
        {
            var currentUser = await _userManager.GetUserAsync(User);
            var companyId   = currentUser?.CompanyId ?? 0;
            var gcLiabilityAcctId = await GetGcLiabilityAccountIdAsync(companyId);
            var otherIncomeAcctId = await _unitOfWork.Accounts.FirstOrDefaultAsync(
                a => a.CompanyId == companyId && a.IsActive && a.AccountSubType == AccountSubTypeEnum.OtherIncome);
            await _accountBalanceService.DebitAsync(gcLiabilityAcctId, remaining);
            await _accountBalanceService.CreditAsync(otherIncomeAcctId?.Id, remaining);
        }

        TempData["Success"] = $"Gift certificate {cert.CertificateCode} has been voided.";
        return RedirectToAction(nameof(Index));
    }

    /// <summary>
    /// Generates and streams a branded gift certificate PDF. The DTO is built manually here (rather
    /// than reusing the Details action's DTO) so that the PDF endpoint is self-contained and does not
    /// depend on view state from a prior page load. Company logo bytes and content type are passed
    /// directly to the PDF service so QuestPDF can embed the logo without a second HTTP request.
    /// The file name includes the certificate code so the downloaded file is immediately identifiable
    /// in the user's downloads folder.
    /// </summary>
    public async Task<IActionResult> DownloadPdf(int id)
    {
        var cert = await _unitOfWork.GiftCertificates.GetByIdAsync(id, false,
            gc => gc.RecipientCustomer,
            gc => gc.PurchasingCustomer,
            gc => gc.IssuedBy,
            gc => gc.Redemptions);

        if (cert == null) return NotFound();

        var currentUser = await _userManager.GetUserAsync(User);
        var companyId = currentUser?.CompanyId ?? 0;
        var company = await _unitOfWork.Companies.GetByIdAsync(companyId);

        var companyInfo = new Application.DTOs.Company.CompanyInfoDto
        {
            CompanyName = company?.CompanyName ?? string.Empty,
            Phone = company?.Phone,
            Address = company?.Address,
            City = company?.City,
            State = company?.State,
            ZipCode = company?.ZipCode,
            PrimaryContactEmail = company?.PrimaryContactEmail
        };

        var dto = new GiftCertificateDto
        {
            Id = cert.Id,
            CertificateCode = cert.CertificateCode,
            OriginalAmount = cert.OriginalAmount,
            RedeemedAmount = cert.RedeemedAmount,
            RemainingBalance = cert.RemainingBalance,
            RecipientCustomerId = cert.RecipientCustomerId,
            RecipientName = cert.RecipientCustomer != null
                ? (cert.RecipientCustomer.CompanyName ?? $"{cert.RecipientCustomer.ContactFirstName} {cert.RecipientCustomer.ContactLastName}".Trim())
                : cert.RecipientName,
            RecipientEmail = cert.RecipientEmail,
            IssuedReason = cert.IssuedReason,
            PurchasePrice = cert.PurchasePrice,
            PurchasingCustomerId = cert.PurchasingCustomerId,
            PurchasingCustomerName = cert.PurchasingCustomer != null
                ? (cert.PurchasingCustomer.CompanyName ?? $"{cert.PurchasingCustomer.ContactFirstName} {cert.PurchasingCustomer.ContactLastName}".Trim())
                : null,
            Status = cert.Status,
            IssueDate = cert.IssueDate,
            ExpiryDate = cert.ExpiryDate,
            Notes = cert.Notes,
            IssuedByName = cert.IssuedBy != null
                ? $"{cert.IssuedBy.FirstName} {cert.IssuedBy.LastName}".Trim()
                : null
        };

        try
        {
            var (logoData, logoContentType) = await LoadCompanyLogoAsync(company);
            var pdfBytes = await _pdfService.GenerateGiftCertificatePdfAsync(dto, logoData, logoContentType, companyInfo);
            return File(pdfBytes, "application/pdf", $"GiftCertificate-{cert.CertificateCode}.pdf");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error generating gift certificate PDF for {Code}", cert.CertificateCode);
            TempData["Error"] = "Could not generate PDF.";
            return RedirectToAction(nameof(Details), new { id });
        }
    }

    /// <summary>
    /// Generates the next sequential GC-YYMM-#### certificate code for the given company. The
    /// sequence resets each calendar month (the YYMM component changes) which keeps codes short and
    /// readable. <c>ignoreQueryFilters: true</c> is passed so soft-deleted certificates from the same
    /// month are included in the max calculation, preventing number reuse after a certificate is
    /// deleted. The zero-padded 4-digit suffix supports up to 9999 certificates per company per month
    /// before overflow.
    /// </summary>
    private async Task<string> GenerateCertificateCodeAsync(int companyId)
    {
        var prefix = $"GC-{DateTime.UtcNow:yy}{DateTime.UtcNow.Month:D2}-";
        var companyCerts = await _unitOfWork.GiftCertificates.FindAsync(
            c => c.CompanyId == companyId && c.CertificateCode.StartsWith(prefix), true);
        var maxNum = companyCerts
            .Select(c => { int.TryParse(c.CertificateCode.Replace(prefix, ""), out int n); return n; })
            .DefaultIfEmpty(0).Max();
        return $"{prefix}{(maxNum + 1):D4}";
    }

    /// <summary>
    /// Populates <c>ViewBag.Customers</c> with active customers for the recipient and purchaser
    /// dropdowns. A "— None —" placeholder is prepended because both fields are optional: a gift
    /// certificate can be issued to a non-customer recipient (no customer record exists) and a
    /// goodwill/promotional certificate has no purchaser at all.
    /// </summary>
    private async Task PopulateCustomersAsync()
    {
        var companyId = (await _userManager.GetUserAsync(User))?.CompanyId ?? 0;
        var customers = await _unitOfWork.Customers.FindAsync(c => c.CompanyId == companyId && c.IsActive);
        var list = customers
            .OrderBy(c => c.CompanyName ?? c.ContactFirstName)
            .Select(c => new SelectListItem
            {
                Value = c.Id.ToString(),
                Text = c.CompanyName ?? $"{c.ContactFirstName} {c.ContactLastName}".Trim()
            })
            .ToList();
        list.Insert(0, new SelectListItem { Value = "", Text = "— None (non-customer recipient) —" });
        ViewBag.Customers = list;
    }

    /// <summary>Returns the Gift Certificate Liability account ID (account 2500) for the company.</summary>
    private async Task<int?> GetGcLiabilityAccountIdAsync(int companyId)
    {
        var acct = await _unitOfWork.Accounts.FirstOrDefaultAsync(
            a => a.CompanyId == companyId && a.IsActive && a.AccountNumber == "2500");
        return acct?.Id;
    }

    /// <summary>
    /// Shows the bulk certificate creation form. Defaults to Promotional reason and 25 certificates
    /// since the primary use case is car shows and events where a batch of same-value certificates
    /// is distributed to attendees.
    /// </summary>
    public IActionResult BulkCreate()
    {
        return View(new BulkCreateGiftCertificateDto());
    }

    /// <summary>
    /// Creates N gift certificates in a single batch, records GL entries for each, then redirects
    /// to a confirmation page where the user can download the full batch as a single print-ready PDF.
    /// Certificate codes are generated sequentially so the batch occupies a contiguous range (e.g.
    /// GC-2506-0012 through GC-2506-0036), making it easy to audit which codes belong to each event.
    /// GL treatment mirrors single-certificate issuance: Sold certs debit Checking, all others debit
    /// Sales Discounts (4950) and credit GC Liability (2500).
    /// </summary>
    [HttpPost, ValidateAntiForgeryToken]
    public async Task<IActionResult> BulkCreate(BulkCreateGiftCertificateDto dto)
    {
        if (!ModelState.IsValid)
            return View(dto);

        try
        {
            var currentUser = await _userManager.GetUserAsync(User);
            var companyId = currentUser?.CompanyId ?? 0;

            var gcLiabilityAcctId = await GetGcLiabilityAccountIdAsync(companyId);
            int? checkingAcctId = null;
            int? discountAcctId = null;

            if (dto.IssuedReason == GiftCertificateIssuedReason.Sold)
            {
                var acct = await _unitOfWork.Accounts.FirstOrDefaultAsync(
                    a => a.CompanyId == companyId && a.IsActive && (a.AccountSubType == AccountSubTypeEnum.Checking
                                     || a.AccountSubType == AccountSubTypeEnum.Cash));
                checkingAcctId = acct?.Id;
            }
            else
            {
                var acct = await _unitOfWork.Accounts.FirstOrDefaultAsync(
                    a => a.CompanyId == companyId && a.IsActive && a.AccountNumber == "4950");
                discountAcctId = acct?.Id;
            }

            var batchId = Guid.NewGuid();
            var now = DateTime.UtcNow;

            for (int i = 0; i < dto.Quantity; i++)
            {
                var code = await GenerateCertificateCodeAsync(companyId);

                var cert = new GiftCertificate
                {
                    CertificateCode = code,
                    OriginalAmount = dto.Amount,
                    RedeemedAmount = 0,
                    IssuedReason = dto.IssuedReason,
                    Status = GiftCertificateStatus.Active,
                    IssueDate = now,
                    ExpiryDate = dto.ExpiryDate,
                    Notes = dto.Notes,
                    IssuedById = currentUser?.Id,
                    CompanyId = companyId,
                    CreatedAt = now,
                    CreatedBy = currentUser?.Email,
                    BatchId = batchId
                };

                await _unitOfWork.GiftCertificates.AddAsync(cert);
                await _unitOfWork.CompleteAsync();

                await _accountBalanceService.CreditAsync(gcLiabilityAcctId, cert.OriginalAmount);
                if (dto.IssuedReason == GiftCertificateIssuedReason.Sold)
                    await _accountBalanceService.DebitAsync(checkingAcctId, cert.OriginalAmount);
                else
                    await _accountBalanceService.DebitAsync(discountAcctId, cert.OriginalAmount);
            }

            return RedirectToAction(nameof(BulkResult), new { batchId });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error creating bulk gift certificates");
            this.ToastError("An error occurred creating the certificates.");
            return View(dto);
        }
    }

    /// <summary>
    /// Displays the batch confirmation page. Driven by BatchId so it is bookmarkable and survives
    /// browser back/refresh — the user can return here any time to re-download the batch PDF.
    /// </summary>
    public async Task<IActionResult> BulkResult(Guid batchId)
    {
        if (batchId == Guid.Empty)
            return RedirectToAction(nameof(Index));

        var certs = await _unitOfWork.GiftCertificates.FindAsync(
            gc => gc.BatchId == batchId, false);

        if (!certs.Any())
            return RedirectToAction(nameof(Index));

        return View(certs.OrderBy(c => c.CertificateCode).ToList());
    }

    /// <summary>
    /// Streams a multi-page PDF for an entire batch identified by BatchId. GET endpoint so the
    /// user can bookmark or re-open it at any time after the batch was originally created.
    /// </summary>
    public async Task<IActionResult> BatchDownloadPdf(Guid batchId)
    {
        if (batchId == Guid.Empty)
            return BadRequest();

        var currentUser = await _userManager.GetUserAsync(User);
        var companyId = currentUser?.CompanyId ?? 0;
        var company = await _unitOfWork.Companies.GetByIdAsync(companyId);

        var companyInfo = new Application.DTOs.Company.CompanyInfoDto
        {
            CompanyName = company?.CompanyName ?? string.Empty,
            Phone = company?.Phone,
            Address = company?.Address,
            City = company?.City,
            State = company?.State,
            ZipCode = company?.ZipCode,
            PrimaryContactEmail = company?.PrimaryContactEmail
        };

        var certs = await _unitOfWork.GiftCertificates.FindAsync(
            gc => gc.BatchId == batchId, false,
            gc => gc.RecipientCustomer);

        if (!certs.Any())
            return NotFound();

        var dtos = certs.OrderBy(c => c.CertificateCode).Select(cert => new GiftCertificateDto
        {
            Id = cert.Id,
            CertificateCode = cert.CertificateCode,
            OriginalAmount = cert.OriginalAmount,
            RedeemedAmount = cert.RedeemedAmount,
            RemainingBalance = cert.RemainingBalance,
            RecipientName = cert.RecipientCustomer != null
                ? (cert.RecipientCustomer.CompanyName ?? $"{cert.RecipientCustomer.ContactFirstName} {cert.RecipientCustomer.ContactLastName}".Trim())
                : cert.RecipientName,
            RecipientEmail = cert.RecipientEmail,
            IssuedReason = cert.IssuedReason,
            Status = cert.Status,
            IssueDate = cert.IssueDate,
            ExpiryDate = cert.ExpiryDate,
            Notes = cert.Notes
        }).ToList();

        try
        {
            var (logoData, logoContentType) = await LoadCompanyLogoAsync(company);
            var pdfBytes = await _pdfService.GenerateBulkGiftCertificatePdfAsync(dtos, logoData, logoContentType, companyInfo);
            var first = dtos.First().CertificateCode;
            var last = dtos.Last().CertificateCode;
            var fileName = dtos.Count == 1
                ? $"GiftCertificate-{first}.pdf"
                : $"GiftCertificates-{first}-to-{last}.pdf";
            return File(pdfBytes, "application/pdf", fileName);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error generating batch gift certificate PDF for batch {BatchId}", batchId);
            TempData["Error"] = "Could not generate PDF.";
            return RedirectToAction(nameof(BulkResult), new { batchId });
        }
    }

    private async Task<(byte[]? LogoData, string? LogoContentType)> LoadCompanyLogoAsync(Company? company)
    {
        if (company == null) return (null, null);
        if (!string.IsNullOrEmpty(company.LogoFilePath))
        {
            var (ok, content, contentType, _) = await _logoService.GetCompanyLogoAsync(company.LogoFilePath);
            if (ok) return (content, contentType);
        }
        return (company.LogoData, company.LogoContentType);
    }
}
