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

namespace PowderCoating.Infrastructure.Services;

/// <summary>
/// Implements financial aggregate reports (P&amp;L, Balance Sheet, AR Aging, Sales &amp; Income)
/// using direct DbContext access with AsNoTracking. Migrated from inline queries in
/// ReportsController as part of Phase 2 of the data-access architecture migration.
/// The four report types each have matching PDF export paths in the controller that
/// share the same data by calling these methods, eliminating the previous duplication.
/// See <c>docs/DATA_ACCESS_ARCHITECTURE.md</c> for the full migration plan.
/// </summary>
public class FinancialReportService : IFinancialReportService
{
    private readonly ApplicationDbContext _context;
    private readonly ILedgerService _ledger;

    public FinancialReportService(ApplicationDbContext context, ILedgerService ledger)
    {
        _context = context;
        _ledger = ledger;
    }

    /// <inheritdoc/>
    public async Task<BalanceReconciliationDto> GetBalanceReconciliationAsync(int companyId)
    {
        var companyName = await GetCompanyNameAsync(companyId);

        var accounts = await _context.Accounts
            .Where(a => a.CompanyId == companyId && a.IsActive && !a.IsDeleted)
            .OrderBy(a => a.AccountNumber)
            .ToListAsync();

        // Epoch start so LedgerService treats OpeningBalance as prior and all activity falls in-window —
        // identical to how AccountBalanceService.RecalculateAllAsync derives the authoritative balance.
        var epoch = new DateTime(2000, 1, 1);
        var now = DateTime.UtcNow;

        var lines = new List<BalanceReconciliationLine>();
        decimal arControl = 0m, apControl = 0m;
        foreach (var a in accounts)
        {
            var ledger = await _ledger.GetAccountLedgerAsync(a.Id, epoch, now);
            var ledgerBalance = ledger?.ClosingBalance ?? 0m;
            lines.Add(new BalanceReconciliationLine
            {
                AccountId = a.Id,
                AccountNumber = a.AccountNumber,
                AccountName = a.Name,
                AccountType = a.AccountType,
                StoredBalance = a.CurrentBalance,
                LedgerBalance = ledgerBalance
            });
            if (a.AccountSubType == AccountSubType.AccountsReceivable) arControl += ledgerBalance;
            if (a.AccountSubType == AccountSubType.AccountsPayable) apControl += ledgerBalance;
        }

        var arSubledger = await _context.Customers
            .Where(c => c.CompanyId == companyId && !c.IsDeleted)
            .SumAsync(c => (decimal?)c.CurrentBalance) ?? 0m;
        var apSubledger = await _context.Vendors
            .Where(v => v.CompanyId == companyId && !v.IsDeleted)
            .SumAsync(v => (decimal?)v.CurrentBalance) ?? 0m;

        return new BalanceReconciliationDto
        {
            AsOf = now,
            CompanyName = companyName,
            AccountLines = lines,
            ArControlBalance = arControl,
            ArSubledgerTotal = arSubledger,
            ApControlBalance = apControl,
            ApSubledgerTotal = apSubledger
        };
    }

    /// <inheritdoc/>
    public async Task<ProfitAndLossDto> GetProfitAndLossAsync(int companyId, DateTime from, DateTime to, AccountingMethod? method = null)
    {
        var toEnd = to.AddDays(1).AddTicks(-1);
        var companyName = await GetCompanyNameAsync(companyId);
        var accountingMethod = method ?? await GetCompanyAccountingMethodAsync(companyId);
        var isCash = accountingMethod == AccountingMethod.Cash;

        var revenueAccounts = await _context.Accounts
            .Where(a => a.CompanyId == companyId && a.AccountType == AccountType.Revenue && a.IsActive)
            .ToDictionaryAsync(a => a.Id);

        var revenueLines = new List<FinancialReportLine>();

        if (isCash)
        {
            // Cash basis: total payments received in period (not split by revenue account)
            var cashRevenue = await _context.Payments
                .Where(p => p.CompanyId == companyId && p.PaymentDate >= from && p.PaymentDate <= toEnd
                         && p.Invoice.Status != InvoiceStatus.Voided)
                .SumAsync(p => (decimal?)p.Amount) ?? 0;
            if (cashRevenue > 0)
                revenueLines.Add(new FinancialReportLine { AccountNumber = "—", AccountName = "Cash Receipts", Amount = cashRevenue });

            // Cash refunds are cash paid back out — they reduce cash-basis revenue.
            var cashRefunds = await _context.Refunds
                .Where(r => r.CompanyId == companyId && !r.IsDeleted && r.RefundMethod != PaymentMethod.StoreCredit
                         && r.RefundDate >= from && r.RefundDate <= toEnd)
                .SumAsync(r => (decimal?)r.Amount) ?? 0m;
            if (cashRefunds > 0)
                revenueLines.Add(new FinancialReportLine { AccountNumber = "4960", AccountName = "Less: Refunds Paid", Amount = -cashRefunds });
        }
        else
        {
            // Accrual basis: revenue = invoice item amounts by invoice date
            var accrualRevenue = await _context.InvoiceItems
                .Where(ii => ii.CompanyId == companyId
                          && ii.RevenueAccountId != null
                          && ii.Invoice.Status != InvoiceStatus.Draft
                          && ii.Invoice.Status != InvoiceStatus.Voided
                          && ii.Invoice.InvoiceDate >= from && ii.Invoice.InvoiceDate <= toEnd)
                .GroupBy(ii => ii.RevenueAccountId!.Value)
                .Select(g => new { AccountId = g.Key, Amount = g.Sum(ii => ii.TotalPrice) })
                .ToListAsync();

            revenueLines.AddRange(accrualRevenue
                .Where(r => revenueAccounts.ContainsKey(r.AccountId))
                .Select(r => new FinancialReportLine
                {
                    AccountId     = r.AccountId,
                    AccountNumber = revenueAccounts[r.AccountId].AccountNumber,
                    AccountName   = revenueAccounts[r.AccountId].Name,
                    Amount        = r.Amount
                })
                .OrderBy(l => l.AccountNumber));

            var unlinkedRevenue = await _context.InvoiceItems
                .Where(ii => ii.CompanyId == companyId
                          && ii.RevenueAccountId == null
                          && ii.Invoice.Status != InvoiceStatus.Draft
                          && ii.Invoice.Status != InvoiceStatus.Voided
                          && ii.Invoice.InvoiceDate >= from && ii.Invoice.InvoiceDate <= toEnd)
                .SumAsync(ii => (decimal?)ii.TotalPrice) ?? 0;
            if (unlinkedRevenue > 0)
                revenueLines.Add(new FinancialReportLine { AccountNumber = "—", AccountName = "Other Sales (unclassified)", Amount = unlinkedRevenue });

            // Contra-revenue: discounts granted and credit memos applied reduce gross revenue.
            var periodDiscounts = await _context.Invoices
                .Where(i => i.CompanyId == companyId && i.Status != InvoiceStatus.Draft && i.Status != InvoiceStatus.Voided
                         && i.DiscountAmount > 0 && i.InvoiceDate >= from && i.InvoiceDate <= toEnd)
                .SumAsync(i => (decimal?)i.DiscountAmount) ?? 0m;
            // Credit-memo contra-revenue is recognized at issue (DR Sales Discounts). Net for the period =
            // memos issued in the period minus the unapplied remainder of memos voided in the period.
            var periodCmIssued = await _context.CreditMemos
                .Where(m => m.CompanyId == companyId && m.IssueDate >= from && m.IssueDate <= toEnd)
                .SumAsync(m => (decimal?)m.Amount) ?? 0m;
            var periodCmVoided = await _context.CreditMemos
                .Where(m => m.CompanyId == companyId && m.Status == CreditMemoStatus.Voided
                         && m.UpdatedAt >= from && m.UpdatedAt <= toEnd)
                .SumAsync(m => (decimal?)(m.Amount - m.AmountApplied)) ?? 0m;
            var periodCredits = periodCmIssued - periodCmVoided;
            var totalDeductions = periodDiscounts + periodCredits;
            if (totalDeductions > 0)
                revenueLines.Add(new FinancialReportLine
                {
                    AccountNumber = "4950",
                    AccountName   = "Less: Sales Discounts & Credits",
                    Amount        = -totalDeductions
                });

            // Cash refunds reverse the sale — the revenue portion is contra-revenue (the tax portion
            // relieves Sales Tax Payable, not revenue). Store-credit refunds are excluded (no GL posting).
            var periodRefunds = await _context.Refunds
                .Where(r => r.CompanyId == companyId && !r.IsDeleted && r.Invoice != null && r.RefundMethod != PaymentMethod.StoreCredit
                         && r.RefundDate >= from && r.RefundDate <= toEnd)
                .Select(r => new { r.Amount, r.Invoice!.TaxAmount, r.Invoice.Total })
                .ToListAsync();
            var periodRefundReturns = periodRefunds.Sum(r => RefundAllocation.Split(r.Amount, r.TaxAmount, r.Total).ReturnsPortion);
            if (periodRefundReturns > 0)
                revenueLines.Add(new FinancialReportLine
                {
                    AccountNumber = "4960",
                    AccountName   = "Less: Sales Returns",
                    Amount        = -periodRefundReturns
                });

            // GC sales are deferred to GC Liability at issuance; revenue is recognized on redemption.
            var periodGcReclassified = await _context.InvoiceItems
                .Where(ii => ii.CompanyId == companyId
                          && ii.IsGiftCertificate
                          && ii.Invoice.Status != InvoiceStatus.Draft
                          && ii.Invoice.Status != InvoiceStatus.Voided
                          && ii.Invoice.InvoiceDate >= from && ii.Invoice.InvoiceDate <= toEnd)
                .SumAsync(ii => (decimal?)ii.TotalPrice) ?? 0m;
            if (periodGcReclassified > 0)
                revenueLines.Add(new FinancialReportLine
                {
                    AccountNumber = "2500",
                    AccountName   = "Less: Gift Certificates Issued (Deferred Revenue)",
                    Amount        = -periodGcReclassified
                });

            // Voided GCs with remaining balance are breakage income (liability extinguished).
            var periodGcBreakage = await _context.GiftCertificates
                .Where(gc => gc.CompanyId == companyId && !gc.IsDeleted && gc.Status == GiftCertificateStatus.Voided
                          && gc.UpdatedAt >= from && gc.UpdatedAt <= toEnd
                          && gc.OriginalAmount > gc.RedeemedAmount)
                .SumAsync(gc => (decimal?)(gc.OriginalAmount - gc.RedeemedAmount)) ?? 0m;
            if (periodGcBreakage > 0)
                revenueLines.Add(new FinancialReportLine
                {
                    AccountNumber = "—",
                    AccountName   = "Gift Certificate Breakage Income",
                    Amount        = periodGcBreakage
                });
        }

        // COGS & Expenses — cash basis: expenses paid in period; accrual: by bill/expense date
        var expenseAmounts = new Dictionary<int, decimal>();

        if (isCash)
        {
            var cashExpenses = await _context.Expenses
                .Where(e => e.CompanyId == companyId && e.Date >= from && e.Date <= toEnd)
                .GroupBy(e => e.ExpenseAccountId)
                .Select(g => new { AccountId = g.Key, Amount = g.Sum(e => e.Amount) })
                .ToListAsync();
            foreach (var e in cashExpenses)
                expenseAmounts[e.AccountId] = expenseAmounts.GetValueOrDefault(e.AccountId) + e.Amount;

            // Pro-rate paid bill line items by payment fraction (bill total may be partial)
            var paidBillLines = await _context.BillPayments
                .Where(bp => bp.CompanyId == companyId && bp.PaymentDate >= from && bp.PaymentDate <= toEnd)
                .Include(bp => bp.Bill).ThenInclude(b => b.LineItems)
                .ToListAsync();
            foreach (var bp in paidBillLines)
            {
                var fraction = bp.Bill.Total == 0 ? 1m : bp.Amount / bp.Bill.Total;
                foreach (var li in bp.Bill.LineItems.Where(li => li.AccountId != null))
                    expenseAmounts[li.AccountId!.Value] = expenseAmounts.GetValueOrDefault(li.AccountId!.Value) + li.Amount * fraction;
            }
        }
        else
        {
            var accrualExpenses = await _context.Expenses
                .Where(e => e.CompanyId == companyId && e.Date >= from && e.Date <= toEnd)
                .GroupBy(e => e.ExpenseAccountId)
                .Select(g => new { AccountId = g.Key, Amount = g.Sum(e => e.Amount) })
                .ToListAsync();
            foreach (var e in accrualExpenses)
                expenseAmounts[e.AccountId] = expenseAmounts.GetValueOrDefault(e.AccountId) + e.Amount;

            var accrualBillLines = await _context.BillLineItems
                .Where(bli => bli.CompanyId == companyId
                           && bli.AccountId != null
                           && bli.Bill.Status != BillStatus.Draft
                           && bli.Bill.Status != BillStatus.Voided
                           && bli.Bill.BillDate >= from && bli.Bill.BillDate <= toEnd)
                .GroupBy(bli => bli.AccountId!.Value)
                .Select(g => new { AccountId = g.Key, Amount = g.Sum(bli => bli.Amount) })
                .ToListAsync();
            foreach (var b in accrualBillLines)
                expenseAmounts[b.AccountId] = expenseAmounts.GetValueOrDefault(b.AccountId) + b.Amount;

            // Inventory consumed on jobs posts DR COGS / CR Inventory — recognise the COGS in the period.
            // (Cash basis recognises inventory cost when purchased, so this applies to accrual only.)
            var consumptionCogs = await _context.InventoryTransactions
                .Where(t => t.CompanyId == companyId
                         && (t.TransactionType == InventoryTransactionType.JobUsage || t.TransactionType == InventoryTransactionType.Waste)
                         && t.InventoryItem.CogsAccountId != null && t.InventoryItem.InventoryAccountId != null
                         && t.TransactionDate >= from && t.TransactionDate <= toEnd)
                .GroupBy(t => t.InventoryItem.CogsAccountId!.Value)
                .Select(g => new { AccountId = g.Key, Amount = g.Sum(t => t.TotalCost) })
                .ToListAsync();
            foreach (var c in consumptionCogs)
                expenseAmounts[c.AccountId] = expenseAmounts.GetValueOrDefault(c.AccountId) + c.Amount;
        }

        var expAccounts = await _context.Accounts
            .Where(a => a.CompanyId == companyId && (a.AccountType == AccountType.Expense || a.AccountType == AccountType.CostOfGoods) && a.IsActive)
            .ToDictionaryAsync(a => a.Id);

        var cogsLines    = new List<FinancialReportLine>();
        var expenseLines = new List<FinancialReportLine>();

        foreach (var (accountId, amount) in expenseAmounts.OrderBy(kv => expAccounts.ContainsKey(kv.Key) ? expAccounts[kv.Key].AccountNumber : "999"))
        {
            if (!expAccounts.TryGetValue(accountId, out var acct)) continue;
            var line = new FinancialReportLine { AccountId = accountId, AccountNumber = acct.AccountNumber, AccountName = acct.Name, Amount = amount };
            if (acct.AccountType == AccountType.CostOfGoods) cogsLines.Add(line);
            else expenseLines.Add(line);
        }

        return new ProfitAndLossDto
        {
            From             = from,
            To               = to,
            CompanyName      = companyName,
            AccountingMethod = accountingMethod,
            RevenueLines     = revenueLines,
            TotalRevenue     = revenueLines.Sum(l => l.Amount),
            CogsLines        = cogsLines,
            TotalCogs        = cogsLines.Sum(l => l.Amount),
            ExpenseLines     = expenseLines,
            TotalExpenses    = expenseLines.Sum(l => l.Amount),
        };
    }

