using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PowderCoating.Application.DTOs.Accounting;
using PowderCoating.Application.Interfaces;
using PowderCoating.Core.Accounting;
using PowderCoating.Core.Enums;
using PowderCoating.Infrastructure.Data;

namespace PowderCoating.Infrastructure.Services;

/// <summary>
/// Builds chronological ledger reports for individual accounts by querying every transaction
/// table that can touch a given account (Payments, Expenses, BillPayments, InvoiceItems, Bills,
/// AR, AP) and computing a running balance. Also drives <see cref="AccountBalanceService.RecalculateAllAsync"/>
/// by returning the authoritative closing balance.
/// </summary>
public class LedgerService : ILedgerService
{
    private readonly ApplicationDbContext _context;
    private readonly ILogger<LedgerService> _logger;

    /// <summary>
    /// Constructs the service with direct <see cref="ApplicationDbContext"/> access.
    /// Direct context access (rather than repositories) is intentional here: the ledger
    /// must query multiple tables with heterogeneous filters and EF navigation properties,
    /// which would require many separate repository calls that would be slower and less readable.
    /// </summary>
    public LedgerService(ApplicationDbContext context, ILogger<LedgerService> logger)
    {
        _context = context;
        _logger = logger;
    }

    /// <summary>
    /// Assembles a full ledger report for an account over the given date range, including
    /// nine distinct transaction sources: customer payments deposited, expenses paid from,
    /// bill payments paid from, invoice revenue line items, sales tax, direct expense
    /// categorizations, bill line-item categorizations, AR movements, and AP movements.
    /// Returns <c>null</c> when the account does not exist or is filtered out by the global
    /// query filters (company isolation / soft delete).
    /// The <c>OpeningBalance</c> in the returned DTO represents the account balance on the day
    /// before <paramref name="from"/>, computed by <see cref="ComputePriorBalanceAsync"/>.
    /// </summary>
    public async Task<AccountLedgerDto?> GetAccountLedgerAsync(int accountId, DateTime from, DateTime to)
    {
        // Use FirstOrDefaultAsync so global query filters (company isolation, soft delete) apply
        var account = await _context.Accounts.FirstOrDefaultAsync(a => a.Id == accountId);
        if (account == null) return null;

        var fromDate = from.Date;
        var toDate = to.Date.AddDays(1).AddTicks(-1); // inclusive end of day

        var entries = new List<LedgerEntryDto>();

        // ── 1. Customer payments deposited INTO this account (DEBIT) ──────────
        // e.g. Checking/Savings account receives a customer payment
        var depositedPayments = await _context.Payments
            .Include(p => p.Invoice)
            .Where(p => p.DepositAccountId == accountId
                     && p.PaymentDate >= fromDate && p.PaymentDate <= toDate)
            .ToListAsync();

        foreach (var p in depositedPayments)
            entries.Add(new LedgerEntryDto
            {
                Date = p.PaymentDate,
                Reference = p.Invoice?.InvoiceNumber ?? $"PMT-{p.Id}",
                Source = "Customer Payment",
                Description = p.Notes ?? p.Reference,
                Debit = p.Amount,
                Credit = 0,
                LinkController = "Invoices",
                LinkId = p.InvoiceId
            });

        // Customer deposits recorded to this account (DEBIT — cash received at deposit time)
        var depositedDeposits = await _context.Deposits
            .Where(d => d.DepositAccountId == accountId
                     && d.ReceivedDate >= fromDate && d.ReceivedDate <= toDate)
            .ToListAsync();

        foreach (var d in depositedDeposits)
            entries.Add(new LedgerEntryDto
            {
                Date = d.ReceivedDate,
                Reference = d.ReceiptNumber,
                Source = "Customer Deposit",
                Description = d.Notes ?? d.Reference,
                Debit = d.Amount,
                Credit = 0,
                LinkController = "Jobs",
                LinkId = d.JobId
            });

        // Refunds paid FROM this account (CREDIT — cash leaves)
        var refundsPaidFrom = await _context.Refunds
            .Include(r => r.Invoice)
            .Where(r => r.DepositAccountId == accountId
                     && r.RefundDate >= fromDate && r.RefundDate <= toDate)
            .ToListAsync();

        foreach (var r in refundsPaidFrom)
            entries.Add(new LedgerEntryDto
            {
                Date = r.RefundDate,
                Reference = r.Reference ?? $"REF-{r.Id}",
                Source = "Refund",
                Description = r.Reason,
                Debit = 0,
                Credit = r.Amount,
                LinkController = "Invoices",
                LinkId = r.InvoiceId
            });

        // ── 2. Direct expenses paid FROM this account (CREDIT) ────────────────
        // e.g. Checking account used to pay an expense
        var expensesPaidFrom = await _context.Expenses
            .Include(e => e.Vendor)
            .Where(e => e.PaymentAccountId == accountId
                     && e.Date >= fromDate && e.Date <= toDate)
            .ToListAsync();

        foreach (var e in expensesPaidFrom)
            entries.Add(new LedgerEntryDto
            {
                Date = e.Date,
                Reference = e.ExpenseNumber,
                Source = "Expense",
                Description = e.Memo ?? e.Vendor?.CompanyName,
                Debit = 0,
                Credit = e.Amount,
                LinkController = "Expenses",
                LinkId = e.Id
            });

        // ── 3. Bill payments made FROM this account (CREDIT) ──────────────────
        // e.g. Checking account used to pay a vendor bill
        var billPaymentsPaidFrom = await _context.BillPayments
            .Include(bp => bp.Bill)
            .Where(bp => bp.BankAccountId == accountId
                      && bp.PaymentDate >= fromDate && bp.PaymentDate <= toDate)
            .ToListAsync();

        foreach (var bp in billPaymentsPaidFrom)
            entries.Add(new LedgerEntryDto
            {
                Date = bp.PaymentDate,
                Reference = bp.PaymentNumber,
                Source = "Bill Payment",
                Description = bp.Memo ?? bp.Bill?.BillNumber,
                Debit = 0,
                Credit = bp.Amount,
                LinkController = "Bills",
                LinkId = bp.BillId
            });

        // ── 4. Invoice line items that post revenue to this account (CREDIT) ──
        // e.g. Revenue account 4000 receives line-item revenue
        var revenueItems = await _context.InvoiceItems
            .Include(ii => ii.Invoice)
            .Where(ii => ii.RevenueAccountId == accountId
                      && ii.Invoice.Status != InvoiceStatus.Draft
                      && ii.Invoice.Status != InvoiceStatus.Voided
                      && ii.Invoice.InvoiceDate >= fromDate && ii.Invoice.InvoiceDate <= toDate)
            .ToListAsync();

        foreach (var item in revenueItems)
            entries.Add(new LedgerEntryDto
            {
                Date = item.Invoice.InvoiceDate,
                Reference = item.Invoice.InvoiceNumber,
                Source = "Invoice",
                Description = item.Description,
                Debit = 0,
                Credit = item.TotalPrice,
                LinkController = "Invoices",
                LinkId = item.InvoiceId
            });

        // ── 5. Sales tax collected to this account (CREDIT) ───────────────────
        // e.g. Liability account 2200 receives sales tax from invoices
        var taxInvoices = await _context.Invoices
            .Where(i => i.SalesTaxAccountId == accountId
                     && i.TaxAmount > 0
                     && i.Status != InvoiceStatus.Draft
                     && i.Status != InvoiceStatus.Voided
                     && i.InvoiceDate >= fromDate && i.InvoiceDate <= toDate)
            .ToListAsync();

        foreach (var inv in taxInvoices)
            entries.Add(new LedgerEntryDto
            {
                Date = inv.InvoiceDate,
                Reference = inv.InvoiceNumber,
                Source = "Sales Tax",
                Description = $"Tax on {inv.InvoiceNumber}",
                Debit = 0,
                Credit = inv.TaxAmount,
                LinkController = "Invoices",
                LinkId = inv.Id
            });

        // ── 5b. Cash refunds reverse the sale (DR Sales Returns 4960 + DR Sales Tax Payable) ──
        // The revenue portion debits Sales Returns; the tax portion debits the invoice's sales-tax
        // account (relieving the liability). Cash leaving the bank is handled in the bank section above.
        // Store-credit refunds are excluded — they post via CreditMemo, not the GL (see CancelRefund).
        if (account.AccountNumber == "4960" || account.AccountType == AccountType.Liability)
        {
            var saleReversingRefunds = await _context.Refunds
                .Include(r => r.Invoice)
                .Where(r => !r.IsDeleted && r.Invoice != null
                         && r.RefundMethod != PaymentMethod.StoreCredit
                         && r.RefundDate >= fromDate && r.RefundDate <= toDate)
                .ToListAsync();

            foreach (var r in saleReversingRefunds)
            {
                var (returnsPortion, taxPortion) = RefundAllocation.Split(r.Amount, r.Invoice.TaxAmount, r.Invoice.Total);

                if (account.AccountNumber == "4960" && returnsPortion != 0)
                    entries.Add(new LedgerEntryDto
                    {
                        Date = r.RefundDate, Reference = r.Reference ?? $"REF-{r.Id}", Source = "Refund",
                        Description = $"Sales return — {r.Invoice.InvoiceNumber}",
                        Debit = returnsPortion, Credit = 0,
                        LinkController = "Invoices", LinkId = r.InvoiceId
                    });

                if (r.Invoice.SalesTaxAccountId == accountId && taxPortion != 0)
                    entries.Add(new LedgerEntryDto
                    {
                        Date = r.RefundDate, Reference = r.Reference ?? $"REF-{r.Id}", Source = "Refund",
                        Description = $"Tax refunded — {r.Invoice.InvoiceNumber}",
                        Debit = taxPortion, Credit = 0,
                        LinkController = "Invoices", LinkId = r.InvoiceId
                    });
            }
        }

        // ── 6. Direct expenses categorized to this account (DEBIT) ────────────
        // e.g. Expense account 6200 receives direct expense entries
        var expensesTo = await _context.Expenses
            .Include(e => e.Vendor)
            .Where(e => e.ExpenseAccountId == accountId
                     && e.Date >= fromDate && e.Date <= toDate)
            .ToListAsync();

        foreach (var e in expensesTo)
            entries.Add(new LedgerEntryDto
            {
                Date = e.Date,
                Reference = e.ExpenseNumber,
                Source = "Expense",
                Description = e.Memo ?? e.Vendor?.CompanyName,
                Debit = e.Amount,
                Credit = 0,
                LinkController = "Expenses",
                LinkId = e.Id
            });

        // ── 7. Bill line items categorized to this account (DEBIT) ────────────
        // e.g. Expense/COGS account receives costs from vendor bill line items
        var billLineItems = await _context.BillLineItems
            .Include(bli => bli.Bill)
            .Where(bli => bli.AccountId == accountId
                       && bli.Bill.Status != BillStatus.Draft
                       && bli.Bill.Status != BillStatus.Voided
                       && bli.Bill.BillDate >= fromDate && bli.Bill.BillDate <= toDate)
            .ToListAsync();

        foreach (var bli in billLineItems)
            entries.Add(new LedgerEntryDto
            {
                Date = bli.Bill.BillDate,
                Reference = bli.Bill.BillNumber,
                Source = "Bill",
                Description = bli.Description,
                Debit = bli.Amount,
                Credit = 0,
                LinkController = "Bills",
                LinkId = bli.BillId
            });

        // ── 8. Accounts Receivable ─────────────────────────────────────────────
        // Invoice creation increases AR (DEBIT); customer payments reduce AR (CREDIT)
        if (account.AccountSubType == AccountSubType.AccountsReceivable)
        {
            var arInvoices = await _context.Invoices
                .Include(i => i.Customer)
                .Where(i => i.Status != InvoiceStatus.Draft
                         && i.Status != InvoiceStatus.Voided
                         && i.InvoiceDate >= fromDate && i.InvoiceDate <= toDate)
                .ToListAsync();

            foreach (var inv in arInvoices)
            {
                var customerName = inv.Customer?.IsCommercial == true
                    ? inv.Customer.CompanyName
                    : $"{inv.Customer?.ContactFirstName} {inv.Customer?.ContactLastName}".Trim();

                entries.Add(new LedgerEntryDto
                {
                    Date = inv.InvoiceDate,
                    Reference = inv.InvoiceNumber,
                    Source = "Invoice",
                    Description = customerName,
                    Debit = inv.Total,
                    Credit = 0,
                    LinkController = "Invoices",
                    LinkId = inv.Id
                });
            }

            var arPayments = await _context.Payments
                .Include(p => p.Invoice)
                .Where(p => p.PaymentDate >= fromDate && p.PaymentDate <= toDate)
                .ToListAsync();

            foreach (var p in arPayments)
                entries.Add(new LedgerEntryDto
                {
                    Date = p.PaymentDate,
                    Reference = p.Invoice?.InvoiceNumber ?? $"PMT-{p.Id}",
                    Source = "Invoice Payment",
                    Description = $"Payment — {p.Invoice?.InvoiceNumber}",
                    Debit = 0,
                    Credit = p.Amount,
                    LinkController = "Invoices",
                    LinkId = p.InvoiceId
                });

            // Credit memo applications reduce open AR (CREDIT)
            var arCreditMemos = await _context.CreditMemoApplications
                .Include(a => a.Invoice)
                .Include(a => a.CreditMemo)
                .Where(a => a.AppliedDate >= fromDate && a.AppliedDate <= toDate
                         && a.Invoice.Status != InvoiceStatus.Voided)
                .ToListAsync();

            foreach (var cm in arCreditMemos)
                entries.Add(new LedgerEntryDto
                {
                    Date = cm.AppliedDate,
                    Reference = cm.CreditMemo?.MemoNumber ?? $"CM-{cm.Id}",
                    Source = "Credit Memo",
                    Description = $"Credit applied to {cm.Invoice?.InvoiceNumber}",
                    Debit = 0,
                    Credit = cm.AmountApplied,
                    LinkController = "Invoices",
                    LinkId = cm.InvoiceId
                });

            // Gift-certificate redemptions reduce open AR (CREDIT) — ApplyGiftCertificate posts DR 2500 / CR AR.
            var arGcRedemptions = await _context.GiftCertificateRedemptions
                .Include(r => r.Invoice)
                .Include(r => r.GiftCertificate)
                .Where(r => !r.IsDeleted && r.RedeemedDate >= fromDate && r.RedeemedDate <= toDate
                         && r.Invoice.Status != InvoiceStatus.Voided)
                .ToListAsync();

            foreach (var r in arGcRedemptions)
                entries.Add(new LedgerEntryDto
                {
                    Date = r.RedeemedDate,
                    Reference = r.GiftCertificate?.CertificateCode ?? $"GC-{r.GiftCertificateId}",
                    Source = "Gift Certificate",
                    Description = $"GC redeemed on {r.Invoice?.InvoiceNumber}",
                    Debit = 0,
                    Credit = r.AmountRedeemed,
                    LinkController = "Invoices",
                    LinkId = r.InvoiceId
                });

            // NOTE: cash refunds no longer touch AR. Under the "reverse the sale" model they debit
            // Sales Returns + Sales Tax Payable and credit the bank (see section 5b above).
        }

        // ── 9. Accounts Payable ────────────────────────────────────────────────
        // Bill creation increases AP (CREDIT); bill payments reduce AP (DEBIT)
        if (account.AccountSubType == AccountSubType.AccountsPayable)
        {
            var apBills = await _context.Bills
                .Include(b => b.Vendor)
                .Where(b => b.APAccountId == accountId
                         && b.Status != BillStatus.Draft
                         && b.Status != BillStatus.Voided
                         && b.BillDate >= fromDate && b.BillDate <= toDate)
                .ToListAsync();

            foreach (var b in apBills)
                entries.Add(new LedgerEntryDto
                {
                    Date = b.BillDate,
                    Reference = b.BillNumber,
                    Source = "Bill",
                    Description = b.Vendor?.CompanyName ?? b.Memo,
                    Debit = 0,
                    Credit = b.Total,
                    LinkController = "Bills",
                    LinkId = b.Id
                });

            var apPayments = await _context.BillPayments
                .Include(bp => bp.Bill)
                .Where(bp => bp.Bill.APAccountId == accountId
                          && bp.PaymentDate >= fromDate && bp.PaymentDate <= toDate)
                .ToListAsync();

            foreach (var bp in apPayments)
                entries.Add(new LedgerEntryDto
                {
                    Date = bp.PaymentDate,
                    Reference = bp.PaymentNumber,
                    Source = "Bill Payment",
                    Description = bp.Memo ?? bp.Bill?.BillNumber,
                    Debit = bp.Amount,
                    Credit = 0,
                    LinkController = "Bills",
                    LinkId = bp.BillId
                });

            // Vendor credit applications reduce AP (DEBIT — offset against what we owe)
            var apVendorCredits = await _context.VendorCreditApplications
                .Include(vca => vca.VendorCredit)
                .Include(vca => vca.Bill)
                .Where(vca => vca.VendorCredit.APAccountId == accountId
                           && vca.AppliedDate >= fromDate && vca.AppliedDate <= toDate)
                .ToListAsync();

            foreach (var vca in apVendorCredits)
                entries.Add(new LedgerEntryDto
                {
                    Date = vca.AppliedDate,
                    Reference = vca.VendorCredit?.CreditNumber ?? $"VC-{vca.VendorCreditId}",
                    Source = "Vendor Credit",
                    Description = $"Credit applied to {vca.Bill?.BillNumber}",
                    Debit = vca.Amount,
                    Credit = 0,
                    LinkController = "VendorCredits",
                    LinkId = vca.VendorCreditId
                });
        }

        // ── 11. Gift Certificate Liability (account 2500) ─────────────────────
        // CR when GC is issued; DR when redeemed or voided with remaining balance.
        if (account.AccountNumber == "2500")
        {
            var gcIssued = await _context.GiftCertificates
                .Where(gc => !gc.IsDeleted && gc.IssueDate >= fromDate && gc.IssueDate <= toDate)
                .ToListAsync();
            foreach (var gc in gcIssued)
                entries.Add(new LedgerEntryDto
                {
                    Date = gc.IssueDate, Reference = gc.CertificateCode,
                    Source = "Gift Certificate", Description = "GC issued",
                    Debit = 0, Credit = gc.OriginalAmount,
                    LinkController = "GiftCertificates", LinkId = gc.Id
                });

            var gcRedemptions = await _context.GiftCertificateRedemptions
                .Include(r => r.GiftCertificate)
                .Where(r => !r.IsDeleted && r.RedeemedDate >= fromDate && r.RedeemedDate <= toDate)
                .ToListAsync();
            foreach (var r in gcRedemptions)
                entries.Add(new LedgerEntryDto
                {
                    Date = r.RedeemedDate, Reference = r.GiftCertificate?.CertificateCode ?? $"GC-{r.GiftCertificateId}",
                    Source = "GC Redemption", Description = "GC applied to invoice",
                    Debit = r.AmountRedeemed, Credit = 0,
                    LinkController = "GiftCertificates", LinkId = r.GiftCertificateId
                });

            var gcVoided = await _context.GiftCertificates
                .Where(gc => !gc.IsDeleted && gc.Status == GiftCertificateStatus.Voided
                          && gc.UpdatedAt >= fromDate && gc.UpdatedAt <= toDate
                          && gc.OriginalAmount > gc.RedeemedAmount)
                .ToListAsync();
            foreach (var gc in gcVoided)
                entries.Add(new LedgerEntryDto
                {
                    Date = gc.UpdatedAt.GetValueOrDefault(), Reference = gc.CertificateCode,
                    Source = "GC Voided", Description = "Breakage income",
                    Debit = gc.OriginalAmount - gc.RedeemedAmount, Credit = 0,
                    LinkController = "GiftCertificates", LinkId = gc.Id
                });
        }

        // ── 12. Customer Deposits liability (account 2300) ────────────────────
        // CR when deposit is recorded; DR when deposit is applied to an invoice.
        if (account.AccountNumber == "2300")
        {
            var depositsRecorded = await _context.Deposits
                .Where(d => !d.IsDeleted && d.ReceivedDate >= fromDate && d.ReceivedDate <= toDate)
                .ToListAsync();
            foreach (var d in depositsRecorded)
                entries.Add(new LedgerEntryDto
                {
                    Date = d.ReceivedDate, Reference = d.ReceiptNumber,
                    Source = "Customer Deposit", Description = d.Notes ?? d.Reference,
                    Debit = 0, Credit = d.Amount,
                    LinkController = "Jobs", LinkId = d.JobId
                });

            var depositsApplied = await _context.Deposits
                .Include(d => d.AppliedToInvoice)
                .Where(d => !d.IsDeleted && d.AppliedToInvoiceId != null
                         && d.AppliedDate >= fromDate && d.AppliedDate <= toDate)
                .ToListAsync();
            foreach (var d in depositsApplied)
                entries.Add(new LedgerEntryDto
                {
                    Date = d.AppliedDate!.Value, Reference = d.AppliedToInvoice?.InvoiceNumber ?? d.ReceiptNumber,
                    Source = "Deposit Applied", Description = $"Deposit {d.ReceiptNumber} applied to invoice",
                    Debit = d.Amount, Credit = 0,
                    LinkController = "Invoices", LinkId = d.AppliedToInvoiceId
                });
        }

        // ── 12b. Customer Credits liability (account 2350) ────────────────────
        // CR when a credit memo (incl. store-credit refund) is issued; DR when applied to an invoice.
        // Voided memos are excluded (their issue/void net to zero).
        if (account.AccountNumber == "2350")
        {
            var memosIssued = await _context.CreditMemos
                .Where(m => m.Status != CreditMemoStatus.Voided
                         && m.IssueDate >= fromDate && m.IssueDate <= toDate)
                .ToListAsync();
            foreach (var m in memosIssued)
                entries.Add(new LedgerEntryDto
                {
                    Date = m.IssueDate, Reference = m.MemoNumber,
                    Source = "Credit Memo", Description = "Store credit issued",
                    Debit = 0, Credit = m.Amount,
                    LinkController = "CreditMemos", LinkId = m.Id
                });

            var memosApplied = await _context.CreditMemoApplications
                .Include(a => a.CreditMemo).Include(a => a.Invoice)
                .Where(a => a.CreditMemo.Status != CreditMemoStatus.Voided
                         && a.AppliedDate >= fromDate && a.AppliedDate <= toDate)
                .ToListAsync();
            foreach (var a in memosApplied)
                entries.Add(new LedgerEntryDto
                {
                    Date = a.AppliedDate, Reference = a.CreditMemo?.MemoNumber ?? $"CM-{a.CreditMemoId}",
                    Source = "Credit Applied", Description = $"Applied to {a.Invoice?.InvoiceNumber}",
                    Debit = a.AmountApplied, Credit = 0,
                    LinkController = "Invoices", LinkId = a.InvoiceId
                });
        }

        // ── 12c. Sales Discounts contra-revenue (account 4950) ────────────────
        // Mirrors the actual postings made by AccountBalanceService so a balance recompute reproduces
        // the stored CurrentBalance (otherwise "Recalculate Balances" would wipe 4950 down to JE-only):
        //   • Invoice discounts        → DR 4950 at invoice date (InvoicesController invoice create/edit).
        //   • Credit memo issuance     → DR 4950 = full memo amount at issue (CreditMemosController.Create
        //     and the store-credit refund path, which both create a CreditMemo row).
        //   • Credit memo void         → CR 4950 = unapplied remainder at void (reverses the unused part).
        // Keep this in step with FinancialReportService's 4950 computation (discountsByAcct + cmContraRevenue).
        if (account.AccountNumber == "4950")
        {
            var discountInvoices = await _context.Invoices
                .Where(i => i.DiscountAmount > 0
                         && i.Status != InvoiceStatus.Draft && i.Status != InvoiceStatus.Voided
                         && i.InvoiceDate >= fromDate && i.InvoiceDate <= toDate)
                .ToListAsync();
            foreach (var inv in discountInvoices)
                entries.Add(new LedgerEntryDto
                {
                    Date = inv.InvoiceDate, Reference = inv.InvoiceNumber,
                    Source = "Invoice", Description = $"Discount on {inv.InvoiceNumber}",
                    Debit = inv.DiscountAmount, Credit = 0,
                    LinkController = "Invoices", LinkId = inv.Id
                });

            var discountMemosIssued = await _context.CreditMemos
                .Where(m => m.IssueDate >= fromDate && m.IssueDate <= toDate)
                .ToListAsync();
            foreach (var m in discountMemosIssued)
                entries.Add(new LedgerEntryDto
                {
                    Date = m.IssueDate, Reference = m.MemoNumber,
                    Source = "Credit Memo", Description = "Store credit issued (contra-revenue)",
                    Debit = m.Amount, Credit = 0,
                    LinkController = "CreditMemos", LinkId = m.Id
                });

            var discountMemosVoided = await _context.CreditMemos
                .Where(m => m.Status == CreditMemoStatus.Voided
                         && m.UpdatedAt >= fromDate && m.UpdatedAt <= toDate
                         && m.Amount > m.AmountApplied)
                .ToListAsync();
            foreach (var m in discountMemosVoided)
                entries.Add(new LedgerEntryDto
                {
                    Date = m.UpdatedAt.GetValueOrDefault(), Reference = m.MemoNumber,
                    Source = "Credit Memo Voided", Description = "Reversed unapplied store credit",
                    Debit = 0, Credit = m.Amount - m.AmountApplied,
                    LinkController = "CreditMemos", LinkId = m.Id
                });
        }

        // ── 12d. Inventory consumption COGS (DR COGS / CR Inventory) ──────────
        // When an item with both a COGS and an Inventory account is consumed (JobUsage/Waste — the only
        // two transaction types created at the COGS-posting sites), JobsController/InventoryController post
        // DR COGS / CR Inventory at the transaction's TotalCost. Reproduce it here so a balance recompute
        // matches the posting and the trial balance stays balanced. TotalCost is stored positive.
        if (account.AccountType == AccountType.CostOfGoods || account.AccountSubType == AccountSubType.Inventory)
        {
            var consumption = await _context.InventoryTransactions
                .Include(t => t.InventoryItem)
                .Where(t => (t.TransactionType == InventoryTransactionType.JobUsage || t.TransactionType == InventoryTransactionType.Waste)
                         && t.InventoryItem.CogsAccountId != null && t.InventoryItem.InventoryAccountId != null
                         && (t.InventoryItem.CogsAccountId == accountId || t.InventoryItem.InventoryAccountId == accountId)
                         && t.TransactionDate >= fromDate && t.TransactionDate <= toDate)
                .ToListAsync();

            foreach (var t in consumption)
            {
                var amount = Math.Abs(t.TotalCost);
                if (t.InventoryItem.CogsAccountId == accountId)
                    entries.Add(new LedgerEntryDto
                    {
                        Date = t.TransactionDate, Reference = t.Reference ?? $"INV-{t.Id}",
                        Source = "Inventory Usage", Description = $"COGS — {t.InventoryItem.Name}",
                        Debit = amount, Credit = 0, LinkController = "Inventory", LinkId = t.InventoryItemId
                    });
                if (t.InventoryItem.InventoryAccountId == accountId)
                    entries.Add(new LedgerEntryDto
                    {
                        Date = t.TransactionDate, Reference = t.Reference ?? $"INV-{t.Id}",
                        Source = "Inventory Usage", Description = $"Inventory relieved — {t.InventoryItem.Name}",
                        Debit = 0, Credit = amount, LinkController = "Inventory", LinkId = t.InventoryItemId
                    });
            }
        }

        // ── 10. Journal Entry lines touching this account ──────────────────
        var jeLines = await _context.JournalEntryLines
            .Include(l => l.JournalEntry)
            .Where(l => l.AccountId == accountId
                     && l.JournalEntry.Status == JournalEntryStatus.Posted
                     && l.JournalEntry.EntryDate >= fromDate
                     && l.JournalEntry.EntryDate <= toDate)
            .ToListAsync();

        foreach (var line in jeLines)
            entries.Add(new LedgerEntryDto
            {
                Date = line.JournalEntry.EntryDate,
                Reference = line.JournalEntry.EntryNumber,
                Source = "Journal Entry",
                Description = line.Description ?? line.JournalEntry.Description,
                Debit = line.DebitAmount,
                Credit = line.CreditAmount,
                LinkController = "JournalEntries",
                LinkId = line.JournalEntry.Id
            });

        // ── Sort and compute running balance ──────────────────────────────────
        entries = entries
            .OrderBy(e => e.Date)
            .ThenBy(e => e.Reference)
            .ToList();

        // Derive normal-debit-balance flag from AccountSubType (more authoritative than AccountType,
        // since users could misconfigure AccountType while SubType is picked from a constrained list).
        bool normalDebitBalance = AccountingRules.IsNormalDebitBalance(account.AccountSubType);

        // Compute the balance before the selected period
        decimal priorBalance = await ComputePriorBalanceAsync(account, fromDate, to.Date, normalDebitBalance);


        decimal runningBalance = priorBalance;
        foreach (var entry in entries)
        {
            runningBalance += normalDebitBalance
                ? entry.Debit - entry.Credit
                : entry.Credit - entry.Debit;
            entry.RunningBalance = runningBalance;
        }

        return new AccountLedgerDto
        {
            Id = account.Id,
            AccountNumber = account.AccountNumber,
            Name = account.Name,
            AccountType = account.AccountType,
            AccountSubType = account.AccountSubType,
            From = fromDate,
            To = to.Date,
            OpeningBalance = priorBalance,
            PeriodDebits = entries.Sum(e => e.Debit),
            PeriodCredits = entries.Sum(e => e.Credit),
            ClosingBalance = runningBalance,
            Entries = entries
        };
    }

