using CashLineERP.Application.Reporting;
using CashLineERP.Domain.Finance;
using CashLineERP.Domain.IslamicFinance.Facilities;
using CashLineERP.Domain.IslamicFinance.PartnerLenders;
using CashLineERP.Domain.Parties;
using CashLineERP.Domain.Reporting;
using CashLineERP.SharedKernel.Finance;
using CashLineERP.SharedKernel.IslamicFinance;
using CashLineERP.SharedKernel.Workflow;
using Microsoft.EntityFrameworkCore;

namespace CashLineERP.Infrastructure.Persistence.Repositories;

public sealed class CoreReportingRepository : ICoreReportingRepository
{
    private readonly CashLineErpDbContext _dbContext;

    public CoreReportingRepository(CashLineErpDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public CoreFinanceReportView GetCoreFinanceReport(DateOnly? dateFrom, DateOnly? dateTo)
    {
        var range = NormalizeDateRange(dateFrom, dateTo);
        var conversion = CurrencyConversionContext.Create(_dbContext);
        var partiesById = _dbContext.BusinessParties
            .AsNoTracking()
            .ToArray()
            .ToDictionary(party => party.Id, party => party.PartyCode);

        var postedApBills = _dbContext.AccountsPayableBills
            .AsNoTracking()
            .ToArray()
            .Where(bill => bill.BillDate >= range.DateFrom && bill.BillDate <= range.DateTo && bill.StatusId == WorkflowStatusSeedIds.Posted)
            .ToArray();

        var postedApPayments = _dbContext.AccountsPayablePayments
            .AsNoTracking()
            .ToArray()
            .Where(payment => payment.PaymentDate >= range.DateFrom && payment.PaymentDate <= range.DateTo && payment.StatusId == WorkflowStatusSeedIds.Posted)
            .ToArray();

        var postedArInvoices = _dbContext.AccountsReceivableInvoices
            .AsNoTracking()
            .ToArray()
            .Where(invoice => invoice.InvoiceDate >= range.DateFrom && invoice.InvoiceDate <= range.DateTo && invoice.StatusId == WorkflowStatusSeedIds.Posted)
            .ToArray();

        var postedArReceipts = _dbContext.AccountsReceivableReceipts
            .AsNoTracking()
            .ToArray()
            .Where(receipt => receipt.ReceiptDate >= range.DateFrom && receipt.ReceiptDate <= range.DateTo && receipt.StatusId == WorkflowStatusSeedIds.Posted)
            .ToArray();

        var generalLedgerBatches = _dbContext.GeneralLedgerBatches
            .AsNoTracking()
            .Include(batch => batch.Journals)
            .ToArray()
            .Where(batch => batch.Journals.Any(journal => journal.PostingDate >= range.DateFrom && journal.PostingDate <= range.DateTo))
            .ToArray();

        var pendingEtaInvoices = postedArInvoices
            .Where(invoice =>
                CashLineERP.Application.Tax.EtaPayloadBuilder.SupportsInvoiceType(invoice.InvoiceType) &&
                !string.Equals(invoice.EtaStatus, "ACCEPTED", StringComparison.OrdinalIgnoreCase))
            .ToArray();

        var rows = new List<CoreFinanceReportRowView>();
        rows.AddRange(postedApBills.Select(bill => new CoreFinanceReportRowView(
            bill.BillDate,
            "AP",
            bill.IsDebitNote ? "AP_DEBIT_NOTE" : "AP_BILL",
            bill.BillNumber,
            partiesById.GetValueOrDefault(bill.SupplierId),
            bill.CurrencyCode,
            conversion.BaseCurrencyCode,
            conversion.ResolveRateToBase(bill.CurrencyCode, bill.BillDate),
            conversion.ConvertToBase(bill.CurrencyCode, bill.TotalAmount, bill.BillDate),
            WorkflowStatusSeedIds.ToCode(bill.StatusId),
            bill.TotalAmount,
            bill.MatchNotes)));
        rows.AddRange(postedApPayments.Select(payment => new CoreFinanceReportRowView(
            payment.PaymentDate,
            "AP",
            "AP_PAYMENT",
            payment.PaymentNumber,
            partiesById.GetValueOrDefault(payment.SupplierId),
            payment.CurrencyCode,
            conversion.BaseCurrencyCode,
            conversion.ResolveRateToBase(payment.CurrencyCode, payment.PaymentDate),
            conversion.ConvertToBase(payment.CurrencyCode, payment.AmountAmount, payment.PaymentDate),
            WorkflowStatusSeedIds.ToCode(payment.StatusId),
            payment.AmountAmount,
            payment.ReferenceNumber)));
        rows.AddRange(postedArInvoices.Select(invoice => new CoreFinanceReportRowView(
            invoice.InvoiceDate,
            "AR",
            invoice.InvoiceType,
            invoice.InvoiceNumber,
            partiesById.GetValueOrDefault(invoice.CustomerId),
            invoice.CurrencyCode,
            conversion.BaseCurrencyCode,
            conversion.ResolveRateToBase(invoice.CurrencyCode, invoice.InvoiceDate),
            conversion.ConvertToBase(invoice.CurrencyCode, invoice.TotalAmount, invoice.InvoiceDate),
            WorkflowStatusSeedIds.ToCode(invoice.StatusId),
            invoice.TotalAmount,
            invoice.EtaStatus)));
        rows.AddRange(postedArReceipts.Select(receipt => new CoreFinanceReportRowView(
            receipt.ReceiptDate,
            "AR",
            "AR_RECEIPT",
            receipt.ReceiptNumber,
            partiesById.GetValueOrDefault(receipt.CustomerId),
            receipt.CurrencyCode,
            conversion.BaseCurrencyCode,
            conversion.ResolveRateToBase(receipt.CurrencyCode, receipt.ReceiptDate),
            conversion.ConvertToBase(receipt.CurrencyCode, receipt.AmountAmount, receipt.ReceiptDate),
            WorkflowStatusSeedIds.ToCode(receipt.StatusId),
            receipt.AmountAmount,
            receipt.ReferenceNumber)));
        rows.AddRange(generalLedgerBatches.Select(batch => new CoreFinanceReportRowView(
            batch.Journals.OrderBy(journal => journal.PostingDate).First().PostingDate,
            "GL",
            batch.BatchType,
            batch.BatchNumber,
            null,
            conversion.BaseCurrencyCode,
            conversion.BaseCurrencyCode,
            1m,
            batch.BatchTotalAmount,
            WorkflowStatusSeedIds.ToCode(batch.StatusId),
            batch.BatchTotalAmount,
            batch.DescriptionText)));
        rows.AddRange(pendingEtaInvoices.Select(invoice => new CoreFinanceReportRowView(
            invoice.TaxDate ?? invoice.InvoiceDate,
            "ETA",
            "ETA_PENDING",
            invoice.InvoiceNumber,
            partiesById.GetValueOrDefault(invoice.CustomerId),
            invoice.CurrencyCode,
            conversion.BaseCurrencyCode,
            conversion.ResolveRateToBase(invoice.CurrencyCode, invoice.TaxDate ?? invoice.InvoiceDate),
            conversion.ConvertToBase(invoice.CurrencyCode, invoice.TotalAmount, invoice.TaxDate ?? invoice.InvoiceDate),
            invoice.EtaStatus ?? "PENDING",
            invoice.TotalAmount,
            invoice.EtaUuid)));

        return new CoreFinanceReportView(
            range.DateFrom,
            range.DateTo,
            conversion.BaseCurrencyCode,
            postedApBills.Length,
            postedApBills.Sum(bill => conversion.ConvertToBase(bill.CurrencyCode, bill.TotalAmount, bill.BillDate)),
            postedApPayments.Length,
            postedApPayments.Sum(payment => conversion.ConvertToBase(payment.CurrencyCode, payment.AmountAmount, payment.PaymentDate)),
            postedArInvoices.Length,
            postedArInvoices.Sum(invoice => conversion.ConvertToBase(invoice.CurrencyCode, invoice.TotalAmount, invoice.InvoiceDate)),
            postedArReceipts.Length,
            postedArReceipts.Sum(receipt => conversion.ConvertToBase(receipt.CurrencyCode, receipt.AmountAmount, receipt.ReceiptDate)),
            generalLedgerBatches.Length,
            generalLedgerBatches.Sum(batch => batch.BatchTotalAmount),
            pendingEtaInvoices.Length,
            pendingEtaInvoices.Sum(invoice => conversion.ConvertToBase(invoice.CurrencyCode, invoice.TotalAmount, invoice.TaxDate ?? invoice.InvoiceDate)),
            rows.OrderBy(row => row.BusinessDate).ThenBy(row => row.ReportSection).ThenBy(row => row.DocumentNumber).ToArray());
    }

    public CoreOperationsReportView GetCoreOperationsReport(DateOnly? dateFrom, DateOnly? dateTo)
    {
        var range = NormalizeDateRange(dateFrom, dateTo);
        var partiesById = _dbContext.BusinessParties
            .AsNoTracking()
            .ToArray()
            .ToDictionary(party => party.Id, party => party.PartyCode);

        var requisitions = _dbContext.PurchaseRequisitions.AsNoTracking().Include(entry => entry.Lines).ToArray()
            .Where(entry => entry.RequisitionDate >= range.DateFrom && entry.RequisitionDate <= range.DateTo)
            .ToArray();
        var rfqs = _dbContext.RequestForQuotations.AsNoTracking().Include(entry => entry.Lines).ToArray()
            .Where(entry => entry.RfqDate >= range.DateFrom && entry.RfqDate <= range.DateTo)
            .ToArray();
        var purchaseOrders = _dbContext.PurchaseOrders.AsNoTracking().Include(entry => entry.Lines).ToArray()
            .Where(entry => entry.PurchaseOrderDate >= range.DateFrom && entry.PurchaseOrderDate <= range.DateTo)
            .ToArray();
        var goodsReceipts = _dbContext.GoodsReceipts.AsNoTracking().Include(entry => entry.Lines).ToArray()
            .Where(entry => DateOnly.FromDateTime(entry.ReceivedAtUtc) >= range.DateFrom && DateOnly.FromDateTime(entry.ReceivedAtUtc) <= range.DateTo)
            .ToArray();
        var purchaseReturns = _dbContext.PurchaseReturns.AsNoTracking().Include(entry => entry.Lines).ToArray()
            .Where(entry => entry.ReturnDate >= range.DateFrom && entry.ReturnDate <= range.DateTo)
            .ToArray();
        var salesOrders = _dbContext.SalesOrders.AsNoTracking().Include(entry => entry.Lines).ToArray()
            .Where(entry => entry.OrderDate >= range.DateFrom && entry.OrderDate <= range.DateTo)
            .ToArray();
        var dispatches = _dbContext.DispatchOrders.AsNoTracking().Include(entry => entry.Lines).ToArray()
            .Where(entry =>
            {
                var activityDate = entry.ShippedAtUtc.HasValue
                    ? DateOnly.FromDateTime(entry.ShippedAtUtc.Value)
                    : entry.PlannedShipAtUtc.HasValue
                        ? DateOnly.FromDateTime(entry.PlannedShipAtUtc.Value)
                        : (DateOnly?)null;
                return activityDate.HasValue && activityDate.Value >= range.DateFrom && activityDate.Value <= range.DateTo;
            })
            .ToArray();
        var salesReturns = _dbContext.SalesReturns.AsNoTracking().Include(entry => entry.Lines).ToArray()
            .Where(entry => entry.ReturnDate >= range.DateFrom && entry.ReturnDate <= range.DateTo)
            .ToArray();
        var adjustments = _dbContext.InventoryAdjustments.AsNoTracking().Include(entry => entry.Lines).ToArray()
            .Where(entry => entry.AdjustmentDate >= range.DateFrom && entry.AdjustmentDate <= range.DateTo)
            .ToArray();
        var stockCounts = _dbContext.StockCounts.AsNoTracking().Include(entry => entry.Lines).ToArray()
            .Where(entry => entry.CountDate >= range.DateFrom && entry.CountDate <= range.DateTo)
            .ToArray();
        var transfers = _dbContext.WarehouseTransfers.AsNoTracking().Include(entry => entry.Lines).ToArray()
            .Where(entry => entry.TransferDate >= range.DateFrom && entry.TransferDate <= range.DateTo)
            .ToArray();

        var rows = new List<CoreOperationsReportRowView>();
        rows.AddRange(requisitions.Select(entry => new CoreOperationsReportRowView(
            entry.RequisitionDate,
            "PURCHASE_REQUISITION",
            entry.RequisitionNumber,
            entry.BranchId,
            null,
            WorkflowStatusSeedIds.ToCode(entry.StatusId),
            entry.Lines.Count,
            entry.Lines.Sum(line => line.RequestedQuantity),
            entry.EstimatedTotalAmount,
            entry.PurposeText)));
        rows.AddRange(rfqs.Select(entry => new CoreOperationsReportRowView(
            entry.RfqDate,
            "RFQ",
            entry.RfqNumber,
            entry.BranchId,
            null,
            WorkflowStatusSeedIds.ToCode(entry.StatusId),
            entry.Lines.Count,
            entry.Lines.Sum(line => line.RequestedQuantity),
            entry.Lines.Sum(line => line.RequestedQuantity * (line.TargetUnitPrice ?? 0m)),
            entry.Notes)));
        rows.AddRange(purchaseOrders.Select(entry => new CoreOperationsReportRowView(
            entry.PurchaseOrderDate,
            "PURCHASE_ORDER",
            entry.PurchaseOrderNumber,
            entry.BranchId,
            partiesById.GetValueOrDefault(entry.SupplierId),
            WorkflowStatusSeedIds.ToCode(entry.StatusId),
            entry.Lines.Count,
            entry.Lines.Sum(line => line.OrderedQuantity),
            entry.TotalAmount,
            null)));
        rows.AddRange(goodsReceipts.Select(entry => new CoreOperationsReportRowView(
            DateOnly.FromDateTime(entry.ReceivedAtUtc),
            "GOODS_RECEIPT",
            entry.GoodsReceiptNumber,
            entry.BranchId,
            partiesById.GetValueOrDefault(entry.SupplierId),
            WorkflowStatusSeedIds.ToCode(entry.StatusId),
            entry.Lines.Count,
            entry.Lines.Sum(line => line.QuantityReceived),
            null,
            entry.InspectionStatus)));
        rows.AddRange(purchaseReturns.Select(entry => new CoreOperationsReportRowView(
            entry.ReturnDate,
            "PURCHASE_RETURN",
            entry.PurchaseReturnNumber,
            entry.BranchId,
            partiesById.GetValueOrDefault(entry.SupplierId),
            WorkflowStatusSeedIds.ToCode(entry.StatusId),
            entry.Lines.Count,
            entry.TotalReturnedQuantity,
            null,
            entry.ReasonCode)));
        rows.AddRange(salesOrders.Select(entry => new CoreOperationsReportRowView(
            entry.OrderDate,
            "SALES_ORDER",
            entry.SalesOrderNumber,
            entry.BranchId,
            partiesById.GetValueOrDefault(entry.CustomerId),
            WorkflowStatusSeedIds.ToCode(entry.StatusId),
            entry.Lines.Count,
            entry.Lines.Sum(line => line.OrderedQuantity),
            entry.TotalAmount,
            entry.CreditStatusCode)));
        rows.AddRange(dispatches.Select(entry => new CoreOperationsReportRowView(
            entry.ShippedAtUtc.HasValue
                ? DateOnly.FromDateTime(entry.ShippedAtUtc.Value)
                : DateOnly.FromDateTime(entry.PlannedShipAtUtc!.Value),
            "DISPATCH_ORDER",
            entry.DispatchNumber,
            entry.BranchId,
            null,
            WorkflowStatusSeedIds.ToCode(entry.StatusId),
            entry.Lines.Count,
            entry.Lines.Sum(line => line.QuantityDispatched),
            null,
            entry.ProofOfDeliveryReceiver)));
        rows.AddRange(salesReturns.Select(entry => new CoreOperationsReportRowView(
            entry.ReturnDate,
            "SALES_RETURN",
            entry.SalesReturnNumber,
            entry.BranchId,
            partiesById.GetValueOrDefault(entry.CustomerId),
            WorkflowStatusSeedIds.ToCode(entry.StatusId),
            entry.Lines.Count,
            entry.TotalReturnedQuantity,
            null,
            entry.ReasonCode)));
        rows.AddRange(adjustments.Select(entry => new CoreOperationsReportRowView(
            entry.AdjustmentDate,
            "INVENTORY_ADJUSTMENT",
            entry.AdjustmentNumber,
            entry.BranchId,
            null,
            WorkflowStatusSeedIds.ToCode(entry.StatusId),
            entry.Lines.Count,
            entry.Lines.Sum(line => Math.Abs(line.QuantityDelta)),
            entry.TotalAdjustmentAmount,
            entry.ReasonCode)));
        rows.AddRange(stockCounts.Select(entry => new CoreOperationsReportRowView(
            entry.CountDate,
            "STOCK_COUNT",
            entry.CountNumber,
            entry.BranchId,
            null,
            WorkflowStatusSeedIds.ToCode(entry.StatusId),
            entry.Lines.Count,
            entry.Lines.Sum(line => Math.Abs(line.VarianceQuantity)),
            entry.TotalVarianceAmount,
            entry.CountTypeCode)));
        rows.AddRange(transfers.Select(entry => new CoreOperationsReportRowView(
            entry.TransferDate,
            "WAREHOUSE_TRANSFER",
            entry.TransferNumber,
            entry.BranchId,
            null,
            WorkflowStatusSeedIds.ToCode(entry.StatusId),
            entry.Lines.Count,
            entry.TotalTransferQuantity,
            entry.Lines.Sum(line => line.QuantityTransferred * (line.UnitCost ?? 0m)),
            entry.ReasonCode)));

        return new CoreOperationsReportView(
            range.DateFrom,
            range.DateTo,
            requisitions.Length,
            rfqs.Length,
            purchaseOrders.Length,
            goodsReceipts.Length,
            purchaseReturns.Length,
            salesOrders.Length,
            dispatches.Length,
            salesReturns.Length,
            adjustments.Length,
            stockCounts.Length,
            transfers.Length,
            goodsReceipts.Sum(entry => entry.Lines.Sum(line => line.QuantityReceived)),
            dispatches.Sum(entry => entry.Lines.Sum(line => line.QuantityDispatched)),
            purchaseReturns.Sum(entry => entry.TotalReturnedQuantity) + salesReturns.Sum(entry => entry.TotalReturnedQuantity),
            adjustments.Sum(entry => entry.Lines.Sum(line => Math.Abs(line.QuantityDelta))) + stockCounts.Sum(entry => entry.Lines.Sum(line => Math.Abs(line.VarianceQuantity))),
            transfers.Sum(entry => entry.TotalTransferQuantity),
            rows.OrderBy(row => row.ActivityDate).ThenBy(row => row.ActivityType).ThenBy(row => row.DocumentNumber).ToArray());
    }

    public IslamicFinancePipelineReportView GetIslamicFinancePipelineReport(DateOnly? dateFrom, DateOnly? dateTo, long? partnerLenderId = null)
    {
        var range = NormalizeDateRange(dateFrom, dateTo);
        var partiesById = _dbContext.BusinessParties
            .AsNoTracking()
            .ToArray()
            .ToDictionary(party => party.Id);
        var productsById = _dbContext.IslamicFinanceProductProfiles
            .AsNoTracking()
            .ToArray()
            .ToDictionary(product => product.Id);
        var partnersById = _dbContext.IslamicFinancePartnerLenders
            .AsNoTracking()
            .ToArray()
            .ToDictionary(partner => partner.Id);
        var facilitiesById = _dbContext.IslamicFinanceFacilities
            .AsNoTracking()
            .ToArray()
            .ToDictionary(facility => facility.Id);

        static bool IsWithinRange(DateTime createdAtUtc, DateOnly dateFromValue, DateOnly dateToValue)
        {
            var activityDate = DateOnly.FromDateTime(createdAtUtc);
            return activityDate >= dateFromValue && activityDate <= dateToValue;
        }

        var partnerLenders = partnersById.Values
            .Where(entry => IsWithinRange(entry.CreatedAtUtc, range.DateFrom, range.DateTo))
            .Where(entry => !partnerLenderId.HasValue || entry.Id == partnerLenderId.Value)
            .OrderBy(entry => entry.PartnerLenderPartyId)
            .ToArray();

        var facilities = _dbContext.IslamicFinanceFacilities
            .AsNoTracking()
            .ToArray()
            .Where(entry => IsWithinRange(entry.CreatedAtUtc, range.DateFrom, range.DateTo))
            .Where(entry => !partnerLenderId.HasValue || entry.PartnerLenderId == partnerLenderId.Value)
            .OrderBy(entry => entry.FacilityNumber)
            .ToArray();

        var requests = _dbContext.IslamicFinanceRequests
            .AsNoTracking()
            .ToArray()
            .Where(entry => IsWithinRange(entry.CreatedAtUtc, range.DateFrom, range.DateTo))
            .Where(entry =>
                !partnerLenderId.HasValue
                || entry.AssignedPartnerLenderId == partnerLenderId.Value
                || (entry.FacilityId.HasValue
                    && facilitiesById.TryGetValue(entry.FacilityId.Value, out var facility)
                    && facility.PartnerLenderId == partnerLenderId.Value))
            .OrderBy(entry => entry.RequestNumber)
            .ToArray();

        var murabahaCases = _dbContext.IslamicFinanceMurabahaCases
            .AsNoTracking()
            .ToArray()
            .Where(entry => IsWithinRange(entry.CreatedAtUtc, range.DateFrom, range.DateTo))
            .Where(entry => !partnerLenderId.HasValue || entry.PartnerLenderId == partnerLenderId.Value)
            .OrderBy(entry => entry.CaseNumber)
            .ToArray();

        var rows = new List<IslamicFinancePipelineReportRowView>();

        rows.AddRange(partnerLenders.Select(entry =>
        {
            var party = partiesById.GetValueOrDefault(entry.PartnerLenderPartyId);
            return new IslamicFinancePipelineReportRowView(
                DateOnly.FromDateTime(entry.CreatedAtUtc),
                "PARTNER",
                party?.PartyCode ?? $"PARTNER-{entry.Id}",
                entry.Id,
                entry.Id,
                party?.PartyCode,
                party?.PartyName,
                null,
                null,
                null,
                null,
                null,
                null,
                entry.DefaultCurrencyCode,
                entry.StatusCode,
                0m,
                null,
                BuildPartnerSupportText(entry.SupportsMurabaha, entry.SupportsReceivablesFinance, entry.SupportsTawarruq));
        }));

        rows.AddRange(facilities.Select(entry =>
        {
            var partner = partnersById.GetValueOrDefault(entry.PartnerLenderId);
            var partnerParty = partner is null ? null : partiesById.GetValueOrDefault(partner.PartnerLenderPartyId);
            var customer = partiesById.GetValueOrDefault(entry.CustomerPartyId);
            var product = productsById.GetValueOrDefault(entry.ProductProfileId);
            return new IslamicFinancePipelineReportRowView(
                DateOnly.FromDateTime(entry.CreatedAtUtc),
                "FACILITY",
                entry.FacilityNumber,
                entry.Id,
                entry.PartnerLenderId,
                partnerParty?.PartyCode,
                partnerParty?.PartyName,
                entry.CustomerPartyId,
                customer?.PartyCode,
                customer?.PartyName,
                entry.ProductProfileId,
                product?.ProductCode,
                product?.ProductName,
                entry.CurrencyCode,
                entry.StatusCode,
                entry.ApprovedLimitAmount,
                entry.UtilizedAmount,
                $"Available {entry.AvailableAmount()}");
        }));

        rows.AddRange(requests.Select(entry =>
        {
            CashLineERP.Domain.IslamicFinance.PartnerLenders.IslamicFinancePartnerLender? requestPartner = null;
            if (entry.AssignedPartnerLenderId.HasValue)
            {
                partnersById.TryGetValue(entry.AssignedPartnerLenderId.Value, out requestPartner);
            }
            else if (entry.FacilityId.HasValue && facilitiesById.TryGetValue(entry.FacilityId.Value, out var facility))
            {
                partnersById.TryGetValue(facility.PartnerLenderId, out requestPartner);
            }

            var partnerParty = requestPartner is null ? null : partiesById.GetValueOrDefault(requestPartner.PartnerLenderPartyId);
            var customer = partiesById.GetValueOrDefault(entry.CustomerPartyId);
            var product = productsById.GetValueOrDefault(entry.ProductProfileId);
            return new IslamicFinancePipelineReportRowView(
                DateOnly.FromDateTime(entry.CreatedAtUtc),
                "REQUEST",
                entry.RequestNumber,
                entry.Id,
                entry.AssignedPartnerLenderId,
                partnerParty?.PartyCode,
                partnerParty?.PartyName,
                entry.CustomerPartyId,
                customer?.PartyCode,
                customer?.PartyName,
                entry.ProductProfileId,
                product?.ProductCode,
                product?.ProductName,
                entry.TransactionCurrencyCode,
                entry.StatusCode,
                entry.RequestedAmount,
                null,
                entry.SourceDocumentType);
        }));

        rows.AddRange(murabahaCases.Select(entry =>
        {
            var partner = partnersById.GetValueOrDefault(entry.PartnerLenderId);
            var partnerParty = partner is null ? null : partiesById.GetValueOrDefault(partner.PartnerLenderPartyId);
            var customer = partiesById.GetValueOrDefault(entry.CustomerPartyId);
            var product = productsById.GetValueOrDefault(entry.ProductProfileId);
            return new IslamicFinancePipelineReportRowView(
                DateOnly.FromDateTime(entry.CreatedAtUtc),
                "MURABAHA",
                entry.CaseNumber,
                entry.Id,
                entry.PartnerLenderId,
                partnerParty?.PartyCode,
                partnerParty?.PartyName,
                entry.CustomerPartyId,
                customer?.PartyCode,
                customer?.PartyName,
                entry.ProductProfileId,
                product?.ProductCode,
                product?.ProductName,
                entry.TransactionCurrencyCode,
                entry.StatusCode,
                entry.SalePriceAmount,
                entry.ProfitAmount,
                $"Purchase cost {entry.PurchaseCostAmount}");
        }));

        return new IslamicFinancePipelineReportView(
            range.DateFrom,
            range.DateTo,
            partnerLenders.Length,
            partnerLenders.Count(entry => entry.IsActive),
            facilities.Length,
            facilities.Count(entry => entry.IsActive),
            requests.Length,
            requests.Count(entry => entry.StatusCode == "APPROVED" || entry.StatusCode == "ASSIGNED"),
            murabahaCases.Length,
            murabahaCases.Count(entry => entry.StatusCode == "ACTIVE"),
            facilities.Sum(entry => entry.ApprovedLimitAmount),
            facilities.Sum(entry => entry.UtilizedAmount),
            requests.Sum(entry => entry.RequestedAmount),
            murabahaCases.Sum(entry => entry.SalePriceAmount),
            rows.OrderBy(entry => entry.ActivityDate).ThenBy(entry => entry.StageCode).ThenBy(entry => entry.RecordNumber).ToArray());
    }

    public FinancialStatementReportView GetFinancialStatementReport(string statementCode, DateOnly? dateFrom, DateOnly? dateTo, DateOnly? asOfDate)
    {
        var normalizedStatementCode = FinancialStatementCatalog.NormalizeStatementCode(statementCode);
        return normalizedStatementCode switch
        {
            FinancialStatementCatalog.BalanceSheet => BuildBalanceSheetReport(asOfDate ?? dateTo),
            FinancialStatementCatalog.IncomeStatement => BuildIncomeStatementReport(dateFrom, dateTo),
            FinancialStatementCatalog.CashFlow => BuildCashFlowReport(dateFrom, dateTo),
            FinancialStatementCatalog.TrialBalance => BuildTrialBalanceReport(dateFrom, dateTo, asOfDate),
            FinancialStatementCatalog.AccountingEntries => BuildAccountingEntriesReport(dateFrom, dateTo),
            _ => throw new InvalidOperationException($"Unsupported financial statement code '{statementCode}'.")
        };
    }

    public AgingReportView GetAccountsPayableAgingReport(DateOnly? asOfDate)
    {
        var resolvedAsOfDate = asOfDate ?? DateOnly.FromDateTime(DateTime.UtcNow);
        var partiesById = _dbContext.BusinessParties
            .AsNoTracking()
            .ToArray()
            .ToDictionary(party => party.Id);

        var postedBills = _dbContext.AccountsPayableBills
            .AsNoTracking()
            .ToArray()
            .Where(bill => bill.StatusId == WorkflowStatusSeedIds.Posted)
            .ToArray();

        var postedPayments = _dbContext.AccountsPayablePayments
            .AsNoTracking()
            .ToArray()
            .Where(payment => payment.StatusId == WorkflowStatusSeedIds.Posted)
            .ToArray();
        var postedPaymentIds = postedPayments.Select(payment => payment.Id).ToArray();
        var allocationsByBillId = _dbContext.AccountsPayablePaymentAllocations
            .AsNoTracking()
            .ToArray()
            .Where(allocation => postedPaymentIds.Contains(allocation.AccountsPayablePaymentId))
            .GroupBy(allocation => allocation.AccountsPayableBillId)
            .ToDictionary(group => group.Key, group => group.Sum(allocation => allocation.AllocationAmount));

        var rows = postedBills
            .Select(bill =>
            {
                var originalAmount = GetAccountsPayableSignedAmount(bill);
                var outstandingAmount = originalAmount - allocationsByBillId.GetValueOrDefault(bill.Id, 0m);
                return new { Bill = bill, OriginalAmount = originalAmount, OutstandingAmount = outstandingAmount };
            })
            .Where(entry => entry.OutstandingAmount != 0m)
            .Select(entry =>
            {
                var party = partiesById.GetValueOrDefault(entry.Bill.SupplierId);
                var daysPastDue = GetDaysPastDue(entry.Bill.DueDate, resolvedAsOfDate);
                return new AgingReportRowView(
                    "AP",
                    entry.Bill.IsDebitNote ? "AP_DEBIT_NOTE" : "AP_BILL",
                    entry.Bill.Id,
                    entry.Bill.BillNumber,
                    entry.Bill.SupplierId,
                    party?.PartyCode ?? "UNKNOWN",
                    party?.PartyName ?? "Unknown Supplier",
                    entry.Bill.BillDate,
                    entry.Bill.DueDate,
                    entry.Bill.CurrencyCode,
                    entry.OriginalAmount,
                    entry.OutstandingAmount,
                    daysPastDue,
                    GetAgingBucketCode(daysPastDue));
            })
            .OrderBy(row => row.PartyCode)
            .ThenBy(row => row.DueDate)
            .ThenBy(row => row.DocumentNumber)
            .ToArray();

        return BuildAgingReport("AP", resolvedAsOfDate, rows);
    }

    public AgingReportView GetAccountsReceivableAgingReport(DateOnly? asOfDate)
    {
        var resolvedAsOfDate = asOfDate ?? DateOnly.FromDateTime(DateTime.UtcNow);
        var partiesById = _dbContext.BusinessParties
            .AsNoTracking()
            .ToArray()
            .ToDictionary(party => party.Id);

        var postedInvoices = _dbContext.AccountsReceivableInvoices
            .AsNoTracking()
            .ToArray()
            .Where(invoice => invoice.StatusId == WorkflowStatusSeedIds.Posted)
            .ToArray();

        var postedReceipts = _dbContext.AccountsReceivableReceipts
            .AsNoTracking()
            .ToArray()
            .Where(receipt => receipt.StatusId == WorkflowStatusSeedIds.Posted)
            .ToArray();
        var postedReceiptIds = postedReceipts.Select(receipt => receipt.Id).ToArray();
        var allocationsByInvoiceId = _dbContext.AccountsReceivableReceiptAllocations
            .AsNoTracking()
            .ToArray()
            .Where(allocation => postedReceiptIds.Contains(allocation.AccountsReceivableReceiptId))
            .GroupBy(allocation => allocation.AccountsReceivableInvoiceId)
            .ToDictionary(group => group.Key, group => group.Sum(allocation => allocation.AllocationAmount));

        var rows = postedInvoices
            .Select(invoice =>
            {
                var originalAmount = GetAccountsReceivableSignedAmount(invoice);
                var outstandingAmount = originalAmount - allocationsByInvoiceId.GetValueOrDefault(invoice.Id, 0m);
                return new { Invoice = invoice, OriginalAmount = originalAmount, OutstandingAmount = outstandingAmount };
            })
            .Where(entry => entry.OutstandingAmount != 0m)
            .Select(entry =>
            {
                var party = partiesById.GetValueOrDefault(entry.Invoice.CustomerId);
                var daysPastDue = GetDaysPastDue(entry.Invoice.DueDate, resolvedAsOfDate);
                return new AgingReportRowView(
                    "AR",
                    entry.Invoice.InvoiceType,
                    entry.Invoice.Id,
                    entry.Invoice.InvoiceNumber,
                    entry.Invoice.CustomerId,
                    party?.PartyCode ?? "UNKNOWN",
                    party?.PartyName ?? "Unknown Customer",
                    entry.Invoice.InvoiceDate,
                    entry.Invoice.DueDate,
                    entry.Invoice.CurrencyCode,
                    entry.OriginalAmount,
                    entry.OutstandingAmount,
                    daysPastDue,
                    GetAgingBucketCode(daysPastDue));
            })
            .OrderBy(row => row.PartyCode)
            .ThenBy(row => row.DueDate)
            .ThenBy(row => row.DocumentNumber)
            .ToArray();

        return BuildAgingReport("AR", resolvedAsOfDate, rows);
    }

    public IReadOnlyCollection<ReportingSnapshotSummaryView> GetReportingSnapshots(string? reportCode = null) =>
        _dbContext.ReportingSnapshots
            .AsNoTracking()
            .Where(entry => string.IsNullOrWhiteSpace(reportCode) || entry.ReportCode == reportCode.Trim().ToUpperInvariant())
            .OrderByDescending(entry => entry.GeneratedAtUtc)
            .ThenByDescending(entry => entry.Id)
            .ToArray()
            .Select(MapSummary)
            .ToArray();

    public ReportingSnapshotDetailView? GetReportingSnapshot(long reportingSnapshotId)
    {
        var snapshot = _dbContext.ReportingSnapshots
            .AsNoTracking()
            .SingleOrDefault(entry => entry.Id == reportingSnapshotId);

        return snapshot is null ? null : MapDetail(snapshot);
    }

    public ReportingSnapshotDetailView CreateReportingSnapshot(long actorUserId, CreateReportingSnapshotCommand command)
    {
        var nextSequence = (_dbContext.ReportingSnapshots.Max(entry => (long?)entry.Id) ?? 0L) + 1L;
        var snapshot = new ReportingSnapshot(
            snapshotNumber: $"RPTSNAP-{nextSequence:D6}",
            reportCode: command.ReportCode,
            reportTitle: command.ReportTitle,
            dateFrom: command.DateFrom,
            dateTo: command.DateTo,
            asOfDate: command.AsOfDate,
            rowCount: command.RowCount,
            jsonPayloadText: command.JsonPayloadText,
            csvPayloadText: command.CsvPayloadText,
            notes: command.Notes,
            actorUserId: actorUserId);

        _dbContext.ReportingSnapshots.Add(snapshot);
        _dbContext.SaveChanges();
        OutboxEventWriter.Enqueue(
            _dbContext,
            sourceModuleCode: "REPORTING",
            aggregateType: "REPORTING_SNAPSHOT",
            aggregateId: snapshot.Id,
            eventType: "REPORTING_SNAPSHOT_GENERATED",
            payload: new
            {
                snapshot.SnapshotNumber,
                snapshot.ReportCode,
                snapshot.ReportTitle,
                snapshot.DateFrom,
                snapshot.DateTo,
                snapshot.AsOfDate,
                snapshot.RowCount
            },
            actorUserId: actorUserId,
            correlationKey: snapshot.SnapshotNumber);
        _dbContext.SaveChanges();
        return GetReportingSnapshot(snapshot.Id)!;
    }

    private FinancialStatementReportView BuildBalanceSheetReport(DateOnly? asOfDate)
    {
        var resolvedAsOfDate = asOfDate ?? DateOnly.FromDateTime(DateTime.UtcNow);
        var rules = GetActiveAccountingRules(resolvedAsOfDate);
        var lines = GetLedgerLineContexts(rules)
            .Where(entry => entry.PostingDate <= resolvedAsOfDate)
            .ToArray();

        var rows = rules.Values
            .Where(rule => FinanceAccountingRuleCatalog.ParticipatesInBalanceSheet(rule.AccountCategoryCode))
            .Select(rule =>
            {
                var accountLines = lines.Where(line => line.Rule.GeneralLedgerAccountId == rule.GeneralLedgerAccountId).ToArray();
                var closingAmount = GetNaturalBalance(rule.AccountCategoryCode, accountLines.Sum(line => line.DebitAmount), accountLines.Sum(line => line.CreditAmount));
                return new FinancialStatementRowView(
                    rule.StatementSectionCode,
                    FinanceAccountingRuleCatalog.LocalizedStatementSectionName(rule.StatementSectionCode),
                    rule.RuleCode,
                    rule.GeneralLedgerAccountName,
                    rule.Id,
                    rule.GeneralLedgerAccountId,
                    rule.GeneralLedgerAccountCode,
                    rule.GeneralLedgerAccountName,
                    null,
                    null,
                    null,
                    null,
                    closingAmount,
                    null,
                    null,
                    null,
                    null,
                    null,
                    rule.RuleName);
            })
            .Where(row => row.ClosingAmount is not null && row.ClosingAmount.Value != 0m)
            .OrderBy(row => GetSectionOrder(row.SectionCode))
            .ThenBy(row => row.GeneralLedgerAccountCode)
            .ToList();

        var netIncome = rules.Values
            .Where(rule => FinanceAccountingRuleCatalog.ParticipatesInIncomeStatement(rule.AccountCategoryCode))
            .Sum(rule =>
            {
                var accountLines = lines.Where(line => line.Rule.GeneralLedgerAccountId == rule.GeneralLedgerAccountId);
                return GetSignedIncomeAmount(rule.AccountCategoryCode, accountLines.Sum(line => line.DebitAmount), accountLines.Sum(line => line.CreditAmount));
            });

        if (netIncome != 0m)
        {
            rows.Add(new FinancialStatementRowView(
                FinanceAccountingRuleCatalog.EquitySection,
                FinanceAccountingRuleCatalog.LocalizedStatementSectionName(FinanceAccountingRuleCatalog.EquitySection),
                "NET_INCOME",
                "Current period result",
                null,
                null,
                null,
                null,
                null,
                null,
                null,
                null,
                netIncome,
                null,
                null,
                null,
                null,
                null,
                "Derived from cumulative income statement activity."));
        }

        rows = rows.OrderBy(row => GetSectionOrder(row.SectionCode)).ThenBy(row => row.GeneralLedgerAccountCode ?? row.LineCode).ToList();

        var totalAssets = rows.Where(row => row.SectionCode is FinanceAccountingRuleCatalog.CurrentAsset or FinanceAccountingRuleCatalog.NonCurrentAsset).Sum(row => row.ClosingAmount ?? 0m);
        var totalLiabilities = rows.Where(row => row.SectionCode is FinanceAccountingRuleCatalog.CurrentLiability or FinanceAccountingRuleCatalog.NonCurrentLiability).Sum(row => row.ClosingAmount ?? 0m);
        var totalEquity = rows.Where(row => row.SectionCode == FinanceAccountingRuleCatalog.EquitySection).Sum(row => row.ClosingAmount ?? 0m);

        return new FinancialStatementReportView(
            FinancialStatementCatalog.BalanceSheet,
            FinancialStatementCatalog.GetTitle(FinancialStatementCatalog.BalanceSheet),
            null,
            resolvedAsOfDate,
            resolvedAsOfDate,
            CurrencyConversionContext.Create(_dbContext).BaseCurrencyCode,
            FinancialStatementCatalog.GetColumns(FinancialStatementCatalog.BalanceSheet),
            [
                new FinancialStatementMetricView("TOTAL_ASSETS", "Total assets", totalAssets),
                new FinancialStatementMetricView("TOTAL_LIABILITIES", "Total liabilities", totalLiabilities),
                new FinancialStatementMetricView("TOTAL_EQUITY", "Total equity", totalEquity)
            ],
            rows);
    }

    public FinanceStakeholderSummaryReportView GetFinanceStakeholderSummaryReport(DateOnly? dateFrom, DateOnly? dateTo)
    {
        var range = NormalizeDateRange(dateFrom, dateTo);
        var stakeholders = new Dictionary<(string StakeholderType, long StakeholderId, string CurrencyCode), StakeholderAccumulator>();

        var partiesById = _dbContext.BusinessParties
            .AsNoTracking()
            .Where(entry => entry.IsActive)
            .ToArray()
            .ToDictionary(entry => entry.Id);

        var partnerLenders = _dbContext.IslamicFinancePartnerLenders
            .AsNoTracking()
            .ToArray();

        var supplierBills = _dbContext.AccountsPayableBills
            .AsNoTracking()
            .Where(entry => entry.StatusId == WorkflowStatusSeedIds.Posted
                && entry.BillDate >= range.DateFrom
                && entry.BillDate <= range.DateTo)
            .ToArray();
        var postedSupplierPaymentIds = _dbContext.AccountsPayablePayments
            .AsNoTracking()
            .Where(entry => entry.StatusId == WorkflowStatusSeedIds.Posted
                && entry.PaymentDate >= range.DateFrom
                && entry.PaymentDate <= range.DateTo)
            .Select(entry => entry.Id)
            .ToArray();
        var postedSupplierPaymentIdSet = postedSupplierPaymentIds.ToHashSet();
        var supplierAllocationsByBillId = _dbContext.AccountsPayablePaymentAllocations
            .AsNoTracking()
            .ToArray()
            .Where(entry => postedSupplierPaymentIdSet.Contains(entry.AccountsPayablePaymentId))
            .GroupBy(entry => entry.AccountsPayableBillId)
            .ToDictionary(group => group.Key, group => group.Sum(item => item.AllocationAmount));
        foreach (var bill in supplierBills)
        {
            if (!partiesById.TryGetValue(bill.SupplierId, out var supplier))
            {
                continue;
            }

            var openPayable = Math.Max(0m, GetSupplierSignedAmount(bill.BillType, bill.TotalAmount) - supplierAllocationsByBillId.GetValueOrDefault(bill.Id));
            AddStakeholderAmount(stakeholders, "SUPPLIER", supplier.Id, supplier.PartyCode, supplier.PartyName, bill.CurrencyCode, accumulator =>
            {
                accumulator.OpenPayableAmount += openPayable;
            });
        }

        foreach (var payment in _dbContext.AccountsPayablePayments
                     .AsNoTracking()
                     .Where(entry => entry.StatusId == WorkflowStatusSeedIds.Posted
                         && entry.PaymentDate >= range.DateFrom
                         && entry.PaymentDate <= range.DateTo)
                     .ToArray())
        {
            if (!partiesById.TryGetValue(payment.SupplierId, out var supplier))
            {
                continue;
            }

            AddStakeholderAmount(stakeholders, "SUPPLIER", supplier.Id, supplier.PartyCode, supplier.PartyName, payment.CurrencyCode, accumulator =>
            {
                accumulator.UnappliedAmount += payment.UnappliedAmount;
            });
        }

        var postedCustomerReceiptIds = _dbContext.AccountsReceivableReceipts
            .AsNoTracking()
            .Where(receipt => receipt.StatusId == WorkflowStatusSeedIds.Posted
                && receipt.ReceiptDate >= range.DateFrom
                && receipt.ReceiptDate <= range.DateTo)
            .Select(receipt => receipt.Id)
            .ToArray();
        var postedCustomerReceiptIdSet = postedCustomerReceiptIds.ToHashSet();
        var customerAllocationsByInvoiceId = _dbContext.AccountsReceivableReceiptAllocations
            .AsNoTracking()
            .ToArray()
            .Where(entry => postedCustomerReceiptIdSet.Contains(entry.AccountsReceivableReceiptId))
            .GroupBy(entry => entry.AccountsReceivableInvoiceId)
            .ToDictionary(group => group.Key, group => group.Sum(item => item.AllocationAmount));
        foreach (var invoice in _dbContext.AccountsReceivableInvoices
                     .AsNoTracking()
                     .Where(entry => entry.StatusId == WorkflowStatusSeedIds.Posted
                         && entry.InvoiceDate >= range.DateFrom
                         && entry.InvoiceDate <= range.DateTo)
                     .ToArray())
        {
            if (!partiesById.TryGetValue(invoice.CustomerId, out var customer))
            {
                continue;
            }

            var openReceivable = Math.Max(0m, GetCustomerSignedAmount(invoice.InvoiceType, invoice.TotalAmount) - customerAllocationsByInvoiceId.GetValueOrDefault(invoice.Id));
            AddStakeholderAmount(stakeholders, "CUSTOMER", customer.Id, customer.PartyCode, customer.PartyName, invoice.CurrencyCode, accumulator =>
            {
                accumulator.OpenReceivableAmount += openReceivable;
            });
        }

        foreach (var receipt in _dbContext.AccountsReceivableReceipts
                     .AsNoTracking()
                     .Where(entry => entry.StatusId == WorkflowStatusSeedIds.Posted
                         && entry.ReceiptDate >= range.DateFrom
                         && entry.ReceiptDate <= range.DateTo)
                     .ToArray())
        {
            if (!partiesById.TryGetValue(receipt.CustomerId, out var customer))
            {
                continue;
            }

            AddStakeholderAmount(stakeholders, "CUSTOMER", customer.Id, customer.PartyCode, customer.PartyName, receipt.CurrencyCode, accumulator =>
            {
                accumulator.UnappliedAmount += receipt.UnappliedAmount;
            });
        }

        var facilities = _dbContext.IslamicFinanceFacilities
            .AsNoTracking()
            .ToArray()
            .Where(entry => entry.IsActive
                && entry.ActivatedAtUtc.HasValue
                && DateOnly.FromDateTime(entry.ActivatedAtUtc.Value) >= range.DateFrom
                && DateOnly.FromDateTime(entry.ActivatedAtUtc.Value) <= range.DateTo)
            .ToArray();
        foreach (var facility in facilities)
        {
            if (partiesById.TryGetValue(facility.CustomerPartyId, out var customer))
            {
                AddStakeholderAmount(stakeholders, "CUSTOMER", customer.Id, customer.PartyCode, customer.PartyName, facility.CurrencyCode, accumulator =>
                {
                    accumulator.FacilityLimitAmount += facility.ApprovedLimitAmount;
                    accumulator.FacilityUtilizedAmount += facility.UtilizedAmount;
                });
            }

            var partnerLender = partnerLenders.SingleOrDefault(entry => entry.Id == facility.PartnerLenderId);
            if (partnerLender is not null && partiesById.TryGetValue(partnerLender.PartnerLenderPartyId, out var lenderParty))
            {
                AddStakeholderAmount(stakeholders, "PARTNER_LENDER", partnerLender.Id, lenderParty.PartyCode, lenderParty.PartyName, facility.CurrencyCode, accumulator =>
                {
                    accumulator.FacilityLimitAmount += facility.ApprovedLimitAmount;
                    accumulator.FacilityUtilizedAmount += facility.UtilizedAmount;
                });
            }
        }

        var requests = _dbContext.IslamicFinanceRequests
            .AsNoTracking()
            .ToArray()
            .Where(entry =>
                (string.Equals(entry.StatusCode, IslamicFinanceWorkflowCatalog.Approved, StringComparison.OrdinalIgnoreCase)
                 || string.Equals(entry.StatusCode, IslamicFinanceWorkflowCatalog.Assigned, StringComparison.OrdinalIgnoreCase))
                && (entry.AssignedPartnerLenderAtUtc.HasValue || entry.ApprovedAtUtc.HasValue)
                && DateOnly.FromDateTime((entry.AssignedPartnerLenderAtUtc ?? entry.ApprovedAtUtc)!.Value) >= range.DateFrom
                && DateOnly.FromDateTime((entry.AssignedPartnerLenderAtUtc ?? entry.ApprovedAtUtc)!.Value) <= range.DateTo)
            .ToArray();
        foreach (var request in requests)
        {
            if (partiesById.TryGetValue(request.CustomerPartyId, out var customer))
            {
                AddStakeholderAmount(stakeholders, "CUSTOMER", customer.Id, customer.PartyCode, customer.PartyName, request.TransactionCurrencyCode, accumulator =>
                {
                    accumulator.RequestedFinancingAmount += request.RequestedAmount;
                });
            }

            if (request.AssignedPartnerLenderId.HasValue)
            {
                var partnerLender = partnerLenders.SingleOrDefault(entry => entry.Id == request.AssignedPartnerLenderId.Value);
                if (partnerLender is not null && partiesById.TryGetValue(partnerLender.PartnerLenderPartyId, out var lenderParty))
                {
                    AddStakeholderAmount(stakeholders, "PARTNER_LENDER", partnerLender.Id, lenderParty.PartyCode, lenderParty.PartyName, request.TransactionCurrencyCode, accumulator =>
                    {
                        accumulator.RequestedFinancingAmount += request.RequestedAmount;
                    });
                }
            }
        }

        var murabahaCases = _dbContext.IslamicFinanceMurabahaCases
            .AsNoTracking()
            .ToArray()
            .Where(entry =>
                (string.Equals(entry.StatusCode, IslamicFinanceWorkflowCatalog.Approved, StringComparison.OrdinalIgnoreCase)
                 || string.Equals(entry.StatusCode, IslamicFinanceWorkflowCatalog.Active, StringComparison.OrdinalIgnoreCase))
                && (entry.ActivatedAtUtc.HasValue || entry.ApprovedAtUtc.HasValue)
                && DateOnly.FromDateTime((entry.ActivatedAtUtc ?? entry.ApprovedAtUtc)!.Value) >= range.DateFrom
                && DateOnly.FromDateTime((entry.ActivatedAtUtc ?? entry.ApprovedAtUtc)!.Value) <= range.DateTo)
            .ToArray();
        foreach (var murabahaCase in murabahaCases)
        {
            if (partiesById.TryGetValue(murabahaCase.CustomerPartyId, out var customer))
            {
                AddStakeholderAmount(stakeholders, "CUSTOMER", customer.Id, customer.PartyCode, customer.PartyName, murabahaCase.TransactionCurrencyCode, accumulator =>
                {
                    accumulator.MurabahaSalePriceAmount += murabahaCase.SalePriceAmount;
                });
            }

            if (murabahaCase.SupplierPartyId.HasValue)
            {
                var supplierPartyId = murabahaCase.SupplierPartyId.Value;
                if (partiesById.TryGetValue(supplierPartyId, out var supplierParty))
                {
                    AddStakeholderAmount(stakeholders, "SUPPLIER", supplierParty.Id, supplierParty.PartyCode, supplierParty.PartyName, murabahaCase.TransactionCurrencyCode, accumulator =>
                    {
                        accumulator.MurabahaSalePriceAmount += murabahaCase.SalePriceAmount;
                    });
                }
            }

            var partnerLender = partnerLenders.SingleOrDefault(entry => entry.Id == murabahaCase.PartnerLenderId);
            if (partnerLender is not null && partiesById.TryGetValue(partnerLender.PartnerLenderPartyId, out var lenderParty))
            {
                AddStakeholderAmount(stakeholders, "PARTNER_LENDER", partnerLender.Id, lenderParty.PartyCode, lenderParty.PartyName, murabahaCase.TransactionCurrencyCode, accumulator =>
                {
                    accumulator.MurabahaSalePriceAmount += murabahaCase.SalePriceAmount;
                });
            }
        }

        foreach (var disbursement in _dbContext.IslamicFinanceMurabahaDisbursements
                     .AsNoTracking()
                     .ToArray()
                     .Where(entry => entry.ExecutedAtUtc.HasValue
                         && entry.ExecutedAmount > 0m
                         && (string.Equals(entry.StatusCode, IslamicFinanceWorkflowCatalog.Active, StringComparison.OrdinalIgnoreCase)
                             || string.Equals(entry.StatusCode, IslamicFinanceWorkflowCatalog.Closed, StringComparison.OrdinalIgnoreCase))
                         && DateOnly.FromDateTime(entry.ExecutedAtUtc.Value) >= range.DateFrom
                         && DateOnly.FromDateTime(entry.ExecutedAtUtc.Value) <= range.DateTo)
                     .ToArray())
        {
            if (partiesById.TryGetValue(disbursement.CustomerPartyId, out var customer))
            {
                AddStakeholderAmount(stakeholders, "CUSTOMER", customer.Id, customer.PartyCode, customer.PartyName, disbursement.TransactionCurrencyCode, accumulator =>
                {
                    accumulator.DisbursedAmount += disbursement.ExecutedAmount;
                });
            }

            if (disbursement.SupplierPartyId.HasValue)
            {
                var supplierPartyId = disbursement.SupplierPartyId.Value;
                if (partiesById.TryGetValue(supplierPartyId, out var supplierParty))
                {
                    AddStakeholderAmount(stakeholders, "SUPPLIER", supplierParty.Id, supplierParty.PartyCode, supplierParty.PartyName, disbursement.TransactionCurrencyCode, accumulator =>
                    {
                        accumulator.DisbursedAmount += disbursement.ExecutedAmount;
                    });
                }
            }

            var partnerLender = partnerLenders.SingleOrDefault(entry => entry.Id == disbursement.PartnerLenderId);
            if (partnerLender is not null && partiesById.TryGetValue(partnerLender.PartnerLenderPartyId, out var lenderParty))
            {
                AddStakeholderAmount(stakeholders, "PARTNER_LENDER", partnerLender.Id, lenderParty.PartyCode, lenderParty.PartyName, disbursement.TransactionCurrencyCode, accumulator =>
                {
                    accumulator.DisbursedAmount += disbursement.ExecutedAmount;
                });
            }
        }

        var murabahaCasesById = murabahaCases.ToDictionary(entry => entry.Id);
        foreach (var settlement in _dbContext.IslamicFinanceSettlementHeaders
                     .AsNoTracking()
                     .ToArray()
                     .Where(entry => string.Equals(entry.StatusCode, IslamicFinanceWorkflowCatalog.Closed, StringComparison.OrdinalIgnoreCase)
                         && entry.SettlementDate >= range.DateFrom
                         && entry.SettlementDate <= range.DateTo)
                     .ToArray())
        {
            if (murabahaCasesById.TryGetValue(settlement.MurabahaCaseId, out var murabahaCase))
            {
                if (partiesById.TryGetValue(murabahaCase.CustomerPartyId, out var customer))
                {
                    AddStakeholderAmount(stakeholders, "CUSTOMER", customer.Id, customer.PartyCode, customer.PartyName, settlement.SettlementCurrencyCode, accumulator =>
                    {
                        accumulator.SettledAmount += settlement.TotalSettledAmount;
                    });
                }

                if (murabahaCase.SupplierPartyId.HasValue)
                {
                    var supplierPartyId = murabahaCase.SupplierPartyId.Value;
                    if (partiesById.TryGetValue(supplierPartyId, out var supplierParty))
                    {
                        AddStakeholderAmount(stakeholders, "SUPPLIER", supplierParty.Id, supplierParty.PartyCode, supplierParty.PartyName, settlement.SettlementCurrencyCode, accumulator =>
                        {
                            accumulator.SettledAmount += settlement.TotalSettledAmount;
                        });
                    }
                }
            }

            var partnerLender = partnerLenders.SingleOrDefault(entry => entry.Id == settlement.PartnerLenderId);
            if (partnerLender is not null && partiesById.TryGetValue(partnerLender.PartnerLenderPartyId, out var lenderParty))
            {
                AddStakeholderAmount(stakeholders, "PARTNER_LENDER", partnerLender.Id, lenderParty.PartyCode, lenderParty.PartyName, settlement.SettlementCurrencyCode, accumulator =>
                {
                    accumulator.SettledAmount += settlement.TotalSettledAmount;
                });
            }
        }

        var rows = stakeholders.Values
            .Select(entry => new FinanceStakeholderSummaryRowView(
                entry.StakeholderType,
                entry.StakeholderId,
                entry.StakeholderCode,
                entry.StakeholderName,
                entry.CurrencyCode,
                entry.OpenPayableAmount,
                entry.OpenReceivableAmount,
                entry.UnappliedAmount,
                entry.FacilityLimitAmount,
                entry.FacilityUtilizedAmount,
                entry.RequestedFinancingAmount,
                entry.MurabahaSalePriceAmount,
                entry.DisbursedAmount,
                entry.SettledAmount))
            .OrderBy(entry => entry.StakeholderType)
            .ThenBy(entry => entry.StakeholderCode)
            .ThenBy(entry => entry.CurrencyCode)
            .ToArray();

        return new FinanceStakeholderSummaryReportView(
            range.DateFrom,
            range.DateTo,
            rows.Length,
            rows.Count(entry => entry.StakeholderType == "SUPPLIER"),
            rows.Count(entry => entry.StakeholderType == "CUSTOMER"),
            rows.Count(entry => entry.StakeholderType == "PARTNER_LENDER"),
            rows.Sum(entry => entry.OpenPayableAmount),
            rows.Sum(entry => entry.OpenReceivableAmount),
            rows.Sum(entry => entry.UnappliedAmount),
            rows.Sum(entry => entry.FacilityLimitAmount),
            rows.Sum(entry => entry.FacilityUtilizedAmount),
            rows.Sum(entry => entry.RequestedFinancingAmount),
            rows.Sum(entry => entry.MurabahaSalePriceAmount),
            rows.Sum(entry => entry.DisbursedAmount),
            rows.Sum(entry => entry.SettledAmount),
            rows);
    }

    public FinanceStakeholderDetailReportView GetFinanceStakeholderDetailReport(string stakeholderType, long stakeholderId, string currencyCode, DateOnly? dateFrom, DateOnly? dateTo)
    {
        var normalizedStakeholderType = NormalizeStakeholderType(stakeholderType);
        var normalizedCurrencyCode = NormalizeCurrencyCode(currencyCode);
        var range = NormalizeDateRange(dateFrom, dateTo);

        var partiesById = _dbContext.BusinessParties
            .AsNoTracking()
            .Where(entry => entry.IsActive)
            .ToArray()
            .ToDictionary(entry => entry.Id);
        var partnerLenders = _dbContext.IslamicFinancePartnerLenders
            .AsNoTracking()
            .ToArray();

        var rows = new List<FinanceStakeholderDetailRowView>();
        var accumulator = ResolveStakeholderAccumulator(normalizedStakeholderType, stakeholderId, normalizedCurrencyCode, partiesById, partnerLenders);

        var supplierBills = _dbContext.AccountsPayableBills
            .AsNoTracking()
            .Where(entry => entry.StatusId == WorkflowStatusSeedIds.Posted
                && entry.CurrencyCode == normalizedCurrencyCode
                && entry.BillDate >= range.DateFrom
                && entry.BillDate <= range.DateTo)
            .ToArray();
        var postedSupplierPayments = _dbContext.AccountsPayablePayments
            .AsNoTracking()
            .Where(entry => entry.StatusId == WorkflowStatusSeedIds.Posted
                && entry.CurrencyCode == normalizedCurrencyCode
                && entry.PaymentDate >= range.DateFrom
                && entry.PaymentDate <= range.DateTo)
            .ToArray();
        var postedSupplierPaymentIdSet = postedSupplierPayments.Select(entry => entry.Id).ToHashSet();
        var supplierAllocationsByBillId = _dbContext.AccountsPayablePaymentAllocations
            .AsNoTracking()
            .ToArray()
            .Where(entry => postedSupplierPaymentIdSet.Contains(entry.AccountsPayablePaymentId))
            .GroupBy(entry => entry.AccountsPayableBillId)
            .ToDictionary(group => group.Key, group => group.Sum(item => item.AllocationAmount));

        foreach (var bill in supplierBills.Where(entry => normalizedStakeholderType == "SUPPLIER" && entry.SupplierId == stakeholderId))
        {
            var openPayable = Math.Max(0m, GetSupplierSignedAmount(bill.BillType, bill.TotalAmount) - supplierAllocationsByBillId.GetValueOrDefault(bill.Id));
            accumulator.OpenPayableAmount += openPayable;
            rows.Add(new FinanceStakeholderDetailRowView(
                bill.BillDate,
                "AP_BILL",
                bill.Id,
                bill.BillNumber,
                normalizedCurrencyCode,
                openPayable,
                bill.TotalAmount,
                "POSTED",
                bill.BillType));
        }

        foreach (var payment in postedSupplierPayments.Where(entry => normalizedStakeholderType == "SUPPLIER" && entry.SupplierId == stakeholderId))
        {
            accumulator.UnappliedAmount += payment.UnappliedAmount;
            rows.Add(new FinanceStakeholderDetailRowView(
                payment.PaymentDate,
                "AP_PAYMENT",
                payment.Id,
                payment.PaymentNumber,
                normalizedCurrencyCode,
                payment.UnappliedAmount,
                payment.AmountAmount,
                "POSTED",
                payment.ReferenceNumber ?? "Supplier payment"));
        }

        var postedCustomerReceipts = _dbContext.AccountsReceivableReceipts
            .AsNoTracking()
            .Where(receipt => receipt.StatusId == WorkflowStatusSeedIds.Posted
                && receipt.CurrencyCode == normalizedCurrencyCode
                && receipt.ReceiptDate >= range.DateFrom
                && receipt.ReceiptDate <= range.DateTo)
            .ToArray();
        var postedCustomerReceiptIdSet = postedCustomerReceipts.Select(receipt => receipt.Id).ToHashSet();
        var customerAllocationsByInvoiceId = _dbContext.AccountsReceivableReceiptAllocations
            .AsNoTracking()
            .ToArray()
            .Where(entry => postedCustomerReceiptIdSet.Contains(entry.AccountsReceivableReceiptId))
            .GroupBy(entry => entry.AccountsReceivableInvoiceId)
            .ToDictionary(group => group.Key, group => group.Sum(item => item.AllocationAmount));
        var customerInvoices = _dbContext.AccountsReceivableInvoices
            .AsNoTracking()
            .Where(entry => entry.StatusId == WorkflowStatusSeedIds.Posted
                && entry.CurrencyCode == normalizedCurrencyCode
                && entry.InvoiceDate >= range.DateFrom
                && entry.InvoiceDate <= range.DateTo)
            .ToArray();

        foreach (var invoice in customerInvoices.Where(entry => normalizedStakeholderType == "CUSTOMER" && entry.CustomerId == stakeholderId))
        {
            var openReceivable = Math.Max(0m, GetCustomerSignedAmount(invoice.InvoiceType, invoice.TotalAmount) - customerAllocationsByInvoiceId.GetValueOrDefault(invoice.Id));
            accumulator.OpenReceivableAmount += openReceivable;
            rows.Add(new FinanceStakeholderDetailRowView(
                invoice.InvoiceDate,
                "AR_INVOICE",
                invoice.Id,
                invoice.InvoiceNumber,
                normalizedCurrencyCode,
                openReceivable,
                invoice.TotalAmount,
                "POSTED",
                invoice.InvoiceType));
        }

        foreach (var receipt in postedCustomerReceipts.Where(entry => normalizedStakeholderType == "CUSTOMER" && entry.CustomerId == stakeholderId))
        {
            accumulator.UnappliedAmount += receipt.UnappliedAmount;
            rows.Add(new FinanceStakeholderDetailRowView(
                receipt.ReceiptDate,
                "AR_RECEIPT",
                receipt.Id,
                receipt.ReceiptNumber,
                normalizedCurrencyCode,
                receipt.UnappliedAmount,
                receipt.AmountAmount,
                "POSTED",
                receipt.ReferenceNumber ?? "Customer receipt"));
        }

        var facilities = _dbContext.IslamicFinanceFacilities
            .AsNoTracking()
            .ToArray()
            .Where(entry => entry.IsActive
                && entry.CurrencyCode == normalizedCurrencyCode
                && entry.ActivatedAtUtc.HasValue
                && DateOnly.FromDateTime(entry.ActivatedAtUtc.Value) >= range.DateFrom
                && DateOnly.FromDateTime(entry.ActivatedAtUtc.Value) <= range.DateTo)
            .ToArray();
        var facilityProductProfileIds = facilities.Select(entry => entry.ProductProfileId).Distinct().ToArray();
        var facilityProductProfilesById = _dbContext.IslamicFinanceProductProfiles
            .AsNoTracking()
            .ToArray()
            .Where(entry => facilityProductProfileIds.Contains(entry.Id))
            .ToDictionary(entry => entry.Id);
        foreach (var facility in facilities.Where(entry =>
                     (normalizedStakeholderType == "CUSTOMER" && entry.CustomerPartyId == stakeholderId)
                     || (normalizedStakeholderType == "PARTNER_LENDER" && entry.PartnerLenderId == stakeholderId)))
        {
            accumulator.FacilityLimitAmount += facility.ApprovedLimitAmount;
            accumulator.FacilityUtilizedAmount += facility.UtilizedAmount;
            rows.Add(new FinanceStakeholderDetailRowView(
                DateOnly.FromDateTime(facility.ActivatedAtUtc!.Value),
                "IF_FACILITY",
                facility.Id,
                facility.FacilityNumber,
                normalizedCurrencyCode,
                facility.ApprovedLimitAmount,
                facility.UtilizedAmount,
                facility.StatusCode,
                facilityProductProfilesById.TryGetValue(facility.ProductProfileId, out var facilityProductProfile)
                    ? facilityProductProfile.ProductCode
                    : $"PRODUCT-{facility.ProductProfileId}"));
        }

        var requests = _dbContext.IslamicFinanceRequests
            .AsNoTracking()
            .ToArray()
            .Where(entry =>
                entry.TransactionCurrencyCode == normalizedCurrencyCode
                && (string.Equals(entry.StatusCode, IslamicFinanceWorkflowCatalog.Approved, StringComparison.OrdinalIgnoreCase)
                    || string.Equals(entry.StatusCode, IslamicFinanceWorkflowCatalog.Assigned, StringComparison.OrdinalIgnoreCase))
                && (entry.AssignedPartnerLenderAtUtc.HasValue || entry.ApprovedAtUtc.HasValue)
                && DateOnly.FromDateTime((entry.AssignedPartnerLenderAtUtc ?? entry.ApprovedAtUtc)!.Value) >= range.DateFrom
                && DateOnly.FromDateTime((entry.AssignedPartnerLenderAtUtc ?? entry.ApprovedAtUtc)!.Value) <= range.DateTo)
            .ToArray();
        foreach (var request in requests.Where(entry =>
                     (normalizedStakeholderType == "CUSTOMER" && entry.CustomerPartyId == stakeholderId)
                     || (normalizedStakeholderType == "PARTNER_LENDER" && entry.AssignedPartnerLenderId == stakeholderId)))
        {
            accumulator.RequestedFinancingAmount += request.RequestedAmount;
            rows.Add(new FinanceStakeholderDetailRowView(
                DateOnly.FromDateTime((request.AssignedPartnerLenderAtUtc ?? request.ApprovedAtUtc)!.Value),
                "IF_REQUEST",
                request.Id,
                request.RequestNumber,
                normalizedCurrencyCode,
                request.RequestedAmount,
                null,
                request.StatusCode,
                facilityProductProfilesById.TryGetValue(request.ProductProfileId, out var requestProductProfile)
                    ? requestProductProfile.ProductCode
                    : $"PRODUCT-{request.ProductProfileId}"));
        }

        var murabahaCases = _dbContext.IslamicFinanceMurabahaCases
            .AsNoTracking()
            .ToArray()
            .Where(entry =>
                entry.TransactionCurrencyCode == normalizedCurrencyCode
                && (string.Equals(entry.StatusCode, IslamicFinanceWorkflowCatalog.Approved, StringComparison.OrdinalIgnoreCase)
                    || string.Equals(entry.StatusCode, IslamicFinanceWorkflowCatalog.Active, StringComparison.OrdinalIgnoreCase))
                && (entry.ActivatedAtUtc.HasValue || entry.ApprovedAtUtc.HasValue)
                && DateOnly.FromDateTime((entry.ActivatedAtUtc ?? entry.ApprovedAtUtc)!.Value) >= range.DateFrom
                && DateOnly.FromDateTime((entry.ActivatedAtUtc ?? entry.ApprovedAtUtc)!.Value) <= range.DateTo)
            .ToArray();
        foreach (var murabahaCase in murabahaCases.Where(entry =>
                     (normalizedStakeholderType == "CUSTOMER" && entry.CustomerPartyId == stakeholderId)
                     || (normalizedStakeholderType == "SUPPLIER" && entry.SupplierPartyId == stakeholderId)
                     || (normalizedStakeholderType == "PARTNER_LENDER" && entry.PartnerLenderId == stakeholderId)))
        {
            accumulator.MurabahaSalePriceAmount += murabahaCase.SalePriceAmount;
            rows.Add(new FinanceStakeholderDetailRowView(
                DateOnly.FromDateTime((murabahaCase.ActivatedAtUtc ?? murabahaCase.ApprovedAtUtc)!.Value),
                "IF_MURABAHA_CASE",
                murabahaCase.Id,
                murabahaCase.CaseNumber,
                normalizedCurrencyCode,
                murabahaCase.SalePriceAmount,
                murabahaCase.PurchaseCostAmount,
                murabahaCase.StatusCode,
                facilityProductProfilesById.TryGetValue(murabahaCase.ProductProfileId, out var murabahaProductProfile)
                    ? murabahaProductProfile.ProductCode
                    : $"REQUEST-{murabahaCase.FinancingRequestId}"));
        }

        var murabahaCasesById = murabahaCases.ToDictionary(entry => entry.Id);
        var disbursements = _dbContext.IslamicFinanceMurabahaDisbursements
            .AsNoTracking()
            .ToArray()
            .Where(entry =>
                entry.TransactionCurrencyCode == normalizedCurrencyCode
                && entry.ExecutedAtUtc.HasValue
                && entry.ExecutedAmount > 0m
                && (string.Equals(entry.StatusCode, IslamicFinanceWorkflowCatalog.Active, StringComparison.OrdinalIgnoreCase)
                    || string.Equals(entry.StatusCode, IslamicFinanceWorkflowCatalog.Closed, StringComparison.OrdinalIgnoreCase))
                && DateOnly.FromDateTime(entry.ExecutedAtUtc.Value) >= range.DateFrom
                && DateOnly.FromDateTime(entry.ExecutedAtUtc.Value) <= range.DateTo)
            .ToArray();
        foreach (var disbursement in disbursements.Where(entry =>
                     (normalizedStakeholderType == "CUSTOMER" && entry.CustomerPartyId == stakeholderId)
                     || (normalizedStakeholderType == "SUPPLIER" && entry.SupplierPartyId == stakeholderId)
                     || (normalizedStakeholderType == "PARTNER_LENDER" && entry.PartnerLenderId == stakeholderId)))
        {
            accumulator.DisbursedAmount += disbursement.ExecutedAmount;
            rows.Add(new FinanceStakeholderDetailRowView(
                DateOnly.FromDateTime(disbursement.ExecutedAtUtc!.Value),
                "IF_DISBURSEMENT",
                disbursement.Id,
                disbursement.DisbursementNumber,
                normalizedCurrencyCode,
                disbursement.ExecutedAmount,
                disbursement.InstructedAmount,
                disbursement.StatusCode,
                disbursement.ExecutionReferenceNumber
                ?? (murabahaCasesById.TryGetValue(disbursement.MurabahaCaseId, out var parentMurabahaCase)
                    ? parentMurabahaCase.CaseNumber
                    : $"CASE-{disbursement.MurabahaCaseId}")));
        }

        var settlements = _dbContext.IslamicFinanceSettlementHeaders
            .AsNoTracking()
            .ToArray()
            .Where(entry =>
                entry.SettlementCurrencyCode == normalizedCurrencyCode
                && string.Equals(entry.StatusCode, IslamicFinanceWorkflowCatalog.Closed, StringComparison.OrdinalIgnoreCase)
                && entry.SettlementDate >= range.DateFrom
                && entry.SettlementDate <= range.DateTo)
            .ToArray();
        foreach (var settlement in settlements.Where(entry =>
                     (normalizedStakeholderType == "PARTNER_LENDER" && entry.PartnerLenderId == stakeholderId)
                     || (normalizedStakeholderType != "PARTNER_LENDER"
                         && murabahaCasesById.TryGetValue(entry.MurabahaCaseId, out var parentCase)
                         && ((normalizedStakeholderType == "CUSTOMER" && parentCase.CustomerPartyId == stakeholderId)
                             || (normalizedStakeholderType == "SUPPLIER" && parentCase.SupplierPartyId == stakeholderId)))))
        {
            accumulator.SettledAmount += settlement.TotalSettledAmount;
            rows.Add(new FinanceStakeholderDetailRowView(
                settlement.SettlementDate,
                "IF_SETTLEMENT",
                settlement.Id,
                settlement.SettlementNumber,
                normalizedCurrencyCode,
                settlement.TotalSettledAmount,
                null,
                settlement.StatusCode,
                murabahaCasesById.TryGetValue(settlement.MurabahaCaseId, out var settledMurabahaCase)
                    ? settledMurabahaCase.CaseNumber
                    : $"CASE-{settlement.MurabahaCaseId}"));
        }

        return new FinanceStakeholderDetailReportView(
            range.DateFrom,
            range.DateTo,
            accumulator.StakeholderType,
            accumulator.StakeholderId,
            accumulator.StakeholderCode,
            accumulator.StakeholderName,
            accumulator.CurrencyCode,
            accumulator.OpenPayableAmount,
            accumulator.OpenReceivableAmount,
            accumulator.UnappliedAmount,
            accumulator.FacilityLimitAmount,
            accumulator.FacilityUtilizedAmount,
            accumulator.RequestedFinancingAmount,
            accumulator.MurabahaSalePriceAmount,
            accumulator.DisbursedAmount,
            accumulator.SettledAmount,
            rows
                .OrderBy(entry => entry.ActivityDate)
                .ThenBy(entry => entry.ActivityType)
                .ThenBy(entry => entry.RecordNumber)
                .ToArray());
    }

    private FinancialStatementReportView BuildIncomeStatementReport(DateOnly? dateFrom, DateOnly? dateTo)
    {
        var range = NormalizeDateRange(dateFrom, dateTo);
        var rules = GetActiveAccountingRules(range.DateTo);
        var lines = GetLedgerLineContexts(rules)
            .Where(entry => entry.PostingDate >= range.DateFrom && entry.PostingDate <= range.DateTo)
            .ToArray();

        var rows = rules.Values
            .Where(rule => FinanceAccountingRuleCatalog.ParticipatesInIncomeStatement(rule.AccountCategoryCode))
            .Select(rule =>
            {
                var accountLines = lines.Where(line => line.Rule.GeneralLedgerAccountId == rule.GeneralLedgerAccountId).ToArray();
                var amount = GetNaturalIncomeAmount(rule.AccountCategoryCode, accountLines.Sum(line => line.DebitAmount), accountLines.Sum(line => line.CreditAmount));
                return new FinancialStatementRowView(
                    rule.StatementSectionCode,
                    FinanceAccountingRuleCatalog.LocalizedStatementSectionName(rule.StatementSectionCode),
                    rule.RuleCode,
                    rule.GeneralLedgerAccountName,
                    rule.Id,
                    rule.GeneralLedgerAccountId,
                    rule.GeneralLedgerAccountCode,
                    rule.GeneralLedgerAccountName,
                    null,
                    accountLines.Sum(line => line.DebitAmount),
                    accountLines.Sum(line => line.CreditAmount),
                    amount,
                    null,
                    null,
                    null,
                    null,
                    null,
                    null,
                    rule.RuleName);
            })
            .Where(row => row.NetAmount is not null && row.NetAmount.Value != 0m)
            .OrderBy(row => GetSectionOrder(row.SectionCode))
            .ThenBy(row => row.GeneralLedgerAccountCode)
            .ToArray();

        var totalRevenue = rows.Where(row => row.SectionCode is FinanceAccountingRuleCatalog.OperatingRevenue or FinanceAccountingRuleCatalog.OtherIncome).Sum(row => row.NetAmount ?? 0m);
        var totalExpense = rows.Where(row => row.SectionCode is FinanceAccountingRuleCatalog.OperatingExpense or FinanceAccountingRuleCatalog.OtherExpense).Sum(row => row.NetAmount ?? 0m);

        return new FinancialStatementReportView(
            FinancialStatementCatalog.IncomeStatement,
            FinancialStatementCatalog.GetTitle(FinancialStatementCatalog.IncomeStatement),
            range.DateFrom,
            range.DateTo,
            range.DateTo,
            CurrencyConversionContext.Create(_dbContext).BaseCurrencyCode,
            FinancialStatementCatalog.GetColumns(FinancialStatementCatalog.IncomeStatement),
            [
                new FinancialStatementMetricView("TOTAL_REVENUE", "Total revenue", totalRevenue),
                new FinancialStatementMetricView("TOTAL_EXPENSE", "Total expense", totalExpense),
                new FinancialStatementMetricView("NET_INCOME", "Net income", totalRevenue - totalExpense)
            ],
            rows);
    }

    private FinancialStatementReportView BuildCashFlowReport(DateOnly? dateFrom, DateOnly? dateTo)
    {
        var range = NormalizeDateRange(dateFrom, dateTo);
        var rules = GetActiveAccountingRules(range.DateTo);
        var lines = GetLedgerLineContexts(rules);
        var openingCashBalance = lines
            .Where(entry => entry.PostingDate < range.DateFrom && entry.Rule.IsCashAccount)
            .Sum(entry => entry.DebitAmount - entry.CreditAmount);

        var journalCashRows = lines
            .Where(entry => entry.PostingDate >= range.DateFrom && entry.PostingDate <= range.DateTo)
            .GroupBy(entry => new { entry.BatchNumber, entry.JournalNumber })
            .Select(group =>
            {
                var journalLines = group.ToArray();
                var cashLines = journalLines.Where(line => line.Rule.IsCashAccount).ToArray();
                if (cashLines.Length == 0)
                {
                    return null;
                }

                var referenceRule = journalLines.FirstOrDefault(line => !line.Rule.IsCashAccount)?.Rule;
                var sectionCode = referenceRule is null || referenceRule.CashFlowCategoryCode == FinanceAccountingRuleCatalog.NonCashFlow
                    ? FinanceAccountingRuleCatalog.OperatingCashFlow
                    : referenceRule.CashFlowCategoryCode;
                var label = referenceRule?.GeneralLedgerAccountName ?? journalLines.First().DocumentType;
                var netCash = cashLines.Sum(line => line.DebitAmount - line.CreditAmount);

                return new FinancialStatementRowView(
                    sectionCode,
                    sectionCode switch
                    {
                        FinanceAccountingRuleCatalog.InvestingCashFlow => "Investing activities",
                        FinanceAccountingRuleCatalog.FinancingCashFlow => "Financing activities",
                        _ => "Operating activities"
                    },
                    group.Key.JournalNumber,
                    label,
                    referenceRule?.Id,
                    referenceRule?.GeneralLedgerAccountId,
                    referenceRule?.GeneralLedgerAccountCode,
                    referenceRule?.GeneralLedgerAccountName,
                    null,
                    null,
                    null,
                    netCash,
                    null,
                    journalLines.First().PostingDate,
                    group.Key.BatchNumber,
                    group.Key.JournalNumber,
                    journalLines.First().DocumentType,
                    journalLines.First().ReferenceNumber,
                    journalLines.First().DescriptionText);
            })
            .Where(entry => entry is not null && entry.NetAmount != 0m)
            .Cast<FinancialStatementRowView>()
            .OrderBy(entry => GetCashFlowSectionOrder(entry.SectionCode))
            .ThenBy(entry => entry.ActivityDate)
            .ThenBy(entry => entry.JournalNumber)
            .ToArray();

        var netChange = journalCashRows.Sum(row => row.NetAmount ?? 0m);
        var closingCashBalance = openingCashBalance + netChange;

        return new FinancialStatementReportView(
            FinancialStatementCatalog.CashFlow,
            FinancialStatementCatalog.GetTitle(FinancialStatementCatalog.CashFlow),
            range.DateFrom,
            range.DateTo,
            range.DateTo,
            CurrencyConversionContext.Create(_dbContext).BaseCurrencyCode,
            FinancialStatementCatalog.GetColumns(FinancialStatementCatalog.CashFlow),
            [
                new FinancialStatementMetricView("OPENING_CASH", "Opening cash balance", openingCashBalance),
                new FinancialStatementMetricView("NET_CHANGE", "Net cash movement", netChange),
                new FinancialStatementMetricView("CLOSING_CASH", "Closing cash balance", closingCashBalance)
            ],
            journalCashRows);
    }

    private FinancialStatementReportView BuildTrialBalanceReport(DateOnly? dateFrom, DateOnly? dateTo, DateOnly? asOfDate)
    {
        var range = NormalizeDateRange(dateFrom, dateTo);
        var resolvedAsOfDate = asOfDate ?? range.DateTo;
        var rules = GetActiveAccountingRules(resolvedAsOfDate);
        var lines = GetLedgerLineContexts(rules)
            .Where(entry => entry.PostingDate <= resolvedAsOfDate)
            .ToArray();

        var rows = rules.Values
            .Select(rule =>
            {
                var openingLines = lines.Where(line => line.Rule.GeneralLedgerAccountId == rule.GeneralLedgerAccountId && line.PostingDate < range.DateFrom).ToArray();
                var movementLines = lines.Where(line => line.Rule.GeneralLedgerAccountId == rule.GeneralLedgerAccountId && line.PostingDate >= range.DateFrom && line.PostingDate <= range.DateTo).ToArray();
                var openingAmount = GetNaturalBalance(rule.AccountCategoryCode, openingLines.Sum(line => line.DebitAmount), openingLines.Sum(line => line.CreditAmount));
                var closingAmount = GetNaturalBalance(rule.AccountCategoryCode, openingLines.Sum(line => line.DebitAmount) + movementLines.Sum(line => line.DebitAmount), openingLines.Sum(line => line.CreditAmount) + movementLines.Sum(line => line.CreditAmount));

                return new FinancialStatementRowView(
                    rule.StatementSectionCode,
                    FinanceAccountingRuleCatalog.LocalizedStatementSectionName(rule.StatementSectionCode),
                    rule.RuleCode,
                    rule.GeneralLedgerAccountName,
                    rule.Id,
                    rule.GeneralLedgerAccountId,
                    rule.GeneralLedgerAccountCode,
                    rule.GeneralLedgerAccountName,
                    openingAmount,
                    movementLines.Sum(line => line.DebitAmount),
                    movementLines.Sum(line => line.CreditAmount),
                    null,
                    closingAmount,
                    null,
                    null,
                    null,
                    null,
                    null,
                    rule.RuleName);
            })
            .Where(row => (row.OpeningAmount ?? 0m) != 0m || (row.DebitAmount ?? 0m) != 0m || (row.CreditAmount ?? 0m) != 0m || (row.ClosingAmount ?? 0m) != 0m)
            .OrderBy(row => GetSectionOrder(row.SectionCode))
            .ThenBy(row => row.GeneralLedgerAccountCode)
            .ToArray();

        return new FinancialStatementReportView(
            FinancialStatementCatalog.TrialBalance,
            FinancialStatementCatalog.GetTitle(FinancialStatementCatalog.TrialBalance),
            range.DateFrom,
            range.DateTo,
            resolvedAsOfDate,
            CurrencyConversionContext.Create(_dbContext).BaseCurrencyCode,
            FinancialStatementCatalog.GetColumns(FinancialStatementCatalog.TrialBalance),
            [
                new FinancialStatementMetricView("TOTAL_DEBIT", "Total debit movement", rows.Sum(row => row.DebitAmount ?? 0m)),
                new FinancialStatementMetricView("TOTAL_CREDIT", "Total credit movement", rows.Sum(row => row.CreditAmount ?? 0m)),
                new FinancialStatementMetricView("TOTAL_CLOSING", "Total closing balance", rows.Sum(row => Math.Abs(row.ClosingAmount ?? 0m)))
            ],
            rows);
    }

    private FinancialStatementReportView BuildAccountingEntriesReport(DateOnly? dateFrom, DateOnly? dateTo)
    {
        var range = NormalizeDateRange(dateFrom, dateTo);
        var rules = GetActiveAccountingRules(range.DateTo);
        var rows = GetLedgerLineContexts(rules)
            .Where(entry => entry.PostingDate >= range.DateFrom && entry.PostingDate <= range.DateTo)
            .OrderBy(entry => entry.PostingDate)
            .ThenBy(entry => entry.BatchNumber)
            .ThenBy(entry => entry.JournalNumber)
            .ThenBy(entry => entry.LineNumber)
            .Select(entry => new FinancialStatementRowView(
                entry.Rule.StatementSectionCode,
                FinanceAccountingRuleCatalog.LocalizedStatementSectionName(entry.Rule.StatementSectionCode),
                entry.Rule.RuleCode,
                entry.Rule.GeneralLedgerAccountName,
                entry.Rule.Id,
                entry.Rule.GeneralLedgerAccountId,
                entry.Rule.GeneralLedgerAccountCode,
                entry.Rule.GeneralLedgerAccountName,
                null,
                entry.DebitAmount,
                entry.CreditAmount,
                null,
                null,
                entry.PostingDate,
                entry.BatchNumber,
                entry.JournalNumber,
                entry.DocumentType,
                entry.ReferenceNumber,
                entry.DescriptionText))
            .ToArray();

        return new FinancialStatementReportView(
            FinancialStatementCatalog.AccountingEntries,
            FinancialStatementCatalog.GetTitle(FinancialStatementCatalog.AccountingEntries),
            range.DateFrom,
            range.DateTo,
            range.DateTo,
            CurrencyConversionContext.Create(_dbContext).BaseCurrencyCode,
            FinancialStatementCatalog.GetColumns(FinancialStatementCatalog.AccountingEntries),
            [
                new FinancialStatementMetricView("ENTRY_COUNT", "Entry rows", rows.Length),
                new FinancialStatementMetricView("TOTAL_DEBIT", "Total debit", rows.Sum(row => row.DebitAmount ?? 0m)),
                new FinancialStatementMetricView("TOTAL_CREDIT", "Total credit", rows.Sum(row => row.CreditAmount ?? 0m))
            ],
            rows);
    }

    private static (DateOnly DateFrom, DateOnly DateTo) NormalizeDateRange(DateOnly? dateFrom, DateOnly? dateTo)
    {
        var today = DateOnly.FromDateTime(DateTime.UtcNow);
        var resolvedDateFrom = dateFrom ?? new DateOnly(today.Year, today.Month, 1);
        var resolvedDateTo = dateTo ?? today;

        if (resolvedDateFrom > resolvedDateTo)
        {
            throw new InvalidOperationException("Report dateFrom cannot be after dateTo.");
        }

        return (resolvedDateFrom, resolvedDateTo);
    }

    private Dictionary<long, FinanceAccountingRule> GetActiveAccountingRules(DateOnly businessDate) =>
        _dbContext.FinanceAccountingRules
            .AsNoTracking()
            .ToArray()
            .Where(rule => rule.AppliesOn(businessDate))
            .GroupBy(rule => rule.GeneralLedgerAccountId)
            .ToDictionary(
                group => group.Key,
                group => group
                    .OrderByDescending(rule => rule.EffectiveFromDate)
                    .ThenByDescending(rule => rule.Id)
                    .First());

    private IReadOnlyCollection<LedgerLineContext> GetLedgerLineContexts(IReadOnlyDictionary<long, FinanceAccountingRule> rulesByAccountId)
    {
        var fallbackRules = new Dictionary<long, FinanceAccountingRule>();
        return _dbContext.GeneralLedgerBatches
            .AsNoTracking()
            .Include(batch => batch.Journals)
            .ThenInclude(journal => journal.Lines)
            .ToArray()
            .SelectMany(batch => batch.Journals.SelectMany(journal => journal.Lines.Select(line =>
            {
                var rule = ResolveRule(line.GeneralLedgerAccountId, rulesByAccountId, fallbackRules);
                return new LedgerLineContext(
                    journal.PostingDate,
                    batch.BatchNumber,
                    journal.JournalNumber,
                    batch.BatchType,
                    batch.SourceDocumentId?.ToString(),
                    line.LineNumber,
                    line.DebitAmount,
                    line.CreditAmount,
                    line.LineDescription ?? journal.DescriptionText ?? batch.DescriptionText,
                    rule);
            })))
            .ToArray();
    }

    private static FinanceAccountingRule ResolveRule(
        long generalLedgerAccountId,
        IReadOnlyDictionary<long, FinanceAccountingRule> rulesByAccountId,
        IDictionary<long, FinanceAccountingRule> fallbackRules)
    {
        if (rulesByAccountId.TryGetValue(generalLedgerAccountId, out var existingRule))
        {
            return existingRule;
        }

        if (fallbackRules.TryGetValue(generalLedgerAccountId, out var fallbackRule))
        {
            return fallbackRule;
        }

        fallbackRule = new FinanceAccountingRule(
            ruleCode: $"GL-{generalLedgerAccountId:D6}",
            ruleName: $"GL Account {generalLedgerAccountId}",
            generalLedgerAccountId: generalLedgerAccountId,
            generalLedgerAccountCode: generalLedgerAccountId.ToString(),
            generalLedgerAccountName: $"GL Account {generalLedgerAccountId}",
            accountCategoryCode: FinanceAccountingRuleCatalog.Expense,
            statementSectionCode: FinanceAccountingRuleCatalog.Unclassified,
            cashFlowCategoryCode: FinanceAccountingRuleCatalog.NonCashFlow,
            isCashAccount: false,
            displayOrder: 9999,
            effectiveFromDate: new DateOnly(2026, 1, 1),
            effectiveToDate: null,
            notes: "Derived fallback rule for uncatalogued GL account.");
        fallbackRules[generalLedgerAccountId] = fallbackRule;
        return fallbackRule;
    }

    private static decimal GetSupplierSignedAmount(string billType, decimal totalAmount) =>
        string.Equals(billType, "DEBIT_NOTE", StringComparison.OrdinalIgnoreCase) ? -totalAmount : totalAmount;

    private static decimal GetCustomerSignedAmount(string invoiceType, decimal totalAmount) =>
        string.Equals(invoiceType, "CREDIT_NOTE", StringComparison.OrdinalIgnoreCase) ? -totalAmount : totalAmount;

    private static string NormalizeStakeholderType(string stakeholderType)
    {
        if (string.IsNullOrWhiteSpace(stakeholderType))
        {
            throw new InvalidOperationException("Stakeholder type is required.");
        }

        var normalized = stakeholderType.Trim().ToUpperInvariant();
        return normalized switch
        {
            "SUPPLIER" => normalized,
            "CUSTOMER" => normalized,
            "PARTNER_LENDER" => normalized,
            _ => throw new InvalidOperationException("Unsupported stakeholder type.")
        };
    }

    private static string NormalizeCurrencyCode(string currencyCode)
    {
        if (string.IsNullOrWhiteSpace(currencyCode))
        {
            throw new InvalidOperationException("Currency code is required for stakeholder detail.");
        }

        return currencyCode.Trim().ToUpperInvariant();
    }

    private static void AddStakeholderAmount(
        IDictionary<(string StakeholderType, long StakeholderId, string CurrencyCode), StakeholderAccumulator> rows,
        string stakeholderType,
        long stakeholderId,
        string stakeholderCode,
        string stakeholderName,
        string currencyCode,
        Action<StakeholderAccumulator> apply)
    {
        var normalizedCurrencyCode = string.IsNullOrWhiteSpace(currencyCode) ? "EGP" : currencyCode.Trim().ToUpperInvariant();
        var key = (stakeholderType, stakeholderId, normalizedCurrencyCode);
        if (!rows.TryGetValue(key, out var accumulator))
        {
            accumulator = new StakeholderAccumulator(stakeholderType, stakeholderId, stakeholderCode, stakeholderName, normalizedCurrencyCode);
            rows[key] = accumulator;
        }

        apply(accumulator);
    }

    private static StakeholderAccumulator ResolveStakeholderAccumulator(
        string stakeholderType,
        long stakeholderId,
        string currencyCode,
        IReadOnlyDictionary<long, BusinessParty> partiesById,
        IReadOnlyCollection<IslamicFinancePartnerLender> partnerLenders)
    {
        return stakeholderType switch
        {
            "SUPPLIER" or "CUSTOMER" when partiesById.TryGetValue(stakeholderId, out var party) =>
                new StakeholderAccumulator(stakeholderType, stakeholderId, party.PartyCode, party.PartyName, currencyCode),
            "PARTNER_LENDER" => ResolvePartnerLenderAccumulator(stakeholderId, currencyCode, partiesById, partnerLenders),
            _ => throw new InvalidOperationException("Stakeholder reference is not valid.")
        };
    }

    private static StakeholderAccumulator ResolvePartnerLenderAccumulator(
        long stakeholderId,
        string currencyCode,
        IReadOnlyDictionary<long, BusinessParty> partiesById,
        IReadOnlyCollection<IslamicFinancePartnerLender> partnerLenders)
    {
        var partnerLender = partnerLenders.SingleOrDefault(entry => entry.Id == stakeholderId);
        if (partnerLender is null || !partiesById.TryGetValue(partnerLender.PartnerLenderPartyId, out var party))
        {
            throw new InvalidOperationException("Stakeholder reference is not valid.");
        }

        return new StakeholderAccumulator("PARTNER_LENDER", partnerLender.Id, party.PartyCode, party.PartyName, currencyCode);
    }

    private sealed class StakeholderAccumulator
    {
        public StakeholderAccumulator(string stakeholderType, long stakeholderId, string stakeholderCode, string stakeholderName, string currencyCode)
        {
            StakeholderType = stakeholderType;
            StakeholderId = stakeholderId;
            StakeholderCode = stakeholderCode;
            StakeholderName = stakeholderName;
            CurrencyCode = currencyCode;
        }

        public string StakeholderType { get; }
        public long StakeholderId { get; }
        public string StakeholderCode { get; }
        public string StakeholderName { get; }
        public string CurrencyCode { get; }
        public decimal OpenPayableAmount { get; set; }
        public decimal OpenReceivableAmount { get; set; }
        public decimal UnappliedAmount { get; set; }
        public decimal FacilityLimitAmount { get; set; }
        public decimal FacilityUtilizedAmount { get; set; }
        public decimal RequestedFinancingAmount { get; set; }
        public decimal MurabahaSalePriceAmount { get; set; }
        public decimal DisbursedAmount { get; set; }
        public decimal SettledAmount { get; set; }
    }

    private static decimal GetNaturalBalance(string accountCategoryCode, decimal debitAmount, decimal creditAmount) =>
        FinanceAccountingRuleCatalog.NormalizeAccountCategoryCode(accountCategoryCode) switch
        {
            FinanceAccountingRuleCatalog.Asset => debitAmount - creditAmount,
            FinanceAccountingRuleCatalog.Expense => debitAmount - creditAmount,
            FinanceAccountingRuleCatalog.Liability => creditAmount - debitAmount,
            FinanceAccountingRuleCatalog.Equity => creditAmount - debitAmount,
            FinanceAccountingRuleCatalog.Revenue => creditAmount - debitAmount,
            _ => debitAmount - creditAmount
        };

    private static decimal GetNaturalIncomeAmount(string accountCategoryCode, decimal debitAmount, decimal creditAmount) =>
        FinanceAccountingRuleCatalog.NormalizeAccountCategoryCode(accountCategoryCode) == FinanceAccountingRuleCatalog.Expense
            ? debitAmount - creditAmount
            : creditAmount - debitAmount;

    private static decimal GetSignedIncomeAmount(string accountCategoryCode, decimal debitAmount, decimal creditAmount) =>
        FinanceAccountingRuleCatalog.NormalizeAccountCategoryCode(accountCategoryCode) == FinanceAccountingRuleCatalog.Expense
            ? -(debitAmount - creditAmount)
            : creditAmount - debitAmount;

    private static int GetSectionOrder(string sectionCode) =>
        sectionCode switch
        {
            FinanceAccountingRuleCatalog.CurrentAsset => 1,
            FinanceAccountingRuleCatalog.NonCurrentAsset => 2,
            FinanceAccountingRuleCatalog.CurrentLiability => 3,
            FinanceAccountingRuleCatalog.NonCurrentLiability => 4,
            FinanceAccountingRuleCatalog.EquitySection => 5,
            FinanceAccountingRuleCatalog.OperatingRevenue => 6,
            FinanceAccountingRuleCatalog.OtherIncome => 7,
            FinanceAccountingRuleCatalog.OperatingExpense => 8,
            FinanceAccountingRuleCatalog.OtherExpense => 9,
            _ => 99
        };

    private static int GetCashFlowSectionOrder(string sectionCode) =>
        sectionCode switch
        {
            FinanceAccountingRuleCatalog.OperatingCashFlow => 1,
            FinanceAccountingRuleCatalog.InvestingCashFlow => 2,
            FinanceAccountingRuleCatalog.FinancingCashFlow => 3,
            _ => 9
        };

    private static AgingReportView BuildAgingReport(string ledgerSideCode, DateOnly asOfDate, IReadOnlyCollection<AgingReportRowView> rows) =>
        new(
            ledgerSideCode,
            asOfDate,
            rows.Count,
            rows.Select(row => row.PartyId).Distinct().Count(),
            rows.Sum(row => row.OutstandingAmount),
            rows.Where(row => row.AgingBucketCode == "CURRENT").Sum(row => row.OutstandingAmount),
            rows.Where(row => row.AgingBucketCode == "1_30").Sum(row => row.OutstandingAmount),
            rows.Where(row => row.AgingBucketCode == "31_60").Sum(row => row.OutstandingAmount),
            rows.Where(row => row.AgingBucketCode == "61_90").Sum(row => row.OutstandingAmount),
            rows.Where(row => row.AgingBucketCode == "90_PLUS").Sum(row => row.OutstandingAmount),
            rows);

    private static int GetDaysPastDue(DateOnly? dueDate, DateOnly asOfDate)
    {
        if (!dueDate.HasValue || dueDate.Value >= asOfDate)
        {
            return 0;
        }

        return asOfDate.DayNumber - dueDate.Value.DayNumber;
    }

    private static string GetAgingBucketCode(int daysPastDue) =>
        daysPastDue switch
        {
            <= 0 => "CURRENT",
            <= 30 => "1_30",
            <= 60 => "31_60",
            <= 90 => "61_90",
            _ => "90_PLUS"
        };

    private static decimal GetAccountsPayableSignedAmount(Domain.Procurement.AccountsPayableBill bill) =>
        bill.IsDebitNote ? -bill.TotalAmount : bill.TotalAmount;

    private static decimal GetAccountsReceivableSignedAmount(Domain.Finance.AccountsReceivableInvoice invoice) =>
        string.Equals(invoice.InvoiceType, "CREDIT_NOTE", StringComparison.OrdinalIgnoreCase)
        || string.Equals(invoice.InvoiceType, "CREDIT", StringComparison.OrdinalIgnoreCase)
        || string.Equals(invoice.InvoiceType, "CREDIT_MEMO", StringComparison.OrdinalIgnoreCase)
            ? -invoice.TotalAmount
            : invoice.TotalAmount;

    private static string BuildPartnerSupportText(bool supportsMurabaha, bool supportsReceivablesFinance, bool supportsTawarruq)
    {
        var supportedProducts = new List<string>();
        if (supportsMurabaha)
        {
            supportedProducts.Add("MURABAHA");
        }

        if (supportsReceivablesFinance)
        {
            supportedProducts.Add("RECEIVABLES_FINANCE");
        }

        if (supportsTawarruq)
        {
            supportedProducts.Add("TAWARRUQ");
        }

        return string.Join(" | ", supportedProducts);
    }

    private static ReportingSnapshotSummaryView MapSummary(ReportingSnapshot snapshot) =>
        new(
            snapshot.Id,
            snapshot.SnapshotNumber,
            snapshot.ReportCode,
            snapshot.ReportTitle,
            snapshot.DateFrom,
            snapshot.DateTo,
            snapshot.AsOfDate,
            snapshot.RowCount,
            snapshot.GeneratedAtUtc,
            snapshot.GeneratedByUserId,
            snapshot.Notes);

    private static ReportingSnapshotDetailView MapDetail(ReportingSnapshot snapshot) =>
        new(
            snapshot.Id,
            snapshot.SnapshotNumber,
            snapshot.ReportCode,
            snapshot.ReportTitle,
            snapshot.DateFrom,
            snapshot.DateTo,
            snapshot.AsOfDate,
            snapshot.RowCount,
            snapshot.JsonPayloadText,
            snapshot.CsvPayloadText,
            snapshot.GeneratedAtUtc,
            snapshot.GeneratedByUserId,
            snapshot.Notes);

    private sealed record LedgerLineContext(
        DateOnly PostingDate,
        string BatchNumber,
        string JournalNumber,
        string DocumentType,
        string? ReferenceNumber,
        int LineNumber,
        decimal DebitAmount,
        decimal CreditAmount,
        string? DescriptionText,
        FinanceAccountingRule Rule);
}