    /// <inheritdoc/>
    public async Task<BalanceSheetDto> GetBalanceSheetAsync(int companyId, DateTime asOf, AccountingMethod? method = null)
    {
        var asOfEnd = asOf.AddDays(1).AddTicks(-1);
        var companyName = await GetCompanyNameAsync(companyId);
        var accountingMethod = method ?? await GetCompanyAccountingMethodAsync(companyId);
        var isCash = accountingMethod == AccountingMethod.Cash;

        // Pre-compute balance contributions per account (batch GROUP BY queries avoid N+1)

        var depositsByAcct = await _context.Payments
            .Where(p => p.CompanyId == companyId && p.PaymentDate <= asOfEnd && p.DepositAccountId != null
                     && p.Invoice.Status != InvoiceStatus.Voided)
            .GroupBy(p => p.DepositAccountId!.Value)
            .Select(g => new { Id = g.Key, Amount = g.Sum(p => p.Amount) })
            .ToDictionaryAsync(g => g.Id, g => g.Amount);

        var expFromByAcct = await _context.Expenses
            .Where(e => e.CompanyId == companyId && e.Date <= asOfEnd)
            .GroupBy(e => e.PaymentAccountId)
            .Select(g => new { Id = g.Key, Amount = g.Sum(e => e.Amount) })
            .ToDictionaryAsync(g => g.Id, g => g.Amount);

        var bpFromByAcct = await _context.BillPayments
            .Where(bp => bp.CompanyId == companyId && bp.PaymentDate <= asOfEnd)
            .GroupBy(bp => bp.BankAccountId)
            .Select(g => new { Id = g.Key, Amount = g.Sum(bp => bp.Amount) })
            .ToDictionaryAsync(g => g.Id, g => g.Amount);

        var billsByApAcct = await _context.Bills
            .Where(b => b.CompanyId == companyId && b.Status != BillStatus.Draft && b.Status != BillStatus.Voided && b.BillDate <= asOfEnd)
            .GroupBy(b => b.APAccountId)
            .Select(g => new { Id = g.Key, Amount = g.Sum(b => b.Total) })
            .ToDictionaryAsync(g => g.Id, g => g.Amount);

        var bpByApAcct = await _context.BillPayments
            .Where(bp => bp.CompanyId == companyId && bp.PaymentDate <= asOfEnd)
            .GroupBy(bp => bp.Bill.APAccountId)
            .Select(g => new { Id = g.Key, Amount = g.Sum(bp => bp.Amount) })
            .ToDictionaryAsync(g => g.Id, g => g.Amount);

        // AP: vendor credit applications reduce AP (DR side) when matched against specific bills.
        var vcByApAcctBs = await _context.VendorCreditApplications
            .Where(vca => vca.CompanyId == companyId && vca.AppliedDate <= asOfEnd)
            .GroupBy(vca => vca.VendorCredit.APAccountId)
            .Select(g => new { Id = g.Key, Amount = g.Sum(vca => vca.Amount) })
            .ToDictionaryAsync(g => g.Id, g => g.Amount);

        var taxByAcct = await _context.Invoices
            .Where(i => i.CompanyId == companyId && i.SalesTaxAccountId != null && i.TaxAmount > 0
                     && i.Status != InvoiceStatus.Draft && i.Status != InvoiceStatus.Voided
                     && i.InvoiceDate <= asOfEnd)
            .GroupBy(i => i.SalesTaxAccountId!.Value)
            .Select(g => new { Id = g.Key, Amount = g.Sum(i => i.TaxAmount) })
            .ToDictionaryAsync(g => g.Id, g => g.Amount);

        var arDebits  = await _context.Invoices
            .Where(i => i.CompanyId == companyId && i.Status != InvoiceStatus.Draft && i.Status != InvoiceStatus.Voided && i.InvoiceDate <= asOfEnd)
            .SumAsync(i => (decimal?)i.Total) ?? 0;
        var arCredits = await _context.Payments
            .Where(p => p.CompanyId == companyId && p.PaymentDate <= asOfEnd
                     && p.Invoice.Status != InvoiceStatus.Voided)
            .SumAsync(p => (decimal?)p.Amount) ?? 0;
        // Credit memo applications reduce open AR (CR AR when a credit is applied to an invoice).
        var cmAppliedBs = await _context.CreditMemoApplications
            .Where(a => a.CompanyId == companyId && a.AppliedDate <= asOfEnd && a.Invoice.Status != InvoiceStatus.Voided)
            .SumAsync(a => (decimal?)a.AmountApplied) ?? 0;
        arCredits += cmAppliedBs;
        // Gift-certificate redemptions also credit AR (ApplyGiftCertificate posts DR 2500 / CR AR).
        // Mirror the posting here so AR is not overstated and the entry's two sides stay balanced.
        var gcRedeemedBs = await _context.GiftCertificateRedemptions
            .Where(r => r.CompanyId == companyId && !r.IsDeleted && r.RedeemedDate <= asOfEnd
                     && r.Invoice.Status != InvoiceStatus.Voided)
            .SumAsync(r => (decimal?)r.AmountRedeemed) ?? 0;
        arCredits += gcRedeemedBs;

        // Customer Credits (2350): a credit memo books DR Sales Discounts / CR Customer Credits on issue,
        // then DR Customer Credits / CR AR on apply. Contra-revenue (retained earnings) = issued amount
        // (active in full + applied portion of voided); the 2350 liability = unapplied balance on active memos.
        var cmIssuedNonVoidedBs = await _context.CreditMemos
            .Where(m => m.CompanyId == companyId && m.Status != CreditMemoStatus.Voided && m.IssueDate <= asOfEnd)
            .SumAsync(m => (decimal?)m.Amount) ?? 0m;
        var cmAppliedNonVoidedBs = await _context.CreditMemoApplications
            .Where(a => a.CompanyId == companyId && a.AppliedDate <= asOfEnd && a.Invoice.Status != InvoiceStatus.Voided
                     && a.CreditMemo.Status != CreditMemoStatus.Voided)
            .SumAsync(a => (decimal?)a.AmountApplied) ?? 0m;
        var cmContraRevenueBs = cmIssuedNonVoidedBs + (cmAppliedBs - cmAppliedNonVoidedBs);
        var customerCreditsAcctIdBs = await _context.Accounts
            .Where(a => a.CompanyId == companyId && a.AccountNumber == "2350" && a.IsActive && !a.IsDeleted)
            .Select(a => (int?)a.Id).FirstOrDefaultAsync();
        // Cash refunds reverse the sale: revenue portion reduces retained earnings (Sales Returns),
        // tax portion relieves Sales Tax Payable, cash leaves the bank (refundsByAcctBs). AR is untouched.
        // Store-credit refunds post via CreditMemo, not the GL, so are excluded.
        var saleReversingRefundsBs = await _context.Refunds
            .Where(r => r.CompanyId == companyId && r.RefundDate <= asOfEnd && !r.IsDeleted && r.Invoice != null
                     && r.RefundMethod != PaymentMethod.StoreCredit)
            .Select(r => new { r.Amount, r.Invoice!.TaxAmount, r.Invoice.Total, r.Invoice.SalesTaxAccountId })
            .ToListAsync();
        decimal refundReturnsTotalBs = 0m;
        var refundTaxByAcctBs = new Dictionary<int, decimal>();
        foreach (var r in saleReversingRefundsBs)
        {
            var (returnsPortion, taxPortion) = RefundAllocation.Split(r.Amount, r.TaxAmount, r.Total);
            refundReturnsTotalBs += returnsPortion;
            if (taxPortion != 0m && r.SalesTaxAccountId.HasValue)
                refundTaxByAcctBs[r.SalesTaxAccountId.Value] = refundTaxByAcctBs.GetValueOrDefault(r.SalesTaxAccountId.Value) + taxPortion;
        }

        // Refunds by bank account: money that left the account (CR to checking/bank).
        var refundsByAcctBs = await _context.Refunds
            .Where(r => r.CompanyId == companyId && r.RefundDate <= asOfEnd && !r.IsDeleted && r.DepositAccountId != null)
            .GroupBy(r => r.DepositAccountId!.Value)
            .Select(g => new { Id = g.Key, Amount = g.Sum(r => r.Amount) })
            .ToDictionaryAsync(g => g.Id, g => g.Amount);

        // Deposits by bank account: cash received at deposit recording time (DR bank).
        var depositsByAcctDepBs = await _context.Deposits
            .Where(d => d.CompanyId == companyId && !d.IsDeleted && d.DepositAccountId != null && d.ReceivedDate <= asOfEnd)
            .GroupBy(d => d.DepositAccountId!.Value)
            .Select(g => new { Id = g.Key, Amount = g.Sum(d => d.Amount) })
            .ToDictionaryAsync(g => g.Id, g => g.Amount);

        // Customer Deposits liability (2300): credits = all deposits taken; debits = deposits applied to invoices.
        var custDepositsAcctIdBs = await _context.Accounts
            .Where(a => a.CompanyId == companyId && a.AccountNumber == "2300" && a.IsActive && !a.IsDeleted)
            .Select(a => (int?)a.Id).FirstOrDefaultAsync();
        var custDepositsCreditsBs = custDepositsAcctIdBs.HasValue
            ? (await _context.Deposits
                .Where(d => d.CompanyId == companyId && !d.IsDeleted && d.ReceivedDate <= asOfEnd)
                .SumAsync(d => (decimal?)d.Amount) ?? 0m) : 0m;
        var custDepositsDebitsBs = custDepositsAcctIdBs.HasValue
            ? (await _context.Deposits
                .Where(d => d.CompanyId == companyId && !d.IsDeleted && d.AppliedToInvoiceId != null && d.AppliedDate <= asOfEnd)
                .SumAsync(d => (decimal?)d.Amount) ?? 0m) : 0m;

        // Gift Certificate Liability (2500): balance driven by GC issuances, redemptions, and voids.
        var gcLiabilityAcctIdBs = await _context.Accounts
            .Where(a => a.CompanyId == companyId && a.AccountNumber == "2500" && a.IsActive && !a.IsDeleted)
            .Select(a => (int?)a.Id).FirstOrDefaultAsync();
        var gcLiabilityCreditsBs = gcLiabilityAcctIdBs.HasValue
            ? (await _context.GiftCertificates
                .Where(gc => gc.CompanyId == companyId && !gc.IsDeleted && gc.IssueDate <= asOfEnd)
                .SumAsync(gc => (decimal?)gc.OriginalAmount) ?? 0m) : 0m;
        var gcLiabilityDebitsBs = gcLiabilityAcctIdBs.HasValue
            ? ((await _context.GiftCertificateRedemptions
                .Where(r => r.CompanyId == companyId && !r.IsDeleted && r.RedeemedDate <= asOfEnd)
                .SumAsync(r => (decimal?)r.AmountRedeemed) ?? 0m)
             + (await _context.GiftCertificates
                .Where(gc => gc.CompanyId == companyId && !gc.IsDeleted && gc.Status == GiftCertificateStatus.Voided
                          && gc.UpdatedAt <= asOfEnd && gc.OriginalAmount > gc.RedeemedAmount)
                .SumAsync(gc => (decimal?)(gc.OriginalAmount - gc.RedeemedAmount)) ?? 0m)) : 0m;

        // Retained earnings = net P&L from inception through asOf, covering four sources:
        // (1) invoice revenue, (2) invoice discounts, (3) direct expenses, (4) vendor bill costs,
        // plus (5) the net effect of any posted journal entries on revenue/expense/COGS accounts
        // (accruals, depreciation, year-end closes, and other adjustments not in the tables above).
        var lifetimeRevenue   = await _context.InvoiceItems
            .Where(ii => ii.CompanyId == companyId && ii.Invoice.Status != InvoiceStatus.Draft && ii.Invoice.Status != InvoiceStatus.Voided && ii.Invoice.InvoiceDate <= asOfEnd)
            .SumAsync(ii => (decimal?)ii.TotalPrice) ?? 0;
        var lifetimeDiscounts = isCash ? 0m
            : (await _context.Invoices
                .Where(i => i.CompanyId == companyId && i.Status != InvoiceStatus.Draft && i.Status != InvoiceStatus.Voided
                         && i.DiscountAmount > 0 && i.InvoiceDate <= asOfEnd)
                .SumAsync(i => (decimal?)i.DiscountAmount) ?? 0m);
        // Credit memos are contra-revenue recognized at issue (DR Sales Discounts). Net revenue is
        // reduced by the issued amount (active memos in full + applied portion of voided memos).
        var lifetimeCreditMemos = isCash ? 0m : cmContraRevenueBs;
        var lifetimeDirectExp = await _context.Expenses
            .Where(e => e.CompanyId == companyId && e.Date <= asOfEnd)
            .SumAsync(e => (decimal?)e.Amount) ?? 0;
        var lifetimeBillCosts = await _context.BillLineItems
            .Where(bli => bli.CompanyId == companyId && bli.Bill.Status != BillStatus.Draft && bli.Bill.Status != BillStatus.Voided && bli.Bill.BillDate <= asOfEnd)
            .SumAsync(bli => (decimal?)bli.Amount) ?? 0;

        // JE net effect on revenue accounts (positive = additional revenue recognised via manual JE)
        var revenueAcctIds = await _context.Accounts
            .Where(a => a.CompanyId == companyId && a.AccountType == AccountType.Revenue && !a.IsDeleted)
            .Select(a => a.Id).ToListAsync();
        var expCogsAcctIds = await _context.Accounts
            .Where(a => a.CompanyId == companyId
                     && (a.AccountType == AccountType.Expense || a.AccountType == AccountType.CostOfGoods)
                     && !a.IsDeleted)
            .Select(a => a.Id).ToListAsync();

        var jeRevNet = revenueAcctIds.Count > 0
            ? (await _context.JournalEntryLines
                .Where(l => revenueAcctIds.Contains(l.AccountId)
                         && l.JournalEntry.Status == JournalEntryStatus.Posted
                         && l.JournalEntry.EntryDate <= asOfEnd)
                .SumAsync(l => (decimal?)(l.CreditAmount - l.DebitAmount)) ?? 0m)
            : 0m;

        // JE net effect on expense/COGS accounts (positive = additional expense recognised via manual JE)
        var jeExpNet = expCogsAcctIds.Count > 0
            ? (await _context.JournalEntryLines
                .Where(l => expCogsAcctIds.Contains(l.AccountId)
                         && l.JournalEntry.Status == JournalEntryStatus.Posted
                         && l.JournalEntry.EntryDate <= asOfEnd)
                .SumAsync(l => (decimal?)(l.DebitAmount - l.CreditAmount)) ?? 0m)
            : 0m;

        // GC items sold via invoices are reclassified to GC Liability and not yet earned income.
        var lifetimeGcReclassified = await _context.InvoiceItems
            .Where(ii => ii.CompanyId == companyId && ii.IsGiftCertificate
                      && ii.Invoice.Status != InvoiceStatus.Draft
                      && ii.Invoice.Status != InvoiceStatus.Voided
                      && ii.Invoice.InvoiceDate <= asOfEnd)
            .SumAsync(ii => (decimal?)ii.TotalPrice) ?? 0m;
        // Voided GCs with remaining balance become breakage income (the liability is extinguished).
        var lifetimeGcBreakage = await _context.GiftCertificates
            .Where(gc => gc.CompanyId == companyId && !gc.IsDeleted && gc.Status == GiftCertificateStatus.Voided
                      && gc.UpdatedAt <= asOfEnd && gc.OriginalAmount > gc.RedeemedAmount)
            .SumAsync(gc => (decimal?)(gc.OriginalAmount - gc.RedeemedAmount)) ?? 0m;

        var retainedEarnings = lifetimeRevenue + jeRevNet
                             - lifetimeDiscounts
                             - lifetimeCreditMemos
                             - refundReturnsTotalBs   // revenue portion of cash refunds (reversed sales)
                             - lifetimeGcReclassified // deferred to GC Liability, not earned yet
                             + lifetimeGcBreakage     // breakage income when GC voided with balance
                             - lifetimeDirectExp
                             - lifetimeBillCosts
                             - jeExpNet;

        var accounts = await _context.Accounts
            .Where(a => a.CompanyId == companyId && a.IsActive)
            .OrderBy(a => a.AccountNumber)
            .ToListAsync();

        // Standard double-entry: assets have normal debit balance; liabilities+equity have normal credit balance.
        decimal ComputeBalance(Account a)
        {
            bool normalDebit = a.AccountType == AccountType.Asset;
            decimal debits = 0, credits = 0;

            if (a.AccountSubType == AccountSubType.AccountsReceivable)
            {
                debits = arDebits; credits = arCredits;
            }
            else if (a.AccountSubType == AccountSubType.AccountsPayable)
            {
                credits  = billsByApAcct.GetValueOrDefault(a.Id);
                debits   = bpByApAcct.GetValueOrDefault(a.Id);
                debits  += vcByApAcctBs.GetValueOrDefault(a.Id); // vendor credit applications reduce AP
            }
            else
            {
                debits  += depositsByAcct.GetValueOrDefault(a.Id);
                credits += expFromByAcct.GetValueOrDefault(a.Id);
                credits += bpFromByAcct.GetValueOrDefault(a.Id);
                credits += taxByAcct.GetValueOrDefault(a.Id);
                credits += refundsByAcctBs.GetValueOrDefault(a.Id);      // refunds reduce bank balance
                debits  += depositsByAcctDepBs.GetValueOrDefault(a.Id);  // deposits increase bank balance
                debits  += refundTaxByAcctBs.GetValueOrDefault(a.Id);    // refund tax portion relieves the tax liability
                if (gcLiabilityAcctIdBs.HasValue && a.Id == gcLiabilityAcctIdBs.Value)
                {
                    credits += gcLiabilityCreditsBs; // GC issued → CR liability
                    debits  += gcLiabilityDebitsBs;  // redeemed/voided → DR liability
                }
                if (customerCreditsAcctIdBs.HasValue && a.Id == customerCreditsAcctIdBs.Value)
                {
                    credits += cmIssuedNonVoidedBs;   // credit memos issued → CR liability
                    debits  += cmAppliedNonVoidedBs;  // applied → DR liability
                }
                if (custDepositsAcctIdBs.HasValue && a.Id == custDepositsAcctIdBs.Value)
                {
                    credits += custDepositsCreditsBs; // deposits taken → CR liability
                    debits  += custDepositsDebitsBs;  // deposits applied → DR liability
                }
            }

            decimal opening = (a.OpeningBalanceDate == null || a.OpeningBalanceDate.Value.Date <= asOf)
                ? a.OpeningBalance : 0;
            decimal net = normalDebit ? debits - credits : credits - debits;
            return opening + net;
        }

        FinancialReportLine ToLine(Account a) => new()
        {
            AccountId     = a.Id,
            AccountNumber = a.AccountNumber,
            AccountName   = a.Name,
            Amount        = ComputeBalance(a)
        };

        var assetAccts     = accounts.Where(a => a.AccountType == AccountType.Asset).ToList();
        var liabilityAccts = accounts.Where(a => a.AccountType == AccountType.Liability).ToList();
        var equityAccts    = accounts.Where(a => a.AccountType == AccountType.Equity).ToList();

        // Cash basis: AR and AP have no meaning (no receivables/payables concept)
        var currentAssets = assetAccts
            .Where(a => a.AccountSubType is AccountSubType.Checking or AccountSubType.Savings or AccountSubType.Inventory or AccountSubType.OtherCurrentAsset
                     || (!isCash && a.AccountSubType == AccountSubType.AccountsReceivable))
            .Select(ToLine).ToList();
        var fixedAssets         = assetAccts.Where(a => a.AccountSubType == AccountSubType.FixedAsset).Select(ToLine).ToList();
        var otherAssets         = assetAccts.Where(a => a.AccountSubType == AccountSubType.OtherAsset).Select(ToLine).ToList();
        var currentLiabilities  = liabilityAccts
            .Where(a => a.AccountSubType is AccountSubType.CreditCard or AccountSubType.OtherCurrentLiability
                     || (!isCash && a.AccountSubType == AccountSubType.AccountsPayable))
            .Select(ToLine).ToList();
        var longTermLiabilities = liabilityAccts.Where(a => a.AccountSubType == AccountSubType.LongTermLiability).Select(ToLine).ToList();
        var equityLines         = equityAccts.Select(ToLine).ToList();

        var totalAssets      = currentAssets.Sum(l => l.Amount) + fixedAssets.Sum(l => l.Amount) + otherAssets.Sum(l => l.Amount);
        var totalLiabilities = currentLiabilities.Sum(l => l.Amount) + longTermLiabilities.Sum(l => l.Amount);
        var totalEquity      = equityLines.Sum(l => l.Amount) + retainedEarnings;

        return new BalanceSheetDto
        {
            AsOf                = asOf,
            CompanyName         = companyName,
            AccountingMethod    = accountingMethod,
            CurrentAssets       = currentAssets,
            FixedAssets         = fixedAssets,
            OtherAssets         = otherAssets,
            TotalAssets         = totalAssets,
            CurrentLiabilities  = currentLiabilities,
            LongTermLiabilities = longTermLiabilities,
            TotalLiabilities    = totalLiabilities,
            EquityLines         = equityLines,
            RetainedEarnings    = retainedEarnings,
            TotalEquity         = totalEquity,
        };
    }

