using AutoMapper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using PowderCoating.Application.DTOs.Company;
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 QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using AccountSubTypeEnum = PowderCoating.Core.Enums.AccountSubType;

namespace PowderCoating.Web.Controllers;

[Authorize(Policy = AppConstants.Policies.CanManageJobs)]
public class DepositsController : Controller
{
    private readonly IUnitOfWork _unitOfWork;
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly ILogger<DepositsController> _logger;
    private readonly ICompanyLogoService _logoService;
    private readonly IAccountBalanceService _accountBalanceService;

    public DepositsController(
        IUnitOfWork unitOfWork,
        UserManager<ApplicationUser> userManager,
        ILogger<DepositsController> logger,
        ICompanyLogoService logoService,
        IAccountBalanceService accountBalanceService)
    {
        _unitOfWork = unitOfWork;
        _userManager = userManager;
        _logger = logger;
        _logoService = logoService;
        _accountBalanceService = accountBalanceService;
    }

    // -----------------------------------------------------------------------
    // POST: /Deposits/Record  — AJAX call from Job/Quote Details modal
    // -----------------------------------------------------------------------
    /// <summary>
    /// Records a customer deposit via AJAX from the Job Details or Quote Details modal. A deposit
    /// must be linked to at least one of <paramref name="jobId"/> or <paramref name="quoteId"/>
    /// so it can be matched to a specific obligation. The deposit is stored as unapplied
    /// (<c>AppliedToInvoiceId</c> is null) until <c>InvoicesController</c> auto-applies it when
    /// creating the invoice, at which point <c>AppliedToInvoiceId</c> is set and the deposit can
    /// no longer be deleted. Returns a JSON object so the view's JavaScript can append the new
    /// row to the deposits table without a full page reload.
    /// A receipt number in the format <c>DEP-YYMM-####</c> is generated via
    /// <see cref="GenerateReceiptNumberAsync"/> using <c>IgnoreQueryFilters</c> to prevent
    /// number reuse after soft deletion.
    /// </summary>
    [HttpPost, ValidateAntiForgeryToken]
    public async Task<IActionResult> Record(
        int? jobId,
        int? quoteId,
        int customerId,
        decimal amount,
        string paymentMethod,
        DateTime receivedDate,
        string? reference,
        string? notes,
        int? depositAccountId = null)
    {
        try
        {
            if (amount <= 0)
                return Json(new { success = false, message = "Amount must be greater than zero." });

            if (jobId == null && quoteId == null)
                return Json(new { success = false, message = "A Job or Quote must be specified." });

            if (!Enum.TryParse<PaymentMethod>(paymentMethod, out var method))
                return Json(new { success = false, message = "Invalid payment method." });

            var currentUser = await _userManager.GetUserAsync(User);
            if (currentUser == null) return Unauthorized();

            var receiptNumber = await GenerateReceiptNumberAsync(currentUser.CompanyId);

            // Resolve the bank/asset account the deposit lands in. The user now picks this on
            // the form; if they didn't (or the value is stale), fall back to the legacy
            // auto-pick of the first Checking/Cash account. Validate any user-supplied id
            // belongs to this company (defense in depth — the global filter alone isn't enough).
            int? depositAcctId = null;
            if (depositAccountId.HasValue &&
                await AccountGuard.IsValidMoneyAccountAsync(_unitOfWork, depositAccountId, currentUser.CompanyId))
            {
                depositAcctId = depositAccountId;
            }
            depositAcctId ??= await GetCheckingAccountIdAsync(currentUser.CompanyId);

            // Guard against an unbalanced GL posting: this deposit credits the Customer Deposits
            // liability (2300). If that account exists but we have no bank/asset account to debit,
            // the entry would be one-sided. Block it so the user picks a deposit account first.
            // (When 2300 doesn't exist — e.g. a company not using accounting — no GL posts at all,
            // so a missing bank account is harmless and the deposit is allowed through.)
            var custDepositsAcctId = await GetCustomerDepositsAccountIdAsync(currentUser.CompanyId);
            if (custDepositsAcctId != null && depositAcctId == null)
                return Json(new
                {
                    success = false,
                    message = "Select a deposit account (the bank/asset account this payment lands in) " +
                              "before recording. None is configured for your company yet."
                });

            var deposit = new Deposit
            {
                ReceiptNumber    = receiptNumber,
                CustomerId       = customerId,
                JobId            = jobId,
                QuoteId          = quoteId,
                Amount           = amount,
                PaymentMethod    = method,
                ReceivedDate     = receivedDate,
                Reference        = reference,
                Notes            = notes,
                DepositAccountId = depositAcctId,
                RecordedById     = currentUser.Id,
                CompanyId        = currentUser.CompanyId,
                CreatedAt        = DateTime.UtcNow,
                CreatedBy        = currentUser.Email
            };

            await _unitOfWork.Deposits.AddAsync(deposit);
            await _unitOfWork.CompleteAsync();

            // GL: DR Checking (cash received) / CR Customer Deposits 2300 (liability until applied to invoice).
            await _accountBalanceService.DebitAsync(depositAcctId, deposit.Amount);
            await _accountBalanceService.CreditAsync(custDepositsAcctId, deposit.Amount);

            return Json(new
            {
                success = true,
                depositId = deposit.Id,
                receiptNumber = deposit.ReceiptNumber,
                amount = deposit.Amount,
                paymentMethod = GetPaymentMethodDisplay(method),
                receivedDate = deposit.ReceivedDate.ToString("MM/dd/yyyy"),
                notes = deposit.Notes,
                reference = deposit.Reference
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error recording deposit for job={JobId} quote={QuoteId}", jobId, quoteId);
            return Json(new { success = false, message = "An error occurred recording the deposit." });
        }
    }

    // -----------------------------------------------------------------------
    // POST: /Deposits/Delete/5
    // -----------------------------------------------------------------------
    /// <summary>
    /// Soft-deletes a deposit via AJAX. A deposit that has already been applied to an invoice
    /// (<c>AppliedToInvoiceId</c> is not null) cannot be deleted because removing it would make
    /// the invoice's payment history inconsistent — the invoice <c>AmountPaid</c> field would
    /// no longer match the sum of its payment records. Returns JSON so the calling page can
    /// remove the row from the UI without a full page reload.
    /// </summary>
    [HttpPost, ValidateAntiForgeryToken]
    public async Task<IActionResult> Delete(int id, string? returnUrl)
    {
        try
        {
            var deposit = await _unitOfWork.Deposits.GetByIdAsync(id);
            if (deposit == null) return Json(new { success = false, message = "Deposit not found." });

            if (deposit.AppliedToInvoiceId != null)
                return Json(new { success = false, message = "This deposit has already been applied to an invoice and cannot be deleted." });

            // Reverse the GL entry made at recording time: CR Checking / DR Customer Deposits 2300.
            var custDepositsAcctId = await GetCustomerDepositsAccountIdAsync(deposit.CompanyId);
            await _accountBalanceService.CreditAsync(deposit.DepositAccountId, deposit.Amount);
            await _accountBalanceService.DebitAsync(custDepositsAcctId, deposit.Amount);

            await _unitOfWork.Deposits.SoftDeleteAsync(id);
            await _unitOfWork.CompleteAsync();

            return Json(new { success = true });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error deleting deposit {DepositId}", id);
            return Json(new { success = false, message = "An error occurred deleting the deposit." });
        }
    }

    // -----------------------------------------------------------------------
    // GET: /Deposits/Receipt/5  — PDF receipt download
    // -----------------------------------------------------------------------
    /// <summary>
    /// Generates and streams a deposit receipt PDF inline (browser opens rather than downloads).
    /// The PDF is built entirely inside this action using QuestPDF directly — deliberately NOT
    /// delegated to <c>IPdfService</c> — because the deposit receipt layout is small and
    /// self-contained, and adding it to the shared PDF service would require coupling the service
    /// to deposit-specific entities. The receipt uses the tenant's accent colour (from
    /// <c>CompanyPreferences.InAccentColor</c>) for branding; if no colour is configured it
    /// falls back to <c>Colors.Blue.Darken2</c>. A company-isolation check guards against
    /// cross-tenant access; SuperAdmins bypass this check.
    /// </summary>
    public async Task<IActionResult> Receipt(int id)
    {
        try
        {
            var deposit = await _unitOfWork.Deposits.GetByIdAsync(id, false,
                d => d.Customer,
                d => d.Job,
                d => d.Quote);

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

            var currentUser = await _userManager.GetUserAsync(User);
            if (currentUser == null) return Unauthorized();

            // Verify the deposit belongs to the user's company
            if (deposit.CompanyId != currentUser.CompanyId && !User.IsInRole("SuperAdmin"))
                return Forbid();

            var company = await _unitOfWork.Companies.GetByIdAsync(deposit.CompanyId);
            var prefs = await _unitOfWork.CompanyPreferences.FirstOrDefaultAsync(
                p => p.CompanyId == deposit.CompanyId && !p.IsDeleted);

            var companyInfo = new 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 (logoData, logoContentType) = await LoadCompanyLogoAsync(company);
            var pdfBytes = GenerateReceiptPdf(deposit, logoData, logoContentType, companyInfo, prefs?.InAccentColor);
            Response.Headers["Content-Disposition"] = $"inline; filename=\"Deposit-Receipt-{deposit.ReceiptNumber}.pdf\"";
            return File(pdfBytes, "application/pdf");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error generating receipt for deposit {DepositId}", id);
            return StatusCode(500, "An error occurred generating the receipt.");
        }
    }

    // -----------------------------------------------------------------------
    // Private helpers
    // -----------------------------------------------------------------------

    /// <summary>
    /// Generates a monotonically increasing deposit receipt number in the format
    /// <c>DEP-YYMM-####</c> (e.g. <c>DEP-2604-0001</c> for April 2026). Scoped to the company
    /// so each tenant has an independent sequence. Uses <c>IgnoreQueryFilters()</c> to include
    /// soft-deleted records in the scan and prevent number reuse.
    /// </summary>
    private async Task<string> GenerateReceiptNumberAsync(int companyId)
    {
        var prefix = $"DEP-{DateTime.UtcNow:yy}{DateTime.UtcNow.Month:D2}-";

        var existing = (await _unitOfWork.Deposits.FindAsync(
                d => d.CompanyId == companyId && d.ReceiptNumber.StartsWith(prefix),
                ignoreQueryFilters: true))
            .Select(d => d.ReceiptNumber)
            .ToList();

        var maxNum = 0;
        foreach (var num in existing)
        {
            var suffix = num.Length >= prefix.Length + 4 ? num.Substring(prefix.Length) : "";
            if (int.TryParse(suffix, out int n) && n > maxNum)
                maxNum = n;
        }

        return $"{prefix}{(maxNum + 1):D4}";
    }

    /// <summary>
    /// Maps a <see cref="PaymentMethod"/> enum value to the user-facing display string shown on
    /// receipts and in the AJAX response. Uses exhaustive switch-expression rather than
    /// <c>.ToString()</c> so that internal enum names (e.g. <c>CreditDebitCard</c>) can be
    /// displayed in a more readable form (e.g. "Credit / Debit Card").
    /// </summary>
    private static string GetPaymentMethodDisplay(PaymentMethod method) => method switch
    {
        PaymentMethod.Cash => "Cash",
        PaymentMethod.Check => "Check",
        PaymentMethod.CreditDebitCard => "Credit / Debit Card",
        PaymentMethod.BankTransferACH => "Bank Transfer / ACH",
        PaymentMethod.DigitalPayment => "Digital Payment",
        PaymentMethod.StoreCredit => "Store Credit",
        _ => method.ToString()
    };

    /// <summary>
    /// Builds the deposit receipt PDF bytes using QuestPDF's fluent API. The layout places the
    /// company header band (with logo or text fallback, phone, and email) at the top, followed
    /// by a "Received From" / "Applied To" row, a large amount box, optional notes, and a footer
    /// note indicating whether the deposit is pending or has been applied to an invoice. The
    /// accent colour is taken from the tenant's <c>CompanyPreferences.InAccentColor</c> hex
    /// string; invalid or missing values fall back to QuestPDF's <c>Colors.Blue.Darken2</c>.
    /// PDF generation is entirely synchronous (QuestPDF is not async) so this method is not async.
    /// </summary>
    private byte[] GenerateReceiptPdf(
        Deposit deposit,
        byte[]? logoData,
        string? logoContentType,
        CompanyInfoDto companyInfo,
        string? accentHex)
    {
        QuestPDF.Settings.License = LicenseType.Community;
        var accent = ResolveColor(accentHex, Colors.Blue.Darken2);

        var document = Document.Create(container =>
        {
            container.Page(page =>
            {
                page.Size(PageSizes.Letter);
                page.Margin(0.75f, Unit.Inch);
                page.PageColor(Colors.White);
                page.DefaultTextStyle(x => x.FontSize(10).FontFamily("Arial"));

                page.Content().Column(col =>
                {
                    // Header band
                    col.Item().Background(accent).Padding(16).Row(row =>
                    {
                        // Logo or company name
                        if (logoData != null && logoData.Length > 0)
                        {
                            row.ConstantItem(80).Image(logoData).FitArea();
                            row.RelativeItem().PaddingLeft(12).Column(c =>
                            {
                                c.Item().Text(companyInfo.CompanyName).FontSize(18).Bold().FontColor(Colors.White);
                                if (!string.IsNullOrWhiteSpace(companyInfo.Phone))
                                    c.Item().Text(companyInfo.Phone).FontSize(9).FontColor(Colors.White);
                                if (!string.IsNullOrWhiteSpace(companyInfo.PrimaryContactEmail))
                                    c.Item().Text(companyInfo.PrimaryContactEmail).FontSize(9).FontColor(Colors.White);
                            });
                        }
                        else
                        {
                            row.RelativeItem().Column(c =>
                            {
                                c.Item().Text(companyInfo.CompanyName).FontSize(20).Bold().FontColor(Colors.White);
                                if (!string.IsNullOrWhiteSpace(companyInfo.Phone))
                                    c.Item().Text(companyInfo.Phone).FontSize(9).FontColor(Colors.White);
                                if (!string.IsNullOrWhiteSpace(companyInfo.PrimaryContactEmail))
                                    c.Item().Text(companyInfo.PrimaryContactEmail).FontSize(9).FontColor(Colors.White);
                            });
                        }

                        row.ConstantItem(160).AlignRight().Column(c =>
                        {
                            c.Item().Text("DEPOSIT RECEIPT").FontSize(22).Bold().FontColor(Colors.White);
                            c.Item().Text(deposit.ReceiptNumber).FontSize(11).FontColor(Colors.White);
                            c.Item().Text(deposit.ReceivedDate.ToString("MMMM dd, yyyy")).FontSize(9).FontColor(Colors.White);
                        });
                    });

                    col.Item().PaddingTop(24).Row(row =>
                    {
                        // Bill To
                        row.RelativeItem().Column(c =>
                        {
                            c.Item().Text("RECEIVED FROM").FontSize(8).Bold().FontColor(Colors.Grey.Darken1);
                            c.Item().Text(deposit.Customer.CompanyName ?? $"{deposit.Customer.ContactFirstName} {deposit.Customer.ContactLastName}".Trim()).FontSize(12).Bold();
                            if (!string.IsNullOrWhiteSpace(deposit.Customer.Email))
                                c.Item().Text(deposit.Customer.Email).FontSize(9);
                            if (!string.IsNullOrWhiteSpace(deposit.Customer.Phone))
                                c.Item().Text(deposit.Customer.Phone).FontSize(9);
                        });

                        // Applied To
                        row.RelativeItem().Column(c =>
                        {
                            c.Item().Text("APPLIED TO").FontSize(8).Bold().FontColor(Colors.Grey.Darken1);
                            if (deposit.Job != null)
                            {
                                c.Item().Text($"Job #{deposit.Job.JobNumber}").FontSize(11).Bold();
                                if (!string.IsNullOrWhiteSpace(deposit.Job.Description))
                                    c.Item().Text(deposit.Job.Description).FontSize(9);
                            }
                            else if (deposit.Quote != null)
                            {
                                c.Item().Text($"Quote #{deposit.Quote.QuoteNumber}").FontSize(11).Bold();
                                if (!string.IsNullOrWhiteSpace(deposit.Quote.Description))
                                    c.Item().Text(deposit.Quote.Description).FontSize(9);
                            }
                        });
                    });

                    // Divider
                    col.Item().PaddingTop(20).PaddingBottom(20)
                        .LineHorizontal(1).LineColor(Colors.Grey.Lighten2);

                    // Amount box
                    col.Item().Background(Colors.Grey.Lighten4).Padding(20).Row(row =>
                    {
                        row.RelativeItem().Column(c =>
                        {
                            c.Item().Text("AMOUNT RECEIVED").FontSize(10).Bold().FontColor(Colors.Grey.Darken1);
                            c.Item().Text($"{deposit.Amount:C}").FontSize(32).Bold().FontColor(accent);
                        });
                        row.ConstantItem(200).AlignRight().Column(c =>
                        {
                            c.Item().Text("PAYMENT METHOD").FontSize(8).Bold().FontColor(Colors.Grey.Darken1);
                            c.Item().Text(GetPaymentMethodDisplay(deposit.PaymentMethod)).FontSize(13);
                            if (!string.IsNullOrWhiteSpace(deposit.Reference))
                            {
                                c.Item().PaddingTop(6).Text("REFERENCE").FontSize(8).Bold().FontColor(Colors.Grey.Darken1);
                                c.Item().Text(deposit.Reference).FontSize(11);
                            }
                        });
                    });

                    // Notes
                    if (!string.IsNullOrWhiteSpace(deposit.Notes))
                    {
                        col.Item().PaddingTop(16).Column(c =>
                        {
                            c.Item().Text("NOTES").FontSize(8).Bold().FontColor(Colors.Grey.Darken1);
                            c.Item().Text(deposit.Notes).FontSize(10);
                        });
                    }

                    // Status note
                    col.Item().PaddingTop(28).LineHorizontal(1).LineColor(Colors.Grey.Lighten2);
                    col.Item().PaddingTop(12).AlignCenter().Text(text =>
                    {
                        text.Span("This deposit will be applied to your invoice when the job is complete. ").FontSize(9).FontColor(Colors.Grey.Darken1);
                        if (deposit.AppliedToInvoiceId != null)
                            text.Span("(Applied)").FontSize(9).Bold().FontColor(Colors.Green.Darken2);
                        else
                            text.Span("(Pending application)").FontSize(9).Italic().FontColor(Colors.Orange.Darken2);
                    });

                    col.Item().PaddingTop(8).AlignCenter()
                        .Text($"Thank you for your business — {companyInfo.CompanyName}")
                        .FontSize(9).Italic().FontColor(Colors.Grey.Medium);
                });
            });
        });

        return document.GeneratePdf();
    }

    /// <summary>
    /// Returns <paramref name="hex"/> if it is non-empty and starts with <c>#</c> (a valid CSS
    /// hex colour), otherwise returns <paramref name="fallback"/>. QuestPDF requires the leading
    /// hash, so this guard prevents colour values imported without it from breaking the PDF.
    /// </summary>
    private static string ResolveColor(string? hex, string fallback)
    {
        if (string.IsNullOrWhiteSpace(hex)) return fallback;
        return hex.StartsWith("#") ? hex : fallback;
    }

    /// <summary>Returns the first active Checking or Cash account for the company, or null.</summary>
    private async Task<int?> GetCheckingAccountIdAsync(int companyId)
    {
        var acct = await _unitOfWork.Accounts.FirstOrDefaultAsync(
            a => a.CompanyId == companyId && a.IsActive
              && (a.AccountSubType == AccountSubTypeEnum.Checking
               || a.AccountSubType == AccountSubTypeEnum.Cash));
        return acct?.Id;
    }

    /// <summary>Returns account 2300 "Customer Deposits" liability for the company, or null.</summary>
    private async Task<int?> GetCustomerDepositsAccountIdAsync(int companyId)
    {
        var acct = await _unitOfWork.Accounts.FirstOrDefaultAsync(
            a => a.CompanyId == companyId && a.IsActive && a.AccountNumber == "2300");
        return acct?.Id;
    }

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