    /// <summary>
    /// Computes the account balance on the day immediately before <paramref name="beforeDate"/>
    /// by summing all activity prior to that date across every transaction source and adding
    /// the stored <c>OpeningBalance</c>. The opening balance is only included when its recorded
    /// date is on or before <paramref name="periodEnd"/> — a future-dated opening balance (e.g.
    /// from a mid-year chart-of-accounts migration) should not pollute earlier period reports.
    /// A null <c>OpeningBalanceDate</c> means the balance predates all transactions and always applies.
    /// The sign convention follows <see cref="AccountingRules.IsNormalDebitBalance"/>: debits increase debit-normal
    /// accounts and credits increase credit-normal accounts.
    /// </summary>
    private async Task<decimal> ComputePriorBalanceAsync(
        PowderCoating.Core.Entities.Account account, DateTime beforeDate, DateTime periodEnd, bool normalDebitBalance)
    {
        var accountId = account.Id;
        decimal debits = 0;
        decimal credits = 0;

        // 1. Customer payments deposited INTO this account (DEBIT)
        debits += await _context.Payments
            .Where(p => p.DepositAccountId == accountId && p.PaymentDate < beforeDate)
            .SumAsync(p => (decimal?)p.Amount) ?? 0;

        // Customer deposits recorded to this account (DEBIT — cash received at deposit time)
        debits += await _context.Deposits
            .Where(d => !d.IsDeleted && d.DepositAccountId == accountId && d.ReceivedDate < beforeDate)
            .SumAsync(d => (decimal?)d.Amount) ?? 0;

        // Refunds paid FROM this account (CREDIT — cash leaves)
        credits += await _context.Refunds
            .Where(r => !r.IsDeleted && r.DepositAccountId == accountId && r.RefundDate < beforeDate)
            .SumAsync(r => (decimal?)r.Amount) ?? 0;

        // 2. Direct expenses paid FROM this account (CREDIT)
        credits += await _context.Expenses
            .Where(e => e.PaymentAccountId == accountId && e.Date < beforeDate)
            .SumAsync(e => (decimal?)e.Amount) ?? 0;

        // 3. Bill payments made FROM this account (CREDIT)
        credits += await _context.BillPayments
            .Where(bp => bp.BankAccountId == accountId && bp.PaymentDate < beforeDate)
            .SumAsync(bp => (decimal?)bp.Amount) ?? 0;

        // 4. Invoice line items posting revenue to this account (CREDIT)
        credits += await _context.InvoiceItems
            .Where(ii => ii.RevenueAccountId == accountId
                      && ii.Invoice.Status != InvoiceStatus.Draft
                      && ii.Invoice.Status != InvoiceStatus.Voided
                      && ii.Invoice.InvoiceDate < beforeDate)
            .SumAsync(ii => (decimal?)ii.TotalPrice) ?? 0;

        // 5. Sales tax collected to this account (CREDIT)
        credits += await _context.Invoices
            .Where(i => i.SalesTaxAccountId == accountId
                     && i.TaxAmount > 0
                     && i.Status != InvoiceStatus.Draft
                     && i.Status != InvoiceStatus.Voided
                     && i.InvoiceDate < beforeDate)
            .SumAsync(i => (decimal?)i.TaxAmount) ?? 0;

        // 5b. Cash refunds reverse the sale (DR Sales Returns 4960 + DR Sales Tax Payable). Store-credit
        // refunds are excluded (no GL posting). Mirrors section 5b in GetAccountLedgerAsync.
        if (account.AccountNumber == "4960" || account.AccountType == AccountType.Liability)
        {
            var priorRefunds = await _context.Refunds
                .Include(r => r.Invoice)
                .Where(r => !r.IsDeleted && r.Invoice != null
                         && r.RefundMethod != PaymentMethod.StoreCredit
                         && r.RefundDate < beforeDate)
                .ToListAsync();

            foreach (var r in priorRefunds)
            {
                var (returnsPortion, taxPortion) = RefundAllocation.Split(r.Amount, r.Invoice.TaxAmount, r.Invoice.Total);
                if (account.AccountNumber == "4960")
                    debits += returnsPortion;
                if (r.Invoice.SalesTaxAccountId == accountId)
                    debits += taxPortion;
            }
        }

        // 6. Direct expenses categorized to this account (DEBIT)
        debits += await _context.Expenses
            .Where(e => e.ExpenseAccountId == accountId && e.Date < beforeDate)
            .SumAsync(e => (decimal?)e.Amount) ?? 0;

        // 7. Bill line items categorized to this account (DEBIT)
        debits += await _context.BillLineItems
            .Where(bli => bli.AccountId == accountId
                       && bli.Bill.Status != BillStatus.Draft
                       && bli.Bill.Status != BillStatus.Voided
                       && bli.Bill.BillDate < beforeDate)
            .SumAsync(bli => (decimal?)bli.Amount) ?? 0;

        // 8. Accounts Receivable
        if (account.AccountSubType == AccountSubType.AccountsReceivable)
        {
            debits += await _context.Invoices
                .Where(i => i.Status != InvoiceStatus.Draft
                         && i.Status != InvoiceStatus.Voided
                         && i.InvoiceDate < beforeDate)
                .SumAsync(i => (decimal?)i.Total) ?? 0;

            credits += await _context.Payments
                .Where(p => p.PaymentDate < beforeDate)
                .SumAsync(p => (decimal?)p.Amount) ?? 0;

            credits += await _context.CreditMemoApplications
                .Where(a => a.AppliedDate < beforeDate && a.Invoice.Status != InvoiceStatus.Voided)
                .SumAsync(a => (decimal?)a.AmountApplied) ?? 0;

            // Gift-certificate redemptions credit AR (DR 2500 / CR AR), same as in GetAccountLedgerAsync.
            credits += await _context.GiftCertificateRedemptions
                .Where(r => !r.IsDeleted && r.RedeemedDate < beforeDate && r.Invoice.Status != InvoiceStatus.Voided)
                .SumAsync(r => (decimal?)r.AmountRedeemed) ?? 0;

            // NOTE: cash refunds no longer debit AR — they reverse the sale (Sales Returns + Sales Tax),
            // handled in section 5b above.
        }

        // 9. Accounts Payable
        if (account.AccountSubType == AccountSubType.AccountsPayable)
        {
            credits += await _context.Bills
                .Where(b => b.APAccountId == accountId
                         && b.Status != BillStatus.Draft
                         && b.Status != BillStatus.Voided
                         && b.BillDate < beforeDate)
                .SumAsync(b => (decimal?)b.Total) ?? 0;

            debits += await _context.BillPayments
                .Where(bp => bp.Bill.APAccountId == accountId && bp.PaymentDate < beforeDate)
                .SumAsync(bp => (decimal?)bp.Amount) ?? 0;

            debits += await _context.VendorCreditApplications
                .Where(vca => vca.VendorCredit.APAccountId == accountId && vca.AppliedDate < beforeDate)
                .SumAsync(vca => (decimal?)vca.Amount) ?? 0;
        }

        // 11. GC Liability (account 2500)
        if (account.AccountNumber == "2500")
        {
            credits += await _context.GiftCertificates
                .Where(gc => !gc.IsDeleted && gc.IssueDate < beforeDate)
                .SumAsync(gc => (decimal?)gc.OriginalAmount) ?? 0;
            debits += await _context.GiftCertificateRedemptions
                .Where(r => !r.IsDeleted && r.RedeemedDate < beforeDate)
                .SumAsync(r => (decimal?)r.AmountRedeemed) ?? 0;
            debits += await _context.GiftCertificates
                .Where(gc => !gc.IsDeleted && gc.Status == GiftCertificateStatus.Voided
                          && gc.UpdatedAt < beforeDate && gc.OriginalAmount > gc.RedeemedAmount)
                .SumAsync(gc => (decimal?)(gc.OriginalAmount - gc.RedeemedAmount)) ?? 0;
        }

        // 12. Customer Deposits liability (account 2300)
        if (account.AccountNumber == "2300")
        {
            credits += await _context.Deposits
                .Where(d => !d.IsDeleted && d.ReceivedDate < beforeDate)
                .SumAsync(d => (decimal?)d.Amount) ?? 0;
            debits += await _context.Deposits
                .Where(d => !d.IsDeleted && d.AppliedToInvoiceId != null && d.AppliedDate < beforeDate)
                .SumAsync(d => (decimal?)d.Amount) ?? 0;
        }

        // 12b. Customer Credits liability (account 2350)
        if (account.AccountNumber == "2350")
        {
            credits += await _context.CreditMemos
                .Where(m => m.Status != CreditMemoStatus.Voided && m.IssueDate < beforeDate)
                .SumAsync(m => (decimal?)m.Amount) ?? 0;
            debits += await _context.CreditMemoApplications
                .Where(a => a.CreditMemo.Status != CreditMemoStatus.Voided && a.AppliedDate < beforeDate)
                .SumAsync(a => (decimal?)a.AmountApplied) ?? 0;
        }

        // 12c. Sales Discounts contra-revenue (account 4950). Mirrors section 12c in GetAccountLedgerAsync
        // so the prior-period opening balance matches the actual postings (invoice discounts + memo issues,
        // less the unapplied remainder of voided memos).
        if (account.AccountNumber == "4950")
        {
            debits += await _context.Invoices
                .Where(i => i.DiscountAmount > 0
                         && i.Status != InvoiceStatus.Draft && i.Status != InvoiceStatus.Voided
                         && i.InvoiceDate < beforeDate)
                .SumAsync(i => (decimal?)i.DiscountAmount) ?? 0;
            debits += await _context.CreditMemos
                .Where(m => m.IssueDate < beforeDate)
                .SumAsync(m => (decimal?)m.Amount) ?? 0;
            credits += await _context.CreditMemos
                .Where(m => m.Status == CreditMemoStatus.Voided && m.UpdatedAt < beforeDate && m.Amount > m.AmountApplied)
                .SumAsync(m => (decimal?)(m.Amount - m.AmountApplied)) ?? 0;
        }

        // 12d. Inventory consumption COGS (DR COGS / CR Inventory). Mirrors section 12d in
        // GetAccountLedgerAsync so the prior-period opening balance matches the posting.
        if (account.AccountType == AccountType.CostOfGoods || account.AccountSubType == AccountSubType.Inventory)
        {
            var priorConsumption = await _context.InventoryTransactions
                .Include(t => t.InventoryItem)
                .Where(t => (t.TransactionType == InventoryTransactionType.JobUsage || t.TransactionType == InventoryTransactionType.Waste)
                         && t.InventoryItem.CogsAccountId != null && t.InventoryItem.InventoryAccountId != null
                         && (t.InventoryItem.CogsAccountId == accountId || t.InventoryItem.InventoryAccountId == accountId)
                         && t.TransactionDate < beforeDate)
                .ToListAsync();

            foreach (var t in priorConsumption)
            {
                var amount = Math.Abs(t.TotalCost);
                if (t.InventoryItem.CogsAccountId == accountId) debits += amount;
                if (t.InventoryItem.InventoryAccountId == accountId) credits += amount;
            }
        }

        // 10. Posted journal entry lines touching this account (prior to period)
        debits += await _context.JournalEntryLines
            .Where(l => l.AccountId == accountId
                     && l.JournalEntry.Status == JournalEntryStatus.Posted
                     && l.JournalEntry.EntryDate < beforeDate)
            .SumAsync(l => (decimal?)l.DebitAmount) ?? 0;

        credits += await _context.JournalEntryLines
            .Where(l => l.AccountId == accountId
                     && l.JournalEntry.Status == JournalEntryStatus.Posted
                     && l.JournalEntry.EntryDate < beforeDate)
            .SumAsync(l => (decimal?)l.CreditAmount) ?? 0;

        decimal netActivity = normalDebitBalance ? debits - credits : credits - debits;

        // Apply the opening balance if it was established on or before the end of the viewed period.
        // A null date means it predates all transactions and always applies.
        decimal openingBalance = (account.OpeningBalanceDate == null || account.OpeningBalanceDate.Value.Date <= periodEnd)
            ? account.OpeningBalance
            : 0;

        return openingBalance + netActivity;
    }
}