    /// <inheritdoc/>
    public async Task<ArAgingReportDto> GetArAgingAsync(int companyId, DateTime asOf)
    {
        var asOfEnd = asOf.AddDays(1).AddTicks(-1);
        var companyName = await GetCompanyNameAsync(companyId);

        var openInvoices = await _context.Invoices
            .Include(i => i.Customer)
            .Where(i => i.CompanyId == companyId
                     && i.Status != InvoiceStatus.Draft
                     && i.Status != InvoiceStatus.Voided
                     && i.Status != InvoiceStatus.Paid
                     && i.InvoiceDate <= asOfEnd
                     && (i.Total - i.AmountPaid - i.CreditApplied - i.GiftCertificateRedeemed) > 0)
            .OrderBy(i => i.Customer!.CompanyName)
            .ThenBy(i => i.DueDate)
            .ToListAsync();

        static string AgingBucket(int d) => d switch
        {
            <= 0  => "current",
            <= 30 => "1-30",
            <= 60 => "31-60",
            <= 90 => "61-90",
            _     => "90+"
        };

        var customerDtos = new List<ArAgingCustomerDto>();

        foreach (var grp in openInvoices.GroupBy(i => new { i.CustomerId, i.Customer!.CompanyName, i.Customer.ContactFirstName, i.Customer.ContactLastName, i.Customer.IsCommercial }))
        {
            var customerName = grp.Key.IsCommercial
                ? grp.Key.CompanyName
                : $"{grp.Key.ContactFirstName} {grp.Key.ContactLastName}".Trim();

            var custDto = new ArAgingCustomerDto { CustomerId = grp.Key.CustomerId, CustomerName = customerName };

            foreach (var inv in grp)
            {
                var balance     = inv.BalanceDue;
                var daysOverdue = inv.DueDate.HasValue ? (int)(asOf - inv.DueDate.Value.Date).TotalDays : 0;

                custDto.Invoices.Add(new ArAgingInvoiceDto
                {
                    InvoiceId     = inv.Id,
                    InvoiceNumber = inv.InvoiceNumber,
                    InvoiceDate   = inv.InvoiceDate,
                    DueDate       = inv.DueDate,
                    BalanceDue    = balance,
                    DaysOverdue   = daysOverdue
                });

                switch (AgingBucket(daysOverdue))
                {
                    case "current": custDto.TotalCurrent += balance; break;
                    case "1-30":    custDto.Total1to30   += balance; break;
                    case "31-60":   custDto.Total31to60  += balance; break;
                    case "61-90":   custDto.Total61to90  += balance; break;
                    default:        custDto.TotalOver90  += balance; break;
                }
            }

            customerDtos.Add(custDto);
        }

        var sorted = customerDtos.OrderByDescending(c => c.TotalBalance).ToList();

        return new ArAgingReportDto
        {
            AsOf         = asOf,
            CompanyName  = companyName,
            Customers    = sorted,
            TotalCurrent = sorted.Sum(c => c.TotalCurrent),
            Total1to30   = sorted.Sum(c => c.Total1to30),
            Total31to60  = sorted.Sum(c => c.Total31to60),
            Total61to90  = sorted.Sum(c => c.Total61to90),
            TotalOver90  = sorted.Sum(c => c.TotalOver90),
        };
    }

    /// <inheritdoc/>
    public async Task<SalesIncomeReportDto> GetSalesAndIncomeAsync(int companyId, DateTime from, DateTime to)
    {
        var toEnd = to.AddDays(1).AddTicks(-1);
        var companyName = await GetCompanyNameAsync(companyId);

        var invoices = await _context.Invoices
            .Include(i => i.Customer)
            .Include(i => i.Payments)
            .Where(i => i.CompanyId == companyId
                     && i.Status != InvoiceStatus.Draft
                     && i.Status != InvoiceStatus.Voided
                     && i.InvoiceDate >= from && i.InvoiceDate <= toEnd)
            .OrderBy(i => i.InvoiceDate)
            .ToListAsync();

        var collectedInPeriod = await _context.Payments
            .Where(p => p.CompanyId == companyId && p.PaymentDate >= from && p.PaymentDate <= toEnd)
            .SumAsync(p => (decimal?)p.Amount) ?? 0;

        var byCustomer = invoices
            .GroupBy(i => new
            {
                i.CustomerId,
                Name = i.Customer!.IsCommercial
                    ? i.Customer.CompanyName
                    : $"{i.Customer.ContactFirstName} {i.Customer.ContactLastName}".Trim()
            })
            .Select(g => new SalesByCustomerDto
            {
                CustomerId    = g.Key.CustomerId,
                CustomerName  = g.Key.Name,
                InvoiceCount  = g.Count(),
                TotalInvoiced = g.Sum(i => i.Total),
                TotalPaid     = g.Sum(i => i.AmountPaid),
                BalanceDue    = g.Sum(i => i.BalanceDue),
            })
            .OrderByDescending(c => c.TotalInvoiced)
            .ToList();

        var byMonth = invoices
            .GroupBy(i => new { i.InvoiceDate.Year, i.InvoiceDate.Month })
            .Select(g => new SalesByMonthDto
            {
                Year           = g.Key.Year,
                Month          = g.Key.Month,
                Label          = new DateTime(g.Key.Year, g.Key.Month, 1).ToString("MMM yyyy"),
                TotalInvoiced  = g.Sum(i => i.Total),
                TotalCollected = g.Sum(i => i.AmountPaid),
                InvoiceCount   = g.Count(),
            })
            .OrderBy(m => m.Year).ThenBy(m => m.Month)
            .ToList();

        var invoiceLines = invoices.Select(i => new SalesInvoiceLineDto
        {
            InvoiceId     = i.Id,
            InvoiceNumber = i.InvoiceNumber,
            CustomerName  = i.Customer!.IsCommercial
                ? i.Customer.CompanyName
                : $"{i.Customer.ContactFirstName} {i.Customer.ContactLastName}".Trim(),
            InvoiceDate   = i.InvoiceDate,
            DueDate       = i.DueDate,
            Status        = i.Status.ToString(),
            SubTotal      = i.SubTotal,
            TaxAmount     = i.TaxAmount,
            Total         = i.Total,
            AmountPaid    = i.AmountPaid,
            BalanceDue    = i.BalanceDue,
        }).ToList();

        return new SalesIncomeReportDto
        {
            From           = from,
            To             = to,
            CompanyName    = companyName,
            TotalInvoiced  = invoices.Sum(i => i.Total),
            TotalCollected = collectedInPeriod,
            TotalTax       = invoices.Sum(i => i.TaxAmount),
            TotalDiscount  = invoices.Sum(i => i.DiscountAmount),
            InvoiceCount   = invoices.Count,
            CustomerCount  = invoices.Select(i => i.CustomerId).Distinct().Count(),
            ByCustomer     = byCustomer,
            ByMonth        = byMonth,
            Invoices       = invoiceLines,
        };
    }

    /// <inheritdoc/>
    public async Task<SalesTaxReportDto> GetSalesTaxReportAsync(int companyId, DateTime from, DateTime to)
    {
        var toEnd = to.AddDays(1).AddTicks(-1);
        var companyName = await GetCompanyNameAsync(companyId);

        var invoices = await _context.Invoices
            .Include(i => i.Customer)
            .Include(i => i.SalesTaxAccount)
            .Where(i => i.CompanyId == companyId
                     && i.Status != InvoiceStatus.Draft
                     && i.Status != InvoiceStatus.Voided
                     && i.InvoiceDate >= from && i.InvoiceDate <= toEnd)
            .AsNoTracking()
            .OrderBy(i => i.InvoiceDate)
            .ToListAsync();

        var taxable    = invoices.Where(i => i.TaxAmount > 0).ToList();
        var nonTaxable = invoices.Where(i => i.TaxAmount == 0).ToList();

        var byAccount = invoices
            .Where(i => i.TaxAmount > 0)
            .GroupBy(i => new
            {
                i.SalesTaxAccountId,
                AccountName   = i.SalesTaxAccount?.Name ?? "Unassigned",
                AccountNumber = i.SalesTaxAccount?.AccountNumber ?? string.Empty
            })
            .Select(g => new SalesTaxByAccountDto
            {
                AccountId     = g.Key.SalesTaxAccountId,
                AccountName   = g.Key.AccountName,
                AccountNumber = g.Key.AccountNumber,
                TaxableSales  = g.Sum(i => i.SubTotal),
                TaxBilled     = g.Sum(i => i.TaxAmount),
                InvoiceCount  = g.Count()
            })
            .OrderBy(a => a.AccountNumber)
            .ThenBy(a => a.AccountName)
            .ToList();

        var byMonth = invoices
            .Where(i => i.TaxAmount > 0)
            .GroupBy(i => new { i.InvoiceDate.Year, i.InvoiceDate.Month })
            .Select(g => new SalesTaxByMonthDto
            {
                Year         = g.Key.Year,
                Month        = g.Key.Month,
                Label        = new DateTime(g.Key.Year, g.Key.Month, 1).ToString("MMM yyyy"),
                TaxableSales = g.Sum(i => i.SubTotal),
                TaxBilled    = g.Sum(i => i.TaxAmount),
                InvoiceCount = g.Count()
            })
            .OrderBy(m => m.Year).ThenBy(m => m.Month)
            .ToList();

        var invoiceLines = invoices.Select(i => new SalesTaxInvoiceLineDto
        {
            InvoiceId      = i.Id,
            InvoiceNumber  = i.InvoiceNumber,
            CustomerName   = i.Customer!.IsCommercial
                ? i.Customer.CompanyName ?? string.Empty
                : $"{i.Customer.ContactFirstName} {i.Customer.ContactLastName}".Trim(),
            InvoiceDate    = i.InvoiceDate,
            Status         = i.Status.ToString(),
            SubTotal       = i.SubTotal,
            TaxPercent     = i.TaxPercent,
            TaxAmount      = i.TaxAmount,
            Total          = i.Total,
            AmountPaid     = i.AmountPaid,
            BalanceDue     = i.BalanceDue,
            TaxAccountName = i.SalesTaxAccount != null
                ? $"{i.SalesTaxAccount.AccountNumber} {i.SalesTaxAccount.Name}".Trim()
                : string.Empty
        }).ToList();

        return new SalesTaxReportDto
        {
            From                   = from,
            To                     = to,
            CompanyName            = companyName,
            TotalTaxableSales      = taxable.Sum(i => i.SubTotal),
            TotalNonTaxableSales   = nonTaxable.Sum(i => i.SubTotal),
            TotalTaxBilled         = taxable.Sum(i => i.TaxAmount),
            TaxableInvoiceCount    = taxable.Count,
            NonTaxableInvoiceCount = nonTaxable.Count,
            ByAccount              = byAccount,
            ByMonth                = byMonth,
            Invoices               = invoiceLines
        };
    }

    /// <inheritdoc/>
    public async Task<ApAgingReportDto> GetApAgingAsync(int companyId, DateTime asOf)
    {
        var asOfEnd = asOf.AddDays(1).AddTicks(-1);
        var companyName = await GetCompanyNameAsync(companyId);

        // BalanceDue is a computed property — filter on persisted columns in SQL,
        // then apply BalanceDue > 0 client-side after materialisation.
        var openBills = (await _context.Bills
            .Include(b => b.Vendor)
            .Where(b => b.CompanyId == companyId
                     && b.Status != BillStatus.Draft
                     && b.Status != BillStatus.Voided
                     && b.Status != BillStatus.Paid
                     && b.BillDate <= asOfEnd)
            .OrderBy(b => b.Vendor!.CompanyName)
            .ThenBy(b => b.DueDate)
            .ToListAsync())
            .Where(b => b.BalanceDue > 0)
            .ToList();

        static string AgingBucket(int d) => d switch
        {
            <= 0  => "current",
            <= 30 => "1-30",
            <= 60 => "31-60",
            <= 90 => "61-90",
            _     => "90+"
        };

        var vendorDtos = new List<ApAgingVendorDto>();

        foreach (var grp in openBills.GroupBy(b => new { b.VendorId, b.Vendor!.CompanyName }))
        {
            var vendDto = new ApAgingVendorDto
            {
                VendorId   = grp.Key.VendorId,
                VendorName = grp.Key.CompanyName
            };

            foreach (var bill in grp)
            {
                var balance     = bill.BalanceDue;
                var daysOverdue = bill.DueDate.HasValue
                    ? (int)(asOf - bill.DueDate.Value.Date).TotalDays
                    : 0;

                vendDto.Bills.Add(new ApAgingBillDto
                {
                    BillId     = bill.Id,
                    BillNumber = bill.BillNumber,
                    BillDate   = bill.BillDate,
                    DueDate    = bill.DueDate,
                    BalanceDue = balance,
                    DaysOverdue = daysOverdue
                });

                switch (AgingBucket(daysOverdue))
                {
                    case "current": vendDto.TotalCurrent += balance; break;
                    case "1-30":    vendDto.Total1to30   += balance; break;
                    case "31-60":   vendDto.Total31to60  += balance; break;
                    case "61-90":   vendDto.Total61to90  += balance; break;
                    default:        vendDto.TotalOver90  += balance; break;
                }
            }

            vendorDtos.Add(vendDto);
        }

        var sorted = vendorDtos.OrderByDescending(v => v.TotalBalance).ToList();

        return new ApAgingReportDto
        {
            AsOf        = asOf,
            CompanyName = companyName,
            Vendors     = sorted,
            TotalCurrent = sorted.Sum(v => v.TotalCurrent),
            Total1to30   = sorted.Sum(v => v.Total1to30),
            Total31to60  = sorted.Sum(v => v.Total31to60),
            Total61to90  = sorted.Sum(v => v.Total61to90),
            TotalOver90  = sorted.Sum(v => v.TotalOver90),
        };
    }

    /// <inheritdoc/>
    /// <remarks>
    /// Balances are computed dynamically from transaction tables using the same pre-computed
    /// dictionary approach as <see cref="GetBalanceSheetAsync"/>, so the <paramref name="asOf"/>
    /// date is respected. This replaces the previous implementation that read the denormalised
    /// <c>Account.CurrentBalance</c> field, which always reflected the current date regardless of
    /// what date was selected.
    /// </remarks>
    public async Task<TrialBalanceDto> GetTrialBalanceAsync(int companyId, DateTime asOf)
    {
        var asOfEnd     = asOf.AddDays(1).AddTicks(-1);
        var companyName = await GetCompanyNameAsync(companyId);

        // ── Pre-compute per-account contribution dictionaries (batch GROUP BY, no N+1) ──────

        // Bank/cash: customer payments deposited here (DR)
        var depositsByAcct = await _context.Payments
            .Where(p => p.CompanyId == companyId && p.PaymentDate <= asOfEnd && p.DepositAccountId != null
                     && p.Invoice.Status != InvoiceStatus.Voided)
            .GroupBy(p => p.DepositAccountId!.Value)
            .Select(g => new { Id = g.Key, Amt = g.Sum(p => p.Amount) })
            .ToDictionaryAsync(g => g.Id, g => g.Amt);

        // AP: vendor credit applications reduce AP (DR) — credits are applied when a vendor
        // issues a credit note and it is matched against a specific bill.
        var vcByApAcct = await _context.VendorCreditApplications
            .Where(vca => vca.CompanyId == companyId && vca.AppliedDate <= asOfEnd)
            .GroupBy(vca => vca.VendorCredit.APAccountId)
            .Select(g => new { Id = g.Key, Amt = g.Sum(vca => vca.Amount) })
            .ToDictionaryAsync(g => g.Id, g => g.Amt);

        // Bank/cash: expenses paid from here (CR)
        var expFromByAcct = await _context.Expenses
            .Where(e => e.CompanyId == companyId && e.Date <= asOfEnd)
            .GroupBy(e => e.PaymentAccountId)
            .Select(g => new { Id = g.Key, Amt = g.Sum(e => e.Amount) })
            .ToDictionaryAsync(g => g.Id, g => g.Amt);

        // Bank/cash: bill payments made from here (CR)
        var bpFromByAcct = await _context.BillPayments
            .Where(bp => bp.CompanyId == companyId && bp.PaymentDate <= asOfEnd)
            .GroupBy(bp => bp.BankAccountId)
            .Select(g => new { Id = g.Key, Amt = g.Sum(bp => bp.Amount) })
            .ToDictionaryAsync(g => g.Id, g => g.Amt);

        // AP: bills increase AP (CR)
        var billsByApAcct = await _context.Bills
            .Where(b => b.CompanyId == companyId && b.Status != BillStatus.Draft && b.Status != BillStatus.Voided && b.BillDate <= asOfEnd)
            .GroupBy(b => b.APAccountId)
            .Select(g => new { Id = g.Key, Amt = g.Sum(b => b.Total) })
            .ToDictionaryAsync(g => g.Id, g => g.Amt);

        // AP: bill payments reduce AP (DR)
        var bpByApAcct = await _context.BillPayments
            .Where(bp => bp.CompanyId == companyId && bp.PaymentDate <= asOfEnd)
            .GroupBy(bp => bp.Bill.APAccountId)
            .Select(g => new { Id = g.Key, Amt = g.Sum(bp => bp.Amount) })
            .ToDictionaryAsync(g => g.Id, g => g.Amt);

        // Tax liability: sales tax collected (CR)
        var taxByAcct = await _context.Invoices
            .Where(i => i.CompanyId == companyId && i.SalesTaxAccountId != null && i.TaxAmount > 0
                     && i.Status != InvoiceStatus.Draft && i.Status != InvoiceStatus.Voided
                     && i.InvoiceDate <= asOfEnd)
            .GroupBy(i => i.SalesTaxAccountId!.Value)
            .Select(g => new { Id = g.Key, Amt = g.Sum(i => i.TaxAmount) })
            .ToDictionaryAsync(g => g.Id, g => g.Amt);

        // Revenue accounts: invoice line items (CR)
        var revenueByAcct = await _context.InvoiceItems
            .Where(ii => ii.CompanyId == companyId && ii.RevenueAccountId != null
                      && ii.Invoice.Status != InvoiceStatus.Draft
                      && ii.Invoice.Status != InvoiceStatus.Voided
                      && ii.Invoice.InvoiceDate <= asOfEnd)
            .GroupBy(ii => ii.RevenueAccountId!.Value)
            .Select(g => new { Id = g.Key, Amt = g.Sum(ii => ii.TotalPrice) })
            .ToDictionaryAsync(g => g.Id, g => g.Amt);

        // Expense accounts: direct expenses (DR)
        var expenseByAcct = await _context.Expenses
            .Where(e => e.CompanyId == companyId && e.Date <= asOfEnd)
            .GroupBy(e => e.ExpenseAccountId)
            .Select(g => new { Id = g.Key, Amt = g.Sum(e => e.Amount) })
            .ToDictionaryAsync(g => g.Id, g => g.Amt);

        // Expense/COGS accounts: vendor bill line items (DR)
        var billLinesByAcct = await _context.BillLineItems
            .Where(bli => bli.CompanyId == companyId && bli.AccountId != null
                       && bli.Bill.Status != BillStatus.Draft
                       && bli.Bill.Status != BillStatus.Voided
                       && bli.Bill.BillDate <= asOfEnd)
            .GroupBy(bli => bli.AccountId!.Value)
            .Select(g => new { Id = g.Key, Amt = g.Sum(bli => bli.Amount) })
            .ToDictionaryAsync(g => g.Id, g => g.Amt);

        // Inventory consumption: COGS account (DR) and Inventory asset account (CR) for JobUsage/Waste
        // transactions on items with both accounts mapped — mirrors the DR COGS / CR Inventory posting.
        var cogsConsumptionByAcct = await _context.InventoryTransactions
            .Where(t => t.CompanyId == companyId
                     && (t.TransactionType == InventoryTransactionType.JobUsage || t.TransactionType == InventoryTransactionType.Waste)
                     && t.InventoryItem.CogsAccountId != null && t.InventoryItem.InventoryAccountId != null
                     && t.TransactionDate <= asOfEnd)
            .GroupBy(t => t.InventoryItem.CogsAccountId!.Value)
            .Select(g => new { Id = g.Key, Amt = g.Sum(t => t.TotalCost) })
            .ToDictionaryAsync(g => g.Id, g => g.Amt);
        var invConsumptionByAcct = await _context.InventoryTransactions
            .Where(t => t.CompanyId == companyId
                     && (t.TransactionType == InventoryTransactionType.JobUsage || t.TransactionType == InventoryTransactionType.Waste)
                     && t.InventoryItem.CogsAccountId != null && t.InventoryItem.InventoryAccountId != null
                     && t.TransactionDate <= asOfEnd)
            .GroupBy(t => t.InventoryItem.InventoryAccountId!.Value)
            .Select(g => new { Id = g.Key, Amt = g.Sum(t => t.TotalCost) })
            .ToDictionaryAsync(g => g.Id, g => g.Amt);

        // Sales Discounts contra-revenue account: invoice discounts and credit memo applications (DR).
        // Both reduce net revenue and are attributed to account 4950 as contra-revenue debits.
        // Credit memo applications are also added to AR credits below so the double-entry balances.
        var discountAcctId = await _context.Accounts
            .Where(a => a.CompanyId == companyId && a.AccountNumber == "4950" && a.IsActive && !a.IsDeleted)
            .Select(a => (int?)a.Id)
            .FirstOrDefaultAsync();
        discountAcctId ??= await _context.Accounts
            .Where(a => a.CompanyId == companyId && a.AccountType == AccountType.Revenue
                     && a.IsActive && !a.IsDeleted && a.Name.ToLower().Contains("discount"))
            .Select(a => (int?)a.Id)
            .FirstOrDefaultAsync();

        var cmApplied = await _context.CreditMemoApplications
            .Where(a => a.CompanyId == companyId && a.AppliedDate <= asOfEnd
                     && a.Invoice.Status != InvoiceStatus.Voided)
            .SumAsync(a => (decimal?)a.AmountApplied) ?? 0m;

        // Customer Credits (2350) model: a credit memo books DR Sales Discounts / CR Customer Credits on
        // issue, then DR Customer Credits / CR AR on apply. So the 4950 contra-revenue is the *issued*
        // amount (active memos in full + the applied portion of voided memos), and the 2350 liability is
        // the unapplied balance on active memos. AR is still credited by applications (cmApplied).
        var cmIssuedNonVoided = await _context.CreditMemos
            .Where(m => m.CompanyId == companyId && m.Status != CreditMemoStatus.Voided && m.IssueDate <= asOfEnd)
            .SumAsync(m => (decimal?)m.Amount) ?? 0m;
        var cmAppliedNonVoided = await _context.CreditMemoApplications
            .Where(a => a.CompanyId == companyId && a.AppliedDate <= asOfEnd
                     && a.Invoice.Status != InvoiceStatus.Voided
                     && a.CreditMemo.Status != CreditMemoStatus.Voided)
            .SumAsync(a => (decimal?)a.AmountApplied) ?? 0m;
        var cmContraRevenue   = cmIssuedNonVoided + (cmApplied - cmAppliedNonVoided); // DR 4950
        var customerCreditsAcctId = await _context.Accounts
            .Where(a => a.CompanyId == companyId && a.AccountNumber == "2350" && a.IsActive && !a.IsDeleted)
            .Select(a => (int?)a.Id).FirstOrDefaultAsync();

        var discountsByAcct = new Dictionary<int, decimal>();
        if (discountAcctId.HasValue)
        {
            var totalDiscounts = await _context.Invoices
                .Where(i => i.CompanyId == companyId && i.DiscountAmount > 0
                         && i.Status != InvoiceStatus.Draft
                         && i.Status != InvoiceStatus.Voided
                         && i.InvoiceDate <= asOfEnd)
                .SumAsync(i => (decimal?)i.DiscountAmount) ?? 0m;
            if (totalDiscounts + cmContraRevenue > 0)
                discountsByAcct[discountAcctId.Value] = totalDiscounts + cmContraRevenue;
        }

        // JE lines: posted entries debit/credit all account types
        var jeDebitsByAcct = await _context.JournalEntryLines
            .Where(l => l.CompanyId == companyId && l.JournalEntry.Status == JournalEntryStatus.Posted
                     && l.JournalEntry.EntryDate <= asOfEnd)
            .GroupBy(l => l.AccountId)
            .Select(g => new { Id = g.Key, Amt = g.Sum(l => l.DebitAmount) })
            .ToDictionaryAsync(g => g.Id, g => g.Amt);

        var jeCreditsByAcct = await _context.JournalEntryLines
            .Where(l => l.CompanyId == companyId && l.JournalEntry.Status == JournalEntryStatus.Posted
                     && l.JournalEntry.EntryDate <= asOfEnd)
            .GroupBy(l => l.AccountId)
            .Select(g => new { Id = g.Key, Amt = g.Sum(l => l.CreditAmount) })
            .ToDictionaryAsync(g => g.Id, g => g.Amt);

        // AR totals (single AR account assumed per standard small-business chart of accounts).
        // Credits include both cash payments and credit memo applications (which reduce open AR
        // when a customer credit is applied against a specific invoice).
        var arTotalDebits  = await _context.Invoices
            .Where(i => i.CompanyId == companyId && i.Status != InvoiceStatus.Draft && i.Status != InvoiceStatus.Voided
                     && i.InvoiceDate <= asOfEnd)
            .SumAsync(i => (decimal?)i.Total) ?? 0m;
        var arTotalCredits = await _context.Payments
            .Where(p => p.CompanyId == companyId && p.PaymentDate <= asOfEnd
                     && p.Invoice.Status != InvoiceStatus.Voided)
            .SumAsync(p => (decimal?)p.Amount) ?? 0m;
        arTotalCredits += cmApplied; // credit memo applications reduce AR balance
        // Gift-certificate redemptions credit AR too (DR 2500 / CR AR). Without this the redemption's
        // 2500 debit is recomputed but its AR credit is not, leaving the trial balance out of balance.
        var gcRedeemedTb = await _context.GiftCertificateRedemptions
            .Where(r => r.CompanyId == companyId && !r.IsDeleted && r.RedeemedDate <= asOfEnd
                     && r.Invoice.Status != InvoiceStatus.Voided)
            .SumAsync(r => (decimal?)r.AmountRedeemed) ?? 0m;
        arTotalCredits += gcRedeemedTb;

        // Cash refunds reverse the sale: revenue portion → DR Sales Returns (4960), tax portion →
        // DR Sales Tax Payable (relieves the liability), cash → CR bank (refundsByAcct below). They no
        // longer touch AR. Store-credit refunds post via CreditMemo, not the GL, so are excluded.
        var saleReversingRefunds = await _context.Refunds
            .Where(r => r.CompanyId == companyId && r.RefundDate <= asOfEnd && !r.IsDeleted && r.Invoice != null
                     && r.RefundMethod != PaymentMethod.StoreCredit)
            .Select(r => new { r.Amount, r.Invoice!.TaxAmount, r.Invoice.Total, r.Invoice.SalesTaxAccountId })
            .ToListAsync();

        decimal refundReturnsTotal = 0m;
        var refundTaxByAcct = new Dictionary<int, decimal>();
        foreach (var r in saleReversingRefunds)
        {
            var (returnsPortion, taxPortion) = RefundAllocation.Split(r.Amount, r.TaxAmount, r.Total);
            refundReturnsTotal += returnsPortion;
            if (taxPortion != 0m && r.SalesTaxAccountId.HasValue)
                refundTaxByAcct[r.SalesTaxAccountId.Value] = refundTaxByAcct.GetValueOrDefault(r.SalesTaxAccountId.Value) + taxPortion;
        }

        var salesReturnsAcctId = await _context.Accounts
            .Where(a => a.CompanyId == companyId && a.AccountNumber == "4960" && a.IsActive && !a.IsDeleted)
            .Select(a => (int?)a.Id).FirstOrDefaultAsync();

        // Refunds by bank account: money leaving the account (CR to checking/bank).
        var refundsByAcct = await _context.Refunds
            .Where(r => r.CompanyId == companyId && r.RefundDate <= asOfEnd && !r.IsDeleted && r.DepositAccountId != null)
            .GroupBy(r => r.DepositAccountId!.Value)
            .Select(g => new { Id = g.Key, Amt = g.Sum(r => r.Amount) })
            .ToDictionaryAsync(g => g.Id, g => g.Amt);

        // Deposits by bank account: cash received at deposit recording time (DR bank).
        // Deposit-sourced Payments have DepositAccountId = null, so there is no double-count with depositsByAcct.
        var depositsByAcctDep = await _context.Deposits
            .Where(d => d.CompanyId == companyId && !d.IsDeleted && d.DepositAccountId != null && d.ReceivedDate <= asOfEnd)
            .GroupBy(d => d.DepositAccountId!.Value)
            .Select(g => new { Id = g.Key, Amt = g.Sum(d => d.Amount) })
            .ToDictionaryAsync(g => g.Id, g => g.Amt);

        // Customer Deposits liability (2300): credits = all deposits taken; debits = deposits applied to invoices.
        var custDepositsAcctId = await _context.Accounts
            .Where(a => a.CompanyId == companyId && a.AccountNumber == "2300" && a.IsActive && !a.IsDeleted)
            .Select(a => (int?)a.Id).FirstOrDefaultAsync();
        var custDepositsCredits = custDepositsAcctId.HasValue
            ? (await _context.Deposits
                .Where(d => d.CompanyId == companyId && !d.IsDeleted && d.ReceivedDate <= asOfEnd)
                .SumAsync(d => (decimal?)d.Amount) ?? 0m) : 0m;
        var custDepositsDebits = custDepositsAcctId.HasValue
            ? (await _context.Deposits
                .Where(d => d.CompanyId == companyId && !d.IsDeleted && d.AppliedToInvoiceId != null && d.AppliedDate <= asOfEnd)
                .SumAsync(d => (decimal?)d.Amount) ?? 0m) : 0m;

        // Gift Certificate Liability (2500): balance driven by GC issuances, redemptions, and voids.
        var gcLiabilityAcctId = await _context.Accounts
            .Where(a => a.CompanyId == companyId && a.AccountNumber == "2500" && a.IsActive && !a.IsDeleted)
            .Select(a => (int?)a.Id).FirstOrDefaultAsync();
        var gcLiabilityCredits = gcLiabilityAcctId.HasValue
            ? (await _context.GiftCertificates
                .Where(gc => gc.CompanyId == companyId && !gc.IsDeleted && gc.IssueDate <= asOfEnd)
                .SumAsync(gc => (decimal?)gc.OriginalAmount) ?? 0m) : 0m;
        var gcLiabilityDebits = gcLiabilityAcctId.HasValue
            ? ((await _context.GiftCertificateRedemptions
                .Where(r => r.CompanyId == companyId && !r.IsDeleted && r.RedeemedDate <= asOfEnd)
                .SumAsync(r => (decimal?)r.AmountRedeemed) ?? 0m)
             + (await _context.GiftCertificates
                .Where(gc => gc.CompanyId == companyId && !gc.IsDeleted && gc.Status == GiftCertificateStatus.Voided
                          && gc.UpdatedAt <= asOfEnd && gc.OriginalAmount > gc.RedeemedAmount)
                .SumAsync(gc => (decimal?)(gc.OriginalAmount - gc.RedeemedAmount)) ?? 0m)) : 0m;

        // ── Per-account balance computation ─────────────────────────────────────────────────

        var accounts = await _context.Accounts
            .Where(a => a.CompanyId == companyId && a.IsActive)
            .OrderBy(a => a.AccountNumber)
            .ToListAsync();

        decimal ComputeAsOfBalance(Account a)
        {
            bool isDebitNormal = AccountingRules.IsNormalDebitBalance(a.AccountSubType);
            decimal debits = 0m, credits = 0m;

            if (a.AccountSubType == AccountSubType.AccountsReceivable)
            {
                debits  = arTotalDebits;
                credits = arTotalCredits;
            }
            else if (a.AccountSubType == AccountSubType.AccountsPayable)
            {
                credits  = billsByApAcct.GetValueOrDefault(a.Id);
                debits   = bpByApAcct.GetValueOrDefault(a.Id);
                debits  += vcByApAcct.GetValueOrDefault(a.Id); // vendor credit applications reduce AP
            }
            else
            {
                // All other accounts: sum contributions from each transaction source that can
                // post to this account. Dictionaries only contain entries for relevant account IDs,
                // so GetValueOrDefault returns 0 for sources that do not apply to this account type.
                debits  += depositsByAcct.GetValueOrDefault(a.Id);
                credits += expFromByAcct.GetValueOrDefault(a.Id);
                credits += bpFromByAcct.GetValueOrDefault(a.Id);
                credits += taxByAcct.GetValueOrDefault(a.Id);
                credits += revenueByAcct.GetValueOrDefault(a.Id);
                debits  += expenseByAcct.GetValueOrDefault(a.Id);
                debits  += billLinesByAcct.GetValueOrDefault(a.Id);
                debits  += discountsByAcct.GetValueOrDefault(a.Id);
                debits  += cogsConsumptionByAcct.GetValueOrDefault(a.Id);  // inventory consumption → DR COGS
                credits += invConsumptionByAcct.GetValueOrDefault(a.Id);   // inventory consumption → CR Inventory
                credits += refundsByAcct.GetValueOrDefault(a.Id);    // refunds reduce bank balance
                debits  += depositsByAcctDep.GetValueOrDefault(a.Id); // deposits increase bank balance
                if (salesReturnsAcctId.HasValue && a.Id == salesReturnsAcctId.Value)
                    debits += refundReturnsTotal;                    // revenue portion of cash refunds
                debits += refundTaxByAcct.GetValueOrDefault(a.Id);   // tax portion relieves the tax liability
                if (gcLiabilityAcctId.HasValue && a.Id == gcLiabilityAcctId.Value)
                {
                    credits += gcLiabilityCredits; // GC issued → CR liability
                    debits  += gcLiabilityDebits;  // redeemed/voided → DR liability
                }
                if (custDepositsAcctId.HasValue && a.Id == custDepositsAcctId.Value)
                {
                    credits += custDepositsCredits; // deposits taken → CR liability
                    debits  += custDepositsDebits;  // deposits applied → DR liability
                }
                if (customerCreditsAcctId.HasValue && a.Id == customerCreditsAcctId.Value)
                {
                    credits += cmIssuedNonVoided;   // credit memos issued → CR liability
                    debits  += cmAppliedNonVoided;  // applied → DR liability
                }
            }

            // Manual JEs apply to all account types (including AR/AP for unusual adjustments)
            debits  += jeDebitsByAcct.GetValueOrDefault(a.Id);
            credits += jeCreditsByAcct.GetValueOrDefault(a.Id);

            decimal opening = (a.OpeningBalanceDate == null || a.OpeningBalanceDate.Value.Date <= asOf)
                ? a.OpeningBalance : 0m;
            decimal net = isDebitNormal ? debits - credits : credits - debits;
            return opening + net;
        }

        var lines = new List<TrialBalanceLine>();
        foreach (var acct in accounts)
        {
            var balance = ComputeAsOfBalance(acct);
            if (balance == 0m) continue;

            var isDebitNormal = AccountingRules.IsNormalDebitBalance(acct.AccountSubType);
            var line = new TrialBalanceLine
            {
                AccountId     = acct.Id,
                AccountNumber = acct.AccountNumber,
                AccountName   = acct.Name,
                AccountType   = acct.AccountType
            };

            if (isDebitNormal)
            {
                // Normal debit: positive balance → Debit column; negative → Credit column (abnormal)
                if (balance >= 0m) line.DebitBalance  = balance;
                else               line.CreditBalance = -balance;
            }
            else
            {
                // Normal credit: positive balance → Credit column; negative → Debit column (abnormal)
                if (balance >= 0m) line.CreditBalance = balance;
                else               line.DebitBalance  = -balance;
            }

            lines.Add(line);
        }

        return new TrialBalanceDto
        {
            AsOf         = asOf,
            CompanyName  = companyName,
            Lines        = lines,
            TotalDebits  = lines.Sum(l => l.DebitBalance),
            TotalCredits = lines.Sum(l => l.CreditBalance),
        };
    }

    /// <inheritdoc/>
    public async Task<AccountingMethod> GetCompanyAccountingMethodAsync(int companyId)
    {
        if (companyId <= 0) return AccountingMethod.Accrual;
        var method = await _context.Companies
            .Where(c => c.Id == companyId)
            .Select(c => (AccountingMethod?)c.AccountingMethod)
            .FirstOrDefaultAsync();
        return method ?? AccountingMethod.Accrual;
    }

    /// <inheritdoc/>
    public async Task<CustomerStatementDto> GetCustomerStatementAsync(int companyId, int customerId, DateTime from, DateTime to)
    {
        var toEnd   = to.AddDays(1).AddTicks(-1);
        var fromEnd = from.AddTicks(-1);  // exclusive upper bound for pre-period queries

        var companyName = await GetCompanyNameAsync(companyId);

        var customer = await _context.Customers
            .Where(c => c.Id == customerId && c.CompanyId == companyId)
            .AsNoTracking().FirstOrDefaultAsync();
        if (customer == null) return new CustomerStatementDto { CompanyName = companyName, From = from, To = to };

        var customerName = customer.IsCommercial
            ? customer.CompanyName ?? string.Empty
            : $"{customer.ContactFirstName} {customer.ContactLastName}".Trim();

        var address = string.Join(", ", new[] { customer.Address, customer.City, customer.State, customer.ZipCode }
            .Where(s => !string.IsNullOrWhiteSpace(s)));

        // Opening balance: invoiced − paid before period start
        var preInvoiced = await _context.Invoices
            .Where(i => i.CompanyId == companyId && i.CustomerId == customerId
                     && i.Status != InvoiceStatus.Draft && i.Status != InvoiceStatus.Voided
                     && i.InvoiceDate < from)
            .SumAsync(i => (decimal?)i.Total) ?? 0;
        var prePaid = await _context.Payments
            .Where(p => p.CompanyId == companyId && p.Invoice.CustomerId == customerId
                     && p.Invoice.Status != InvoiceStatus.Voided
                     && p.PaymentDate < from)
            .SumAsync(p => (decimal?)p.Amount) ?? 0;
        var preCredits = await _context.CreditMemoApplications
            .Where(a => a.CompanyId == companyId && a.Invoice.CustomerId == customerId && a.AppliedDate < from)
            .SumAsync(a => (decimal?)a.AmountApplied) ?? 0;

        var openingBalance = preInvoiced - prePaid - preCredits;

        // In-period activity — gather, then sort, then compute running balance
        var lines = new List<StatementLineDto>();

        var periodInvoices = await _context.Invoices
            .Where(i => i.CompanyId == companyId && i.CustomerId == customerId
                     && i.Status != InvoiceStatus.Draft && i.Status != InvoiceStatus.Voided
                     && i.InvoiceDate >= from && i.InvoiceDate <= toEnd)
            .AsNoTracking().ToListAsync();

        foreach (var inv in periodInvoices)
            lines.Add(new StatementLineDto
            {
                Date        = inv.InvoiceDate,
                Type        = "Invoice",
                Reference   = inv.InvoiceNumber,
                Description = "Invoice",
                Debit       = inv.Total,
            });

        var periodPayments = await _context.Payments
            .Include(p => p.Invoice)
            .Where(p => p.CompanyId == companyId && p.Invoice.CustomerId == customerId
                     && p.Invoice.Status != InvoiceStatus.Voided
                     && p.PaymentDate >= from && p.PaymentDate <= toEnd)
            .AsNoTracking().ToListAsync();

        foreach (var pay in periodPayments)
            lines.Add(new StatementLineDto
            {
                Date        = pay.PaymentDate,
                Type        = "Payment",
                Reference   = pay.Invoice.InvoiceNumber,
                Description = pay.Notes ?? "Payment received",
                Credit      = pay.Amount,
            });

        var periodCredits = await _context.CreditMemoApplications
            .Include(a => a.Invoice)
            .Include(a => a.CreditMemo)
            .Where(a => a.CompanyId == companyId && a.Invoice.CustomerId == customerId
                     && a.AppliedDate >= from && a.AppliedDate <= toEnd)
            .AsNoTracking().ToListAsync();

        foreach (var cr in periodCredits)
            lines.Add(new StatementLineDto
            {
                Date        = cr.AppliedDate,
                Type        = "Credit Applied",
                Reference   = cr.Invoice?.InvoiceNumber ?? string.Empty,
                Description = $"Credit memo applied",
                Credit      = cr.AmountApplied,
            });

        // Sort by date then compute running balance
        lines = lines.OrderBy(l => l.Date).ThenBy(l => l.Type).ToList();
        var running = openingBalance;
        foreach (var line in lines)
        {
            running += (line.Debit ?? 0) - (line.Credit ?? 0);
            line.RunningBalance = running;
        }

        return new CustomerStatementDto
        {
            CustomerId      = customerId,
            CustomerName    = customerName,
            CustomerAddress = address,
            CompanyName     = companyName,
            From            = from,
            To              = to,
            OpeningBalance  = openingBalance,
            Lines           = lines,
            ClosingBalance  = running,
        };
    }

    /// <inheritdoc/>
    public async Task<VendorStatementDto> GetVendorStatementAsync(int companyId, int vendorId, DateTime from, DateTime to)
    {
        var toEnd = to.AddDays(1).AddTicks(-1);

        var companyName = await GetCompanyNameAsync(companyId);

        var vendor = await _context.Vendors
            .Where(v => v.Id == vendorId && v.CompanyId == companyId)
            .AsNoTracking().FirstOrDefaultAsync();
        if (vendor == null) return new VendorStatementDto { CompanyName = companyName, From = from, To = to };

        // Opening balance: bills − payments − credits before period start
        var preBills = await _context.Bills
            .Where(b => b.CompanyId == companyId && b.VendorId == vendorId
                     && b.Status != BillStatus.Draft && b.Status != BillStatus.Voided
                     && b.BillDate < from)
            .SumAsync(b => (decimal?)b.Total) ?? 0;
        var prePayments = await _context.BillPayments
            .Where(bp => bp.CompanyId == companyId && bp.Bill.VendorId == vendorId && bp.PaymentDate < from)
            .SumAsync(bp => (decimal?)bp.Amount) ?? 0;
        var preVcApplied = await _context.VendorCreditApplications
            .Where(vca => vca.CompanyId == companyId && vca.Bill.VendorId == vendorId && vca.AppliedDate < from)
            .SumAsync(vca => (decimal?)vca.Amount) ?? 0;

        var openingBalance = preBills - prePayments - preVcApplied;

        var lines = new List<StatementLineDto>();

        var periodBills = await _context.Bills
            .Where(b => b.CompanyId == companyId && b.VendorId == vendorId
                     && b.Status != BillStatus.Draft && b.Status != BillStatus.Voided
                     && b.BillDate >= from && b.BillDate <= toEnd)
            .AsNoTracking().ToListAsync();

        foreach (var bill in periodBills)
            lines.Add(new StatementLineDto
            {
                Date        = bill.BillDate,
                Type        = "Bill",
                Reference   = bill.BillNumber,
                Description = bill.Memo ?? "Vendor bill",
                Debit       = bill.Total,
            });

        var periodPayments = await _context.BillPayments
            .Include(bp => bp.Bill)
            .Where(bp => bp.CompanyId == companyId && bp.Bill.VendorId == vendorId
                      && bp.PaymentDate >= from && bp.PaymentDate <= toEnd)
            .AsNoTracking().ToListAsync();

        foreach (var pay in periodPayments)
            lines.Add(new StatementLineDto
            {
                Date        = pay.PaymentDate,
                Type        = "Payment",
                Reference   = pay.Bill.BillNumber,
                Description = pay.Memo ?? "Bill payment",
                Credit      = pay.Amount,
            });

        var periodVcApplied = await _context.VendorCreditApplications
            .Include(vca => vca.VendorCredit)
            .Include(vca => vca.Bill)
            .Where(vca => vca.CompanyId == companyId && vca.Bill.VendorId == vendorId
                       && vca.AppliedDate >= from && vca.AppliedDate <= toEnd)
            .AsNoTracking().ToListAsync();

        foreach (var vca in periodVcApplied)
            lines.Add(new StatementLineDto
            {
                Date        = vca.AppliedDate,
                Type        = "Credit Applied",
                Reference   = vca.VendorCredit.CreditNumber,
                Description = $"Vendor credit applied to {vca.Bill.BillNumber}",
                Credit      = vca.Amount,
            });

        lines = lines.OrderBy(l => l.Date).ThenBy(l => l.Type).ToList();
        var running = openingBalance;
        foreach (var line in lines)
        {
            running += (line.Debit ?? 0) - (line.Credit ?? 0);
            line.RunningBalance = running;
        }

        return new VendorStatementDto
        {
            VendorId       = vendorId,
            VendorName     = vendor.CompanyName,
            CompanyName    = companyName,
            From           = from,
            To             = to,
            OpeningBalance = openingBalance,
            Lines          = lines,
            ClosingBalance = running,
        };
    }

    /// <inheritdoc/>
    /// <summary>
    /// Computes a Cash Flow Statement for the given period using the direct (cash-basis) method
    /// for operating activities:
    /// <list type="bullet">
    ///   <item><b>CashFromCustomers</b> — sum of <see cref="Payment"/> amounts in the period.</item>
    ///   <item><b>CashToVendors</b> — sum of <see cref="BillPayment"/> amounts in the period.</item>
    ///   <item><b>CashForExpenses</b> — sum of <see cref="Expense"/> amounts in the period.</item>
    /// </list>
    /// BeginningCash is derived by summing all Payment inflows minus BillPayment and Expense outflows
    /// prior to <paramref name="from"/>. This is an approximation when cash accounts have
    /// an OpeningBalance; it is the most accurate representation available without a dedicated
    /// cash-tracking journal.
    /// Investing and Financing sections are populated from the expense/asset account ledger
    /// (FixedAsset purchases from Expense entries whose account is FixedAsset subtype) and
    /// equity account changes respectively.
    /// </summary>
    public async Task<CashFlowStatementDto> GetCashFlowStatementAsync(int companyId, DateTime from, DateTime to)
    {
        var toEnd = to.Date.AddDays(1).AddTicks(-1);
        var companyName = await GetCompanyNameAsync(companyId);
        var method = await GetCompanyAccountingMethodAsync(companyId);

        // ── Operating — direct / cash ──────────────────────────────────────
        var cashFromCustomers = await _context.Payments
            .IgnoreQueryFilters()
            .Where(p => p.CompanyId == companyId && !p.IsDeleted
                     && p.PaymentDate >= from && p.PaymentDate <= toEnd)
            .SumAsync(p => (decimal?)p.Amount) ?? 0m;

        var cashToVendors = await _context.BillPayments
            .IgnoreQueryFilters()
            .Where(bp => bp.CompanyId == companyId && !bp.IsDeleted
                      && bp.PaymentDate >= from && bp.PaymentDate <= toEnd)
            .SumAsync(bp => (decimal?)bp.Amount) ?? 0m;

        var cashForExpenses = await _context.Expenses
            .IgnoreQueryFilters()
            .Where(e => e.CompanyId == companyId && !e.IsDeleted
                     && e.Date >= from && e.Date <= toEnd)
            .SumAsync(e => (decimal?)e.Amount) ?? 0m;

        // ── Investing — fixed-asset purchases from Expense entries ─────────
        var fixedAssetAccountIds = await _context.Accounts
            .IgnoreQueryFilters()
            .Where(a => a.CompanyId == companyId && !a.IsDeleted
                     && a.AccountSubType == AccountSubType.FixedAsset)
            .Select(a => a.Id)
            .ToListAsync();

        var capEx = fixedAssetAccountIds.Count > 0
            ? (await _context.Expenses
                .IgnoreQueryFilters()
                .Where(e => e.CompanyId == companyId && !e.IsDeleted
                         && e.Date >= from && e.Date <= toEnd
                         && fixedAssetAccountIds.Contains(e.ExpenseAccountId))
                .SumAsync(e => (decimal?)e.Amount) ?? 0m)
            : 0m;

        var investingLines = new List<CashFlowLineDto>();
        if (capEx != 0m)
            investingLines.Add(new CashFlowLineDto { Label = "Capital Expenditures", Amount = -capEx });

        // ── Financing — placeholder (equity changes not explicitly tracked) ─
        var financingLines = new List<CashFlowLineDto>();

        // ── Beginning cash ─────────────────────────────────────────────────
        // Cash account opening balances + pre-period payments in - pre-period payments out
        var cashAccountOpeningBalance = await _context.Accounts
            .IgnoreQueryFilters()
            .Where(a => a.CompanyId == companyId && !a.IsDeleted
                     && (a.AccountSubType == AccountSubType.Cash
                      || a.AccountSubType == AccountSubType.Checking
                      || a.AccountSubType == AccountSubType.Savings))
            .SumAsync(a => (decimal?)a.OpeningBalance) ?? 0m;

        var prePaymentsIn = await _context.Payments
            .IgnoreQueryFilters()
            .Where(p => p.CompanyId == companyId && !p.IsDeleted && p.PaymentDate < from)
            .SumAsync(p => (decimal?)p.Amount) ?? 0m;

        var preBillPaymentsOut = await _context.BillPayments
            .IgnoreQueryFilters()
            .Where(bp => bp.CompanyId == companyId && !bp.IsDeleted && bp.PaymentDate < from)
            .SumAsync(bp => (decimal?)bp.Amount) ?? 0m;

        var preExpensesOut = await _context.Expenses
            .IgnoreQueryFilters()
            .Where(e => e.CompanyId == companyId && !e.IsDeleted && e.Date < from)
            .SumAsync(e => (decimal?)e.Amount) ?? 0m;

        var beginningCash = cashAccountOpeningBalance + prePaymentsIn - preBillPaymentsOut - preExpensesOut;

        return new CashFlowStatementDto
        {
            CompanyName       = companyName,
            From              = from,
            To                = to,
            Method            = method,
            CashFromCustomers = cashFromCustomers,
            CashToVendors     = cashToVendors,
            CashForExpenses   = cashForExpenses,
            InvestingLines    = investingLines,
            FinancingLines    = financingLines,
            BeginningCash     = beginningCash,
        };
    }

    /// <summary>
    /// Looks up the company name by ID for report headers and AI prompt injection.
    /// Falls back to "Your Company" if the record is not found.
    /// </summary>
    private async Task<string> GetCompanyNameAsync(int companyId)
    {
        if (companyId <= 0) return "Your Company";
        var company = await _context.Companies.FirstOrDefaultAsync(c => c.Id == companyId);
        return company?.CompanyName ?? "Your Company";
    }
}
