using CashLineERP.Application.External;
using CashLineERP.Application.External.Views;
using CashLineERP.Application.Procurement;
using CashLineERP.Application.Suppliers;
using CashLineERP.Domain.Customers;
using CashLineERP.Domain.Documents;
using CashLineERP.Domain.Finance;
using CashLineERP.Domain.IslamicFinance.Disbursements;
using CashLineERP.Domain.IslamicFinance.Murabaha;
using CashLineERP.Domain.IslamicFinance.ReceivablesFinance;
using CashLineERP.Domain.IslamicFinance.Requests;
using CashLineERP.Domain.IslamicFinance.Settlements;
using CashLineERP.Domain.Parties;
using CashLineERP.Domain.Procurement;
using CashLineERP.SharedKernel.Documents;
using CashLineERP.SharedKernel.IslamicFinance;
using CashLineERP.SharedKernel.Workflow;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;

namespace CashLineERP.Infrastructure.Persistence.Repositories;

public sealed class ExternalPortalRepository : IExternalPortalRepository
{
    private readonly CashLineErpDbContext _dbContext;

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

    public IReadOnlyCollection<ExternalRfqInvitationView> GetRfqInvitations(long partyId)
    {
        var party = GetPartyContext(partyId);
        if (!IsSupplier(party))
        {
            return [];
        }

        return _dbContext.RequestForQuotations
            .AsNoTracking()
            .Include(entry => entry.Lines)
            .Include(entry => entry.Suppliers)
            .ToArray()
            .SelectMany(entry => entry.Suppliers
                .Where(supplier => supplier.SupplierId == partyId)
                .Select(supplier => new ExternalRfqInvitationView(
                    entry.Id,
                    entry.RfqNumber,
                    entry.RfqType,
                    entry.RfqDate,
                    entry.ValidUntilDate,
                    entry.CurrencyCode,
                    WorkflowStatusSeedIds.ToCode(entry.StatusId),
                    supplier.InviteStatus,
                    supplier.InvitedAtUtc,
                    supplier.RespondedAtUtc,
                    entry.PurchaseRequisitionId,
                    entry.Lines.Count,
                    entry.Notes)))
            .OrderByDescending(entry => entry.RfqDate)
            .ThenByDescending(entry => entry.RfqId)
            .ToArray();
    }

    public ExternalRfqInvitationDetailView? GetRfqInvitation(long partyId, long rfqId)
    {
        var party = GetPartyContext(partyId);
        if (!IsSupplier(party))
        {
            return null;
        }

        var rfq = _dbContext.RequestForQuotations
            .AsNoTracking()
            .Include(entry => entry.Lines)
            .Include(entry => entry.Suppliers)
            .SingleOrDefault(entry => entry.Id == rfqId && entry.Suppliers.Any(supplier => supplier.SupplierId == partyId));

        if (rfq is null)
        {
            return null;
        }

        var supplierInvite = rfq.Suppliers.Single(supplier => supplier.SupplierId == partyId);
        return new ExternalRfqInvitationDetailView(
            rfq.Id,
            rfq.RfqNumber,
            rfq.RfqType,
            rfq.RfqDate,
            rfq.ValidUntilDate,
            rfq.CurrencyCode,
            WorkflowStatusSeedIds.ToCode(rfq.StatusId),
            supplierInvite.InviteStatus,
            supplierInvite.InvitedAtUtc,
            supplierInvite.RespondedAtUtc,
            rfq.PurchaseRequisitionId,
            rfq.Notes,
            rfq.Lines
                .OrderBy(line => line.LineNumber)
                .Select(line => new ExternalRfqInvitationLineView(
                    line.Id,
                    line.LineNumber,
                    line.ItemId,
                    line.ItemDescription,
                    line.RequestedQuantity,
                    line.UnitOfMeasureCode,
                    line.NeedByDate,
                    line.TaxCode,
                    line.TargetUnitPrice,
                    line.Notes))
                .ToArray());
    }

    public ExternalRfqInvitationDetailView RespondToRfqInvitation(long partyId, long rfqId, string responseStatusCode, string? declineReason)
    {
        var party = GetPartyContext(partyId);
        if (!IsSupplier(party))
        {
            throw new InvalidOperationException("Only supplier parties can respond to RFQ invitations in the external portal.");
        }

        var rfq = _dbContext.RequestForQuotations
            .Include(entry => entry.Lines)
            .Include(entry => entry.Suppliers)
            .SingleOrDefault(entry => entry.Id == rfqId && entry.Suppliers.Any(supplier => supplier.SupplierId == partyId))
            ?? throw new InvalidOperationException($"RFQ '{rfqId}' was not found for supplier '{partyId}'.");

        var supplierInvite = rfq.Suppliers.Single(entry => entry.SupplierId == partyId);
        switch (responseStatusCode?.Trim().ToUpperInvariant())
        {
            case "ACCEPTED":
                supplierInvite.Accept();
                break;
            case "DECLINED":
                supplierInvite.Decline(declineReason);
                break;
            default:
                throw new InvalidOperationException($"RFQ response status '{responseStatusCode}' is not supported.");
        }

        _dbContext.SaveChanges();
        return GetRfqInvitation(partyId, rfqId)!;
    }

    public IReadOnlyCollection<ExternalQuotationView> GetQuotations(long partyId)
    {
        var party = GetPartyContext(partyId);
        if (!IsSupplier(party))
        {
            return [];
        }

        var rfqsById = _dbContext.RequestForQuotations
            .AsNoTracking()
            .ToArray()
            .ToDictionary(entry => entry.Id, entry => entry.RfqNumber);

        return _dbContext.SupplierQuotations
            .AsNoTracking()
            .Where(entry => entry.SupplierId == partyId)
            .ToArray()
            .OrderByDescending(entry => entry.QuoteDate)
            .ThenByDescending(entry => entry.Id)
            .Select(entry => new ExternalQuotationView(
                "SUPPLIER_QUOTATION",
                entry.Id,
                entry.QuotationNumber,
                rfqsById.GetValueOrDefault(entry.RequestForQuotationId),
                entry.QuoteDate,
                entry.ValidUntilDate,
                entry.CurrencyCode,
                WorkflowStatusSeedIds.ToCode(entry.StatusId),
                entry.TotalAmount,
                entry.IsSelected,
                entry.Notes))
            .ToArray();
    }

    public SupplierQuotationDetailView? GetQuotation(long partyId, long quotationId)
    {
        var party = GetPartyContext(partyId);
        if (!IsSupplier(party))
        {
            return null;
        }

        var quotationExists = _dbContext.SupplierQuotations
            .AsNoTracking()
            .Any(entry => entry.Id == quotationId && entry.SupplierId == partyId);

        return quotationExists ? GetSupplierQuotationDetail(quotationId) : null;
    }

    public SupplierQuotationDetailView CreateQuotation(long partyId, long actorUserId, CreateSupplierQuotationCommand command)
    {
        var party = GetPartyContext(partyId);
        if (!IsSupplier(party))
        {
            throw new InvalidOperationException("Only supplier parties can create quotations in the external portal.");
        }

        if (command.SupplierId != partyId)
        {
            throw new InvalidOperationException("External quotation creation must use the signed-in supplier party.");
        }

        var rfq = _dbContext.RequestForQuotations
            .Include(entry => entry.Lines)
            .Include(entry => entry.Suppliers)
            .SingleOrDefault(entry => entry.Id == command.RequestForQuotationId);

        if (rfq is null || rfq.Suppliers.All(entry => entry.SupplierId != partyId))
        {
            throw new InvalidOperationException($"RFQ '{command.RequestForQuotationId}' is not assigned to supplier '{partyId}'.");
        }

        foreach (var line in command.Lines)
        {
            if (!rfq.Lines.Any(entry => entry.Id == line.RequestForQuotationLineId))
            {
                throw new InvalidOperationException($"RFQ line '{line.RequestForQuotationLineId}' does not belong to RFQ '{command.RequestForQuotationId}'.");
            }
        }

        var supplierInvite = rfq.Suppliers.Single(entry => entry.SupplierId == partyId);
        if (string.Equals(supplierInvite.InviteStatus, "DECLINED", StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException("Supplier quotation cannot be created after the RFQ invitation has been declined.");
        }

        if (!string.Equals(supplierInvite.InviteStatus, "ACCEPTED", StringComparison.OrdinalIgnoreCase))
        {
            supplierInvite.Accept();
        }

        var nextSequence = (_dbContext.SupplierQuotations.Max(quotation => (long?)quotation.Id) ?? 0L) + 1L;
        var quotation = new SupplierQuotation(
            quotationNumber: $"QTN-{nextSequence:D6}",
            requestForQuotationId: command.RequestForQuotationId,
            supplierId: partyId,
            quoteDate: command.QuoteDate,
            validUntilDate: command.ValidUntilDate,
            currencyCode: command.CurrencyCode,
            fxRate: command.FxRate,
            notes: command.Notes);

        var lineNumber = 1;
        foreach (var line in command.Lines)
        {
            quotation.AddLine(
                lineNumber++,
                line.RequestForQuotationLineId,
                line.ItemId,
                line.QuotedQuantity,
                line.UnitOfMeasureCode,
                line.UnitPrice,
                line.DiscountPercent,
                line.DiscountAmount,
                line.TaxCode,
                line.LeadTimeDays);
        }

        _dbContext.SupplierQuotations.Add(quotation);
        _dbContext.SaveChanges();

        return GetSupplierQuotationDetail(quotation.Id)!;
    }

    public SupplierQuotationDetailView SubmitQuotation(long partyId, long quotationId, long actorUserId)
    {
        var party = GetPartyContext(partyId);
        if (!IsSupplier(party))
        {
            throw new InvalidOperationException("Only supplier parties can submit quotations in the external portal.");
        }

        var quotation = _dbContext.SupplierQuotations.SingleOrDefault(entry => entry.Id == quotationId && entry.SupplierId == partyId);
        if (quotation is null)
        {
            throw new InvalidOperationException($"Quotation '{quotationId}' was not found for supplier '{partyId}'.");
        }

        quotation.Submit(actorUserId);
        _dbContext.SaveChanges();
        return GetSupplierQuotationDetail(quotation.Id)!;
    }

    public IReadOnlyCollection<ExternalOrderView> GetOrders(long partyId)
    {
        var party = GetPartyContext(partyId);
        if (IsSupplier(party))
        {
            return _dbContext.PurchaseOrders
                .AsNoTracking()
                .Where(entry => entry.SupplierId == partyId)
                .ToArray()
                .OrderByDescending(entry => entry.PurchaseOrderDate)
                .ThenByDescending(entry => entry.Id)
                .Select(entry => new ExternalOrderView(
                    "PURCHASE_ORDER",
                    entry.Id,
                    entry.PurchaseOrderNumber,
                    entry.PurchaseOrderDate,
                    entry.CurrencyCode,
                    WorkflowStatusSeedIds.ToCode(entry.StatusId),
                    entry.TotalAmount,
                    entry.BranchId,
                    entry.PurchaseRequisitionId?.ToString(),
                    entry.SupplierAcknowledgmentStatusCode,
                    entry.SupplierAcknowledgmentNotes))
                .ToArray();
        }

        return _dbContext.SalesOrders
            .AsNoTracking()
            .Where(entry => entry.CustomerId == partyId)
            .ToArray()
            .OrderByDescending(entry => entry.OrderDate)
            .ThenByDescending(entry => entry.Id)
            .Select(entry => new ExternalOrderView(
                "SALES_ORDER",
                entry.Id,
                entry.SalesOrderNumber,
                entry.OrderDate,
                entry.CurrencyCode,
                WorkflowStatusSeedIds.ToCode(entry.StatusId),
                entry.TotalAmount,
                entry.BranchId,
                null,
                entry.CreditStatusCode,
                entry.CreditHoldReason))
            .ToArray();
    }

    public IReadOnlyCollection<ExternalDocumentWorkflowView> GetDocumentWorkflows(long partyId)
    {
        GetPartyContext(partyId);

        var documentsById = _dbContext.DocumentLinks
            .AsNoTracking()
            .Where(entry => entry.ModuleCode == "PARTY" && entry.EntityName == "BUSINESS_PARTY" && entry.EntityId == partyId)
            .ToArray()
            .ToDictionary(entry => entry.Id);

        return _dbContext.CounterpartyDocumentWorkflows
            .AsNoTracking()
            .Where(entry => entry.PartyId == partyId)
            .ToArray()
            .OrderByDescending(entry => entry.SubmittedAtUtc ?? entry.UpdatedAtUtc ?? entry.CreatedAtUtc)
            .ThenByDescending(entry => entry.Id)
            .Select(entry => MapDocumentWorkflow(entry, documentsById.GetValueOrDefault(entry.CurrentDocumentLinkId)))
            .Where(entry => entry is not null)
            .Cast<ExternalDocumentWorkflowView>()
            .ToArray();
    }

    public ExternalDocumentWorkflowView? GetDocumentWorkflow(long partyId, long workflowId)
    {
        GetPartyContext(partyId);

        var workflow = _dbContext.CounterpartyDocumentWorkflows
            .AsNoTracking()
            .SingleOrDefault(entry => entry.Id == workflowId && entry.PartyId == partyId);

        if (workflow is null)
        {
            return null;
        }

        var document = _dbContext.DocumentLinks.AsNoTracking().SingleOrDefault(entry => entry.Id == workflow.CurrentDocumentLinkId);
        return MapDocumentWorkflow(workflow, document);
    }

    public ExternalDocumentWorkflowView CreateDocumentWorkflow(
        long partyId,
        long actorUserId,
        string partyTypeCode,
        string documentType,
        string fileName,
        string storageReference,
        string? contentType,
        int? fileSizeBytes,
        string? workflowNotes)
    {
        var party = GetPartyContext(partyId);
        if (!string.Equals(party.PartyType, partyTypeCode, StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException("Counterparty document workflow party type does not match the signed-in party.");
        }

        var document = CreateManagedPartyDocument(partyId, actorUserId, documentType, fileName, storageReference, contentType, fileSizeBytes);
        var workflow = new CounterpartyDocumentWorkflow(
            partyId,
            partyTypeCode,
            document.Id,
            document.DocumentType,
            workflowNotes,
            actorUserId);

        _dbContext.CounterpartyDocumentWorkflows.Add(workflow);
        _dbContext.SaveChanges();
        return GetDocumentWorkflow(partyId, workflow.Id)!;
    }

    public ExternalDocumentWorkflowView SubmitDocumentWorkflow(long partyId, long workflowId, long actorUserId)
    {
        GetPartyContext(partyId);
        var workflow = GetRequiredDocumentWorkflow(partyId, workflowId);
        workflow.Submit(actorUserId);
        _dbContext.SaveChanges();
        return GetDocumentWorkflow(partyId, workflow.Id)!;
    }

    public ExternalDocumentWorkflowView ReplaceDocumentWorkflow(
        long partyId,
        long workflowId,
        long actorUserId,
        string fileName,
        string storageReference,
        string? contentType,
        int? fileSizeBytes,
        string? workflowNotes,
        string? replaceReason)
    {
        GetPartyContext(partyId);
        var workflow = GetRequiredDocumentWorkflow(partyId, workflowId);
        var currentDocument = _dbContext.DocumentLinks.SingleOrDefault(entry => entry.Id == workflow.CurrentDocumentLinkId)
            ?? throw new InvalidOperationException("Current workflow document was not found.");

        EnsureNoDuplicateActiveStorageReference(
            currentDocument.ModuleCode,
            currentDocument.EntityName,
            currentDocument.EntityId,
            currentDocument.DocumentType,
            storageReference,
            currentDocument.Id);

        using var transaction = _dbContext.Database.IsRelational()
            ? _dbContext.Database.BeginTransaction()
            : null;

        var replacementDocument = new DocumentLink(
            moduleCode: currentDocument.ModuleCode,
            entityName: currentDocument.EntityName,
            entityId: currentDocument.EntityId,
            documentType: currentDocument.DocumentType,
            attachmentCategoryCode: currentDocument.AttachmentCategoryCode,
            fileName: fileName.Trim(),
            storageReference: storageReference.Trim(),
            storageProviderCode: DocumentStorageProviderCode.ManagedStorage,
            contentType: string.IsNullOrWhiteSpace(contentType) ? null : contentType.Trim(),
            fileSizeBytes: fileSizeBytes,
            checksumText: null,
            retentionUntilUtc: null,
            versionNumber: currentDocument.VersionNumber + 1,
            uploadedByUserId: actorUserId);

        _dbContext.DocumentLinks.Add(replacementDocument);
        _dbContext.SaveChanges();

        currentDocument.MarkSuperseded(actorUserId, replacementDocument.Id, replaceReason);
        workflow.ReplaceDocument(actorUserId, replacementDocument.Id, workflowNotes);
        _dbContext.SaveChanges();
        transaction?.Commit();

        return GetDocumentWorkflow(partyId, workflow.Id)!;
    }

    public ExternalDocumentWorkflowView ApproveDocumentWorkflow(long workflowId, long actorUserId, string? reviewNotes)
    {
        var workflow = _dbContext.CounterpartyDocumentWorkflows.SingleOrDefault(entry => entry.Id == workflowId)
            ?? throw new InvalidOperationException($"Counterparty document workflow '{workflowId}' was not found.");

        workflow.Approve(actorUserId, reviewNotes);
        _dbContext.SaveChanges();
        return GetDocumentWorkflow(workflow.PartyId, workflow.Id)!;
    }

    public ExternalDocumentWorkflowView ReturnDocumentWorkflow(long workflowId, long actorUserId, string? reviewNotes)
    {
        var workflow = _dbContext.CounterpartyDocumentWorkflows.SingleOrDefault(entry => entry.Id == workflowId)
            ?? throw new InvalidOperationException($"Counterparty document workflow '{workflowId}' was not found.");

        workflow.Return(actorUserId, reviewNotes);
        _dbContext.SaveChanges();
        return GetDocumentWorkflow(workflow.PartyId, workflow.Id)!;
    }

    public ExternalDocumentWorkflowView RejectDocumentWorkflow(long workflowId, long actorUserId, string? reviewNotes)
    {
        var workflow = _dbContext.CounterpartyDocumentWorkflows.SingleOrDefault(entry => entry.Id == workflowId)
            ?? throw new InvalidOperationException($"Counterparty document workflow '{workflowId}' was not found.");

        workflow.Reject(actorUserId, reviewNotes);
        _dbContext.SaveChanges();
        return GetDocumentWorkflow(workflow.PartyId, workflow.Id)!;
    }

    public IReadOnlyCollection<ExternalFinancingOfferView> GetFinancingOffers(long partyId, long? financingRequestId = null)
    {
        GetPartyContext(partyId);

        var requests = _dbContext.IslamicFinanceRequests
            .AsNoTracking()
            .Where(entry => entry.ApplicantPartyId == partyId)
            .ToArray();

        if (financingRequestId.HasValue)
        {
            requests = requests
                .Where(entry => entry.Id == financingRequestId.Value)
                .ToArray();
        }

        if (requests.Length == 0)
        {
            return [];
        }

        var requestsById = requests.ToDictionary(entry => entry.Id);
        var requestNumbersById = requests.ToDictionary(entry => entry.Id, entry => entry.RequestNumber);
        var requestIds = requestNumbersById.Keys.ToArray();
        var productIds = requests.Select(entry => entry.ProductProfileId).Distinct().ToArray();
        var productsById = _dbContext.IslamicFinanceProductProfiles
            .AsNoTracking()
            .Where(entry => productIds.Contains(entry.Id))
            .ToDictionary(entry => entry.Id);
        var partnerIds = requests
            .Where(entry => entry.AssignedPartnerLenderId.HasValue)
            .Select(entry => entry.AssignedPartnerLenderId!.Value)
            .Concat(_dbContext.IslamicFinanceDecisions
                .AsNoTracking()
                .Where(entry => requestIds.Contains(entry.FinancingRequestId)
                    && entry.PartnerLenderId.HasValue
                    && string.Equals(entry.DecisionCode, IslamicFinanceDecisionCatalog.ToCode(IslamicFinanceDecisionCode.Approved), StringComparison.OrdinalIgnoreCase)
                    && string.Equals(entry.StatusCode, IslamicFinanceWorkflowCatalog.Approved, StringComparison.OrdinalIgnoreCase))
                .Select(entry => entry.PartnerLenderId!.Value))
            .Distinct()
            .ToArray();

        var partnersById = _dbContext.IslamicFinancePartnerLenders
            .AsNoTracking()
            .Where(entry => partnerIds.Contains(entry.Id))
            .ToArray()
            .ToDictionary(entry => entry.Id);

        var partnerNamesById = partnersById.Values
            .Join(
                _dbContext.BusinessParties.AsNoTracking(),
                lender => lender.PartnerLenderPartyId,
                party => party.Id,
                (lender, party) => new { lender.Id, party.PartyName })
            .ToDictionary(entry => entry.Id, entry => entry.PartyName);

        var assignmentOffers = requests
            .Where(entry => entry.AssignedPartnerLenderId.HasValue)
            .Select(entry =>
            {
                var selectedPartner = entry.AssignedPartnerLenderId.HasValue
                    ? partnersById.GetValueOrDefault(entry.AssignedPartnerLenderId.Value)
                    : null;

                return new ExternalFinancingOfferView(
                    "REQUEST_ASSIGNMENT",
                    entry.SelectedFinancingDecisionId,
                    entry.Id,
                    entry.RequestNumber,
                    entry.ApplicantPartyId,
                    entry.ApplicantPartyTypeCode,
                    productsById.GetValueOrDefault(entry.ProductProfileId)?.ProductType ?? "MURABAHA",
                    productsById.GetValueOrDefault(entry.ProductProfileId)?.ProductCode ?? $"PRODUCT-{entry.ProductProfileId}",
                    productsById.GetValueOrDefault(entry.ProductProfileId)?.ProductName ?? $"Product {entry.ProductProfileId}",
                    productsById.GetValueOrDefault(entry.ProductProfileId)?.RequiresShariahReview ?? false,
                    entry.FundingSourceMode,
                    entry.AssignedPartnerLenderId,
                    entry.AssignedPartnerLenderId.HasValue ? partnerNamesById.GetValueOrDefault(entry.AssignedPartnerLenderId.Value) : null,
                    selectedPartner?.DefaultCurrencyCode,
                    selectedPartner?.SupportsMurabaha ?? false,
                    selectedPartner?.SupportsReceivablesFinance ?? false,
                    selectedPartner?.SupportsTawarruq ?? false,
                    "ASSIGNED",
                    entry.StatusCode,
                    true,
                    entry.TransactionCurrencyCode,
                    entry.RequestedAmount,
                    null,
                    null,
                    null,
                    null,
                    null,
                    null,
                    entry.AssignedPartnerLenderAtUtc ?? entry.UpdatedAtUtc ?? entry.CreatedAtUtc,
                    entry.SubmittedAtUtc,
                    entry.ApprovedAtUtc);
            });

        var decisionOffers = _dbContext.IslamicFinanceDecisions
            .AsNoTracking()
            .Where(entry => requestIds.Contains(entry.FinancingRequestId)
                && entry.PartnerLenderId.HasValue
                && string.Equals(entry.DecisionCode, IslamicFinanceDecisionCatalog.ToCode(IslamicFinanceDecisionCode.Approved), StringComparison.OrdinalIgnoreCase)
                && string.Equals(entry.StatusCode, IslamicFinanceWorkflowCatalog.Approved, StringComparison.OrdinalIgnoreCase))
            .ToArray()
            .Select(entry =>
            {
                var request = requestsById[entry.FinancingRequestId];
                var offerPartner = entry.PartnerLenderId.HasValue
                    ? partnersById.GetValueOrDefault(entry.PartnerLenderId.Value)
                    : null;

                return new ExternalFinancingOfferView(
                    "PARTNER_DECISION",
                    entry.Id,
                    entry.FinancingRequestId,
                    requestNumbersById[entry.FinancingRequestId],
                    request.ApplicantPartyId,
                    request.ApplicantPartyTypeCode,
                    entry.OfferProductTypeCode,
                    productsById.GetValueOrDefault(request.ProductProfileId)?.ProductCode ?? $"PRODUCT-{request.ProductProfileId}",
                    productsById.GetValueOrDefault(request.ProductProfileId)?.ProductName ?? $"Product {request.ProductProfileId}",
                    productsById.GetValueOrDefault(request.ProductProfileId)?.RequiresShariahReview ?? false,
                    entry.OfferFundingSourceMode,
                    entry.PartnerLenderId,
                    entry.PartnerLenderId.HasValue ? partnerNamesById.GetValueOrDefault(entry.PartnerLenderId.Value) : null,
                    offerPartner?.DefaultCurrencyCode,
                    offerPartner?.SupportsMurabaha ?? false,
                    offerPartner?.SupportsReceivablesFinance ?? false,
                    offerPartner?.SupportsTawarruq ?? false,
                    entry.DecisionCode,
                    entry.StatusCode,
                    request.SelectedFinancingDecisionId == entry.Id,
                    entry.OfferCurrencyCode,
                    entry.OfferedAmount,
                    entry.TenorMonths,
                    entry.ProfitRatePercent,
                    entry.ProcessingFeeAmount,
                    entry.SecurityRequirementText,
                    entry.OfferRank,
                    entry.DecisionNotes,
                    entry.CreatedAtUtc,
                    entry.SubmittedAtUtc,
                    entry.ApprovedAtUtc);
            });

        return assignmentOffers
            .Concat(decisionOffers)
            .OrderByDescending(entry => entry.ApprovedAtUtc ?? entry.SubmittedAtUtc ?? entry.CreatedAtUtc)
            .ThenByDescending(entry => entry.FinancingRequestId)
            .ToArray();
    }

    public IReadOnlyCollection<ExternalFinancingNotificationView> GetFinancingNotifications(long partyId, long financingRequestId)
    {
        GetPartyContext(partyId);

        var requestVisible = _dbContext.IslamicFinanceRequests
            .AsNoTracking()
            .Any(entry => entry.Id == financingRequestId && entry.ApplicantPartyId == partyId);

        if (!requestVisible)
        {
            return [];
        }

        var recipientReference = BuildExternalNotificationRecipientReference(partyId);

        return (
            from item in _dbContext.NotificationQueueItems.AsNoTracking()
            join template in _dbContext.NotificationTemplates.AsNoTracking()
                on item.NotificationTemplateId equals template.Id
            where item.RelatedEntityType == "FINANCING_REQUEST"
            where item.RelatedEntityId == financingRequestId
            where item.RecipientReference == recipientReference
            orderby item.CreatedAtUtc descending, item.Id descending
            select new ExternalFinancingNotificationView(
                item.Id,
                template.TemplateCode,
                item.ChannelCode,
                item.ContextCode,
                item.Subject,
                item.Body,
                item.StatusCode,
                item.CreatedAtUtc,
                item.SentAtUtc))
            .ToArray();
    }

    public IReadOnlyCollection<ExternalFinancingTaskView> GetFinancingTasks(long partyId)
    {
        GetPartyContext(partyId);

        var requests = _dbContext.IslamicFinanceRequests
            .AsNoTracking()
            .Where(entry => entry.ApplicantPartyId == partyId)
            .OrderByDescending(entry => entry.SubmittedAtUtc ?? entry.CreatedAtUtc)
            .ThenByDescending(entry => entry.Id)
            .ToArray();

        if (requests.Length == 0)
        {
            return [];
        }

        var requestIds = requests.Select(entry => entry.Id).ToArray();
        var recipientReference = BuildExternalNotificationRecipientReference(partyId);

        var pendingNotificationCounts = _dbContext.NotificationQueueItems
            .AsNoTracking()
            .Where(entry => entry.RelatedEntityType == "FINANCING_REQUEST"
                && entry.RelatedEntityId.HasValue
                && requestIds.Contains(entry.RelatedEntityId.Value)
                && entry.RecipientReference == recipientReference
                && (entry.StatusCode == "PENDING" || entry.StatusCode == "RETRY"))
            .ToArray()
            .GroupBy(entry => entry.RelatedEntityId!.Value)
            .ToDictionary(group => group.Key, group => group.Count());
        var partnerRoutingNotificationCounts = _dbContext.NotificationQueueItems
            .AsNoTracking()
            .Where(entry => entry.RelatedEntityType == "FINANCING_REQUEST"
                && entry.RelatedEntityId.HasValue
                && requestIds.Contains(entry.RelatedEntityId.Value)
                && entry.RecipientReference.StartsWith("PARTNER_LENDER:")
                && (entry.StatusCode == "PENDING" || entry.StatusCode == "RETRY"))
            .ToArray()
            .GroupBy(entry => entry.RelatedEntityId!.Value)
            .ToDictionary(group => group.Key, group => group.Count());

        var offerCounts = GetFinancingOffers(partyId)
            .Where(entry => entry.FinancingDecisionId.HasValue)
            .GroupBy(entry => entry.FinancingRequestId)
            .ToDictionary(group => group.Key, group => group.Count());

        var murabahaCaseRequestIds = _dbContext.IslamicFinanceMurabahaCases
            .AsNoTracking()
            .Where(entry => requestIds.Contains(entry.FinancingRequestId))
            .Select(entry => entry.FinancingRequestId)
            .Distinct()
            .ToHashSet();

        var receivablesCaseRequestIds = _dbContext.IslamicFinanceReceivablesCases
            .AsNoTracking()
            .Where(entry => requestIds.Contains(entry.FinancingRequestId))
            .Select(entry => entry.FinancingRequestId)
            .Distinct()
            .ToHashSet();

        var unsettledRequestIds = _dbContext.IslamicFinanceSettlementHeaders
            .AsNoTracking()
            .Where(entry => requestIds.Contains(entry.FinancingRequestId) && entry.StatusCode != "CLOSED")
            .Select(entry => entry.FinancingRequestId)
            .Distinct()
            .ToHashSet();

        var pendingDisbursementRequestIds =
            (from disbursement in _dbContext.IslamicFinanceMurabahaDisbursements.AsNoTracking()
             join murabahaCase in _dbContext.IslamicFinanceMurabahaCases.AsNoTracking()
                on disbursement.MurabahaCaseId equals murabahaCase.Id
             where requestIds.Contains(murabahaCase.FinancingRequestId)
             where disbursement.StatusCode != "EXECUTED"
             select murabahaCase.FinancingRequestId)
            .Distinct()
            .ToHashSet();

        var tasks = new List<ExternalFinancingTaskView>();

        foreach (var request in requests)
        {
            var notificationCount = pendingNotificationCounts.GetValueOrDefault(request.Id);
            var partnerRoutingNotificationCount = partnerRoutingNotificationCounts.GetValueOrDefault(request.Id);
            var offerCount = offerCounts.GetValueOrDefault(request.Id);
            var dueAtUtc = ResolveExternalTaskDueAtUtc(request);
            var primaryTask = BuildPrimaryFinancingTask(
                request,
                offerCount,
                notificationCount,
                partnerRoutingNotificationCount,
                murabahaCaseRequestIds.Contains(request.Id),
                receivablesCaseRequestIds.Contains(request.Id),
                pendingDisbursementRequestIds.Contains(request.Id),
                unsettledRequestIds.Contains(request.Id),
                dueAtUtc);
            tasks.Add(primaryTask);

            if (notificationCount > 0)
            {
                tasks.Add(new ExternalFinancingTaskView(
                    request.Id,
                    request.RequestNumber,
                    request.ApplicantPartyTypeCode,
                    "REVIEW_FINANCING_UPDATES",
                    "Review financing updates",
                    "NOTIFICATIONS",
                    "OPEN",
                    $"You have {notificationCount} financing update notification(s) for this request.",
                    notificationCount,
                    null));
            }
        }

        return tasks
            .OrderBy(entry => ResolveTaskSortPriority(entry.TaskStatusCode, entry.TaskCategoryCode))
            .ThenBy(entry => entry.DueAtUtc ?? DateTime.MaxValue)
            .ThenByDescending(entry => entry.FinancingRequestId)
            .ToArray();
    }

    public IReadOnlyCollection<ExternalPurchaseOrderView> GetPurchaseOrders(long partyId)
    {
        var party = GetPartyContext(partyId);
        if (!IsSupplier(party))
        {
            return [];
        }

        return _dbContext.PurchaseOrders
            .AsNoTracking()
            .Where(entry => entry.SupplierId == partyId)
            .ToArray()
            .OrderByDescending(entry => entry.PurchaseOrderDate)
            .ThenByDescending(entry => entry.Id)
            .Select(MapPurchaseOrder)
            .ToArray();
    }

    public ExternalPurchaseOrderView AcknowledgePurchaseOrder(long partyId, long purchaseOrderId, long actorUserId, string acknowledgmentStatusCode, string? notes)
    {
        var party = GetPartyContext(partyId);
        if (!IsSupplier(party))
        {
            throw new InvalidOperationException("Only supplier parties can acknowledge purchase orders in the external portal.");
        }

        var purchaseOrder = _dbContext.PurchaseOrders
            .SingleOrDefault(entry => entry.Id == purchaseOrderId && entry.SupplierId == partyId);

        if (purchaseOrder is null)
        {
            throw new InvalidOperationException($"Purchase order '{purchaseOrderId}' was not found for supplier '{partyId}'.");
        }

        purchaseOrder.AcknowledgeBySupplier(actorUserId, acknowledgmentStatusCode, notes);
        _dbContext.SaveChanges();

        return MapPurchaseOrder(purchaseOrder);
    }

    public ExternalPurchaseOrderView RaisePurchaseOrderIssue(long partyId, long purchaseOrderId, long actorUserId, string issueCategoryCode, string issueNotes)
    {
        if (string.IsNullOrWhiteSpace(issueCategoryCode))
        {
            throw new InvalidOperationException("Supplier purchase-order issue category is required.");
        }

        if (string.IsNullOrWhiteSpace(issueNotes))
        {
            throw new InvalidOperationException("Supplier purchase-order issue notes are required.");
        }

        var party = GetPartyContext(partyId);
        if (!IsSupplier(party))
        {
            throw new InvalidOperationException("Only supplier parties can raise purchase-order issues in the external portal.");
        }

        var purchaseOrder = _dbContext.PurchaseOrders
            .SingleOrDefault(entry => entry.Id == purchaseOrderId && entry.SupplierId == partyId);

        if (purchaseOrder is null)
        {
            throw new InvalidOperationException($"Purchase order '{purchaseOrderId}' was not found for supplier '{partyId}'.");
        }

        var normalizedCategory = issueCategoryCode.Trim().ToUpperInvariant();
        purchaseOrder.AcknowledgeBySupplier(
            actorUserId,
            "ISSUE_RAISED",
            $"[{normalizedCategory}] {issueNotes.Trim()}");
        _dbContext.SaveChanges();

        return MapPurchaseOrder(purchaseOrder);
    }

    public IReadOnlyCollection<ExternalInvoiceView> GetInvoices(long partyId)
    {
        var party = GetPartyContext(partyId);
        if (IsSupplier(party))
        {
            var allocationsByBillId = _dbContext.AccountsPayablePaymentAllocations
                .AsNoTracking()
                .ToArray()
                .GroupBy(entry => entry.AccountsPayableBillId)
                .ToDictionary(group => group.Key, group => group.Sum(item => item.AllocationAmount));

            return _dbContext.AccountsPayableBills
                .AsNoTracking()
                .Where(entry => entry.SupplierId == partyId && entry.StatusId == WorkflowStatusSeedIds.Posted)
                .ToArray()
                .OrderByDescending(entry => entry.BillDate)
                .ThenByDescending(entry => entry.Id)
                .Select(entry =>
                {
                    var signedTotal = GetSupplierInvoiceSignedAmount(entry.BillType, entry.TotalAmount);
                    var allocatedAmount = allocationsByBillId.GetValueOrDefault(entry.Id);
                    var openAmount = signedTotal > 0m ? Math.Max(0m, signedTotal - allocatedAmount) : signedTotal;
                    return new ExternalInvoiceView(
                        entry.BillType,
                        entry.Id,
                        entry.BillNumber,
                        entry.BillDate,
                        entry.DueDate,
                        entry.CurrencyCode,
                        WorkflowStatusSeedIds.ToCode(entry.StatusId),
                        signedTotal,
                        openAmount,
                        entry.ThreeWayMatchStatus,
                        entry.PurchaseOrderId?.ToString());
                })
                .ToArray();
        }

        var receiptAllocationsByInvoiceId = _dbContext.AccountsReceivableReceiptAllocations
            .AsNoTracking()
            .ToArray()
            .GroupBy(entry => entry.AccountsReceivableInvoiceId)
            .ToDictionary(group => group.Key, group => group.Sum(item => item.AllocationAmount));

        return _dbContext.AccountsReceivableInvoices
            .AsNoTracking()
            .Where(entry => entry.CustomerId == partyId && entry.StatusId == WorkflowStatusSeedIds.Posted)
            .ToArray()
            .OrderByDescending(entry => entry.InvoiceDate)
            .ThenByDescending(entry => entry.Id)
            .Select(entry =>
            {
                var signedTotal = GetCustomerInvoiceSignedAmount(entry.InvoiceType, entry.TotalAmount);
                var allocatedAmount = receiptAllocationsByInvoiceId.GetValueOrDefault(entry.Id);
                var openAmount = signedTotal > 0m ? Math.Max(0m, signedTotal - allocatedAmount) : signedTotal;
                return new ExternalInvoiceView(
                    entry.InvoiceType,
                    entry.Id,
                    entry.InvoiceNumber,
                    entry.InvoiceDate,
                    entry.DueDate,
                    entry.CurrencyCode,
                    WorkflowStatusSeedIds.ToCode(entry.StatusId),
                    signedTotal,
                    openAmount,
                    entry.EtaStatus,
                    entry.DispatchOrderId?.ToString() ?? entry.SalesOrderId?.ToString());
            })
            .ToArray();
    }

    public ExternalPaymentView CreateCustomerPaymentRequest(
        long customerPartyId,
        long actorUserId,
        long accountsReceivableInvoiceId,
        string integrationProviderCode,
        string currencyCode,
        decimal amountAmount,
        DateOnly paymentDate,
        string? referenceNumber,
        string? notes)
    {
        var party = GetPartyContext(customerPartyId);
        if (IsSupplier(party))
        {
            throw new InvalidOperationException("Only customer parties can initiate customer payment requests in the external portal.");
        }

        if (string.IsNullOrWhiteSpace(integrationProviderCode))
        {
            throw new InvalidOperationException("Customer payment request integration provider is required.");
        }

        if (string.IsNullOrWhiteSpace(currencyCode))
        {
            throw new InvalidOperationException("Customer payment request currency is required.");
        }

        if (amountAmount <= 0m)
        {
            throw new InvalidOperationException("Customer payment request amount must be greater than zero.");
        }

        var invoice = _dbContext.AccountsReceivableInvoices
            .AsNoTracking()
            .SingleOrDefault(entry => entry.Id == accountsReceivableInvoiceId && entry.CustomerId == customerPartyId)
            ?? throw new InvalidOperationException("Customer payment request invoice was not found for the signed-in customer party.");

        if (invoice.StatusId != WorkflowStatusSeedIds.Posted)
        {
            throw new InvalidOperationException("Customer payment request invoice must be posted before payment initiation.");
        }

        var signedTotal = GetCustomerInvoiceSignedAmount(invoice.InvoiceType, invoice.TotalAmount);
        if (signedTotal <= 0m)
        {
            throw new InvalidOperationException("Customer payment requests are supported for debit invoices only.");
        }

        var allocatedAmount = _dbContext.AccountsReceivableReceiptAllocations
            .AsNoTracking()
            .Where(entry => entry.AccountsReceivableInvoiceId == accountsReceivableInvoiceId)
            .Select(entry => entry.AllocationAmount)
            .AsEnumerable()
            .Sum();
        var openAmount = Math.Max(0m, signedTotal - allocatedAmount);
        if (openAmount <= 0m)
        {
            throw new InvalidOperationException("Customer payment request invoice does not have an open amount.");
        }

        var normalizedAmount = decimal.Round(amountAmount, 2, MidpointRounding.AwayFromZero);
        if (normalizedAmount > openAmount)
        {
            throw new InvalidOperationException("Customer payment request amount cannot exceed the invoice open amount.");
        }

        var channel = ResolveCustomerCollectionChannel(integrationProviderCode, currencyCode);
        var nextSequence = (_dbContext.CustomerPaymentRequests.Max(entry => (long?)entry.Id) ?? 0L) + 1L;
        var request = new CustomerPaymentRequest(
            paymentRequestNumber: $"CPAY-{nextSequence:D6}",
            customerId: customerPartyId,
            accountsReceivableInvoiceId: accountsReceivableInvoiceId,
            paymentChannelConfigurationId: channel.Id,
            paymentDate: paymentDate,
            currencyCode: currencyCode,
            amountAmount: normalizedAmount,
            referenceNumber: referenceNumber,
            notes: notes);
        request.Touch(actorUserId);
        request.Submit(actorUserId, request.PaymentRequestNumber);

        _dbContext.CustomerPaymentRequests.Add(request);
        _dbContext.SaveChanges();

        return new ExternalPaymentView(
            "CUSTOMER_PAYMENT_REQUEST",
            request.Id,
            request.PaymentRequestNumber,
            request.PaymentDate,
            request.CurrencyCode,
            request.StatusCode,
            request.AmountAmount,
            0m,
            channel.IntegrationProviderCode,
            request.ReferenceNumber);
    }

    public IReadOnlyCollection<ExternalPaymentView> GetPayments(long partyId)
    {
        var party = GetPartyContext(partyId);
        if (IsSupplier(party))
        {
            return _dbContext.AccountsPayablePayments
                .AsNoTracking()
                .Where(entry => entry.SupplierId == partyId && entry.StatusId == WorkflowStatusSeedIds.Posted)
                .ToArray()
                .OrderByDescending(entry => entry.PaymentDate)
                .ThenByDescending(entry => entry.Id)
                .Select(entry => new ExternalPaymentView(
                    "AP_PAYMENT",
                    entry.Id,
                    entry.PaymentNumber,
                    entry.PaymentDate,
                    entry.CurrencyCode,
                    WorkflowStatusSeedIds.ToCode(entry.StatusId),
                    entry.AmountAmount,
                    entry.UnappliedAmount,
                    entry.PaymentMethod,
                    entry.ReferenceNumber))
                .ToArray();
        }

        var paymentRequests = _dbContext.CustomerPaymentRequests
            .AsNoTracking()
            .Where(entry => entry.CustomerId == partyId)
            .ToArray();
        var channelsById = _dbContext.PaymentChannelConfigurations
            .AsNoTracking()
            .ToDictionary(entry => entry.Id);
        var postedRefunds = GetPostedCustomerRefundsSafe(partyId);
        var refundedAmountsByReceiptId = postedRefunds
            .Where(entry => entry.CustomerId == partyId && entry.StatusId == WorkflowStatusSeedIds.Posted)
            .GroupBy(entry => entry.SourceAccountsReceivableReceiptId)
            .ToDictionary(group => group.Key, group => group.Sum(item => item.AmountAmount));

        return _dbContext.AccountsReceivableReceipts
            .AsNoTracking()
            .Where(entry => entry.CustomerId == partyId && entry.StatusId == WorkflowStatusSeedIds.Posted)
            .ToArray()
            .Select(entry => new
            {
                Receipt = entry,
                RefundedAmount = refundedAmountsByReceiptId.GetValueOrDefault(entry.Id)
            })
            .OrderByDescending(entry => entry.Receipt.ReceiptDate)
            .ThenByDescending(entry => entry.Receipt.Id)
            .Select(entry => new ExternalPaymentView(
                "AR_RECEIPT",
                entry.Receipt.Id,
                entry.Receipt.ReceiptNumber,
                entry.Receipt.ReceiptDate,
                entry.Receipt.CurrencyCode,
                WorkflowStatusSeedIds.ToCode(entry.Receipt.StatusId),
                entry.Receipt.AmountAmount,
                Math.Max(0m, entry.Receipt.UnappliedAmount - entry.RefundedAmount),
                entry.Receipt.ReceiptMethod,
                entry.Receipt.ReferenceNumber))
            .Concat(paymentRequests
                .OrderByDescending(entry => entry.PaymentDate)
                .ThenByDescending(entry => entry.Id)
                .Select(entry => new ExternalPaymentView(
                    "CUSTOMER_PAYMENT_REQUEST",
                    entry.Id,
                    entry.PaymentRequestNumber,
                    entry.PaymentDate,
                    entry.CurrencyCode,
                    entry.StatusCode,
                    entry.AmountAmount,
                    0m,
                    channelsById.GetValueOrDefault(entry.PaymentChannelConfigurationId)?.IntegrationProviderCode,
                    entry.ReferenceNumber)))
            .Concat(postedRefunds
                .OrderByDescending(entry => entry.RefundDate)
                .ThenByDescending(entry => entry.Id)
                .Select(entry => new ExternalPaymentView(
                    "CUSTOMER_REFUND",
                    entry.Id,
                    entry.RefundNumber,
                    entry.RefundDate,
                    entry.CurrencyCode,
                    WorkflowStatusSeedIds.ToCode(entry.StatusId),
                    entry.AmountAmount,
                    0m,
                    entry.RefundMethod,
                    entry.ReferenceNumber)))
            .ToArray();
    }

    private CustomerRefund[] GetPostedCustomerRefundsSafe(long partyId)
    {
        try
        {
            return _dbContext.CustomerRefunds
                .AsNoTracking()
                .Where(entry => entry.CustomerId == partyId && entry.StatusId == WorkflowStatusSeedIds.Posted)
                .ToArray();
        }
        catch (SqliteException exception) when (exception.Message.Contains("no such column", StringComparison.OrdinalIgnoreCase))
        {
            // Some local SQLite UAT files can lag the latest refund-workflow columns.
            return [];
        }
    }

    public ExternalCustomerPaymentStatusView GetCustomerPaymentStatus(long customerPartyId, long customerPaymentRequestId)
    {
        var party = GetPartyContext(customerPartyId);
        if (IsSupplier(party))
        {
            throw new InvalidOperationException("Supplier parties do not have customer gateway payment requests.");
        }

        var paymentRequest = _dbContext.CustomerPaymentRequests
            .AsNoTracking()
            .SingleOrDefault(entry => entry.Id == customerPaymentRequestId && entry.CustomerId == customerPartyId)
            ?? throw new InvalidOperationException("Customer payment request was not found for the selected customer party.");

        var invoice = _dbContext.AccountsReceivableInvoices
            .AsNoTracking()
            .Single(entry => entry.Id == paymentRequest.AccountsReceivableInvoiceId);
        var channel = _dbContext.PaymentChannelConfigurations
            .AsNoTracking()
            .Single(entry => entry.Id == paymentRequest.PaymentChannelConfigurationId);
        var latestExecution = _dbContext.PaymentGatewayExecutions
            .AsNoTracking()
            .Where(entry => entry.SourceDocumentType == "CUSTOMER_PAYMENT_REQUEST" && entry.SourceDocumentId == paymentRequest.Id)
            .OrderByDescending(entry => entry.CreatedAtUtc)
            .ThenByDescending(entry => entry.Id)
            .FirstOrDefault();
        var providerContract = new PaymentGatewayExecutionRepository(_dbContext).GetCustomerCollectionProviderContract(paymentRequest.Id);

        return new ExternalCustomerPaymentStatusView(
            paymentRequest.Id,
            paymentRequest.PaymentRequestNumber,
            paymentRequest.AccountsReceivableInvoiceId,
            invoice.InvoiceNumber,
            paymentRequest.PaymentDate,
            paymentRequest.CurrencyCode,
            paymentRequest.AmountAmount,
            paymentRequest.StatusCode,
            channel.IntegrationProviderCode,
            paymentRequest.ReferenceNumber,
            latestExecution?.CorrelationKey ?? paymentRequest.CorrelationKey,
            paymentRequest.SettledAccountsReceivableReceiptId,
            paymentRequest.SubmittedAtUtc,
            paymentRequest.CompletedAtUtc,
            paymentRequest.LastErrorText,
            latestExecution?.Id,
            latestExecution?.StatusCode,
            latestExecution?.ProviderStatusCode,
            latestExecution?.ProviderReference,
            providerContract);
    }

    public IReadOnlyCollection<ExternalCustomerDeliveryView> GetCustomerDeliveries(long partyId)
    {
        var party = GetPartyContext(partyId);
        if (IsSupplier(party))
        {
            return [];
        }

        var salesOrdersById = _dbContext.SalesOrders
            .AsNoTracking()
            .Where(entry => entry.CustomerId == partyId)
            .ToArray()
            .ToDictionary(entry => entry.Id);

        return _dbContext.DispatchOrders
            .AsNoTracking()
            .Where(entry => salesOrdersById.Keys.Contains(entry.SalesOrderId))
            .ToArray()
            .OrderByDescending(entry => entry.PlannedShipAtUtc ?? entry.CreatedAtUtc)
            .ThenByDescending(entry => entry.Id)
            .Select(entry =>
            {
                var salesOrder = salesOrdersById[entry.SalesOrderId];
                return new ExternalCustomerDeliveryView(
                    entry.Id,
                    entry.DispatchNumber,
                    entry.SalesOrderId,
                    salesOrder.SalesOrderNumber,
                    WorkflowStatusSeedIds.ToCode(entry.StatusId),
                    WorkflowStatusSeedIds.ToCode(salesOrder.StatusId),
                    entry.PlannedShipAtUtc,
                    entry.ShippedAtUtc,
                    entry.DeliveredAtUtc,
                    entry.ProofOfDeliveryReceiver,
                    entry.DeliveryConditionCode,
                    entry.DeliveryExceptionCode,
                    entry.DeliveryRecoveryStatusCode);
            })
            .ToArray();
    }

    public IReadOnlyCollection<ExternalCustomerServiceRequestView> GetCustomerServiceRequests(long customerPartyId)
    {
        var party = GetPartyContext(customerPartyId);
        if (IsSupplier(party))
        {
            return [];
        }

        return _dbContext.CustomerServiceRequests
            .AsNoTracking()
            .Where(entry => entry.CustomerPartyId == customerPartyId)
            .OrderByDescending(entry => entry.SubmittedAtUtc ?? entry.CreatedAtUtc)
            .ThenByDescending(entry => entry.Id)
            .Select(MapCustomerServiceRequest)
            .ToArray();
    }

    public ExternalCustomerServiceRequestView? GetCustomerServiceRequest(long customerPartyId, long serviceRequestId)
    {
        var party = GetPartyContext(customerPartyId);
        if (IsSupplier(party))
        {
            return null;
        }

        return _dbContext.CustomerServiceRequests
            .AsNoTracking()
            .Where(entry => entry.CustomerPartyId == customerPartyId && entry.Id == serviceRequestId)
            .Select(MapCustomerServiceRequest)
            .SingleOrDefault();
    }

    public ExternalCustomerServiceRequestView CreateCustomerServiceRequest(
        long customerPartyId,
        long actorUserId,
        string requestTypeCode,
        string subjectText,
        string detailsText,
        string priorityCode,
        string commercialImpactCode,
        string requestedResolutionCode,
        string assignedBackofficeLaneCode,
        string? relatedDocumentType,
        long? relatedDocumentId)
    {
        var party = GetPartyContext(customerPartyId);
        if (IsSupplier(party))
        {
            throw new InvalidOperationException("Only customer parties can create customer service requests.");
        }

        var nextSequence = (_dbContext.CustomerServiceRequests.Max(entry => (long?)entry.Id) ?? 0L) + 1L;
        var request = new CustomerServiceRequest(
            customerPartyId,
            $"CSR-{nextSequence:D6}",
            requestTypeCode,
            subjectText,
            detailsText,
            priorityCode,
            commercialImpactCode,
            requestedResolutionCode,
            assignedBackofficeLaneCode,
            relatedDocumentType,
            relatedDocumentId,
            actorUserId);

        _dbContext.CustomerServiceRequests.Add(request);
        _dbContext.SaveChanges();
        return GetCustomerServiceRequest(customerPartyId, request.Id)!;
    }

    public ExternalCustomerServiceRequestView SubmitCustomerServiceRequest(long customerPartyId, long serviceRequestId, long actorUserId)
    {
        var request = _dbContext.CustomerServiceRequests.SingleOrDefault(entry => entry.CustomerPartyId == customerPartyId && entry.Id == serviceRequestId)
            ?? throw new InvalidOperationException("Customer service request was not found for the signed-in customer.");

        request.Submit(actorUserId);
        _dbContext.SaveChanges();
        return GetCustomerServiceRequest(customerPartyId, serviceRequestId)!;
    }

    public ExternalCustomerServiceRequestView EscalateCustomerServiceRequest(long customerPartyId, long serviceRequestId, long actorUserId, string escalationReason, string? assignedBackofficeLaneCode, string? requestedResolutionCode)
    {
        var request = _dbContext.CustomerServiceRequests.SingleOrDefault(entry => entry.CustomerPartyId == customerPartyId && entry.Id == serviceRequestId)
            ?? throw new InvalidOperationException("Customer service request was not found for the signed-in customer.");

        request.Escalate(actorUserId, escalationReason, assignedBackofficeLaneCode, requestedResolutionCode);
        _dbContext.SaveChanges();
        return GetCustomerServiceRequest(customerPartyId, serviceRequestId)!;
    }

    public ExternalCustomerServiceRequestView RouteCustomerServiceRequest(long serviceRequestId, long actorUserId, string assignedBackofficeLaneCode, string? requestedResolutionCode)
    {
        var request = _dbContext.CustomerServiceRequests.SingleOrDefault(entry => entry.Id == serviceRequestId)
            ?? throw new InvalidOperationException($"Customer service request '{serviceRequestId}' was not found.");

        request.RouteCommercialEscalation(actorUserId, assignedBackofficeLaneCode, requestedResolutionCode);
        _dbContext.SaveChanges();
        return GetCustomerServiceRequest(request.CustomerPartyId, request.Id)!;
    }

    public ExternalCustomerServiceRequestView StartCustomerServiceRequestReview(long serviceRequestId, long actorUserId)
    {
        var request = _dbContext.CustomerServiceRequests.SingleOrDefault(entry => entry.Id == serviceRequestId)
            ?? throw new InvalidOperationException($"Customer service request '{serviceRequestId}' was not found.");

        request.StartReview(actorUserId);
        _dbContext.SaveChanges();
        return GetCustomerServiceRequest(request.CustomerPartyId, request.Id)!;
    }

    public ExternalCustomerServiceRequestView RespondToCustomerServiceRequest(long serviceRequestId, long actorUserId, string responseText)
    {
        var request = _dbContext.CustomerServiceRequests.SingleOrDefault(entry => entry.Id == serviceRequestId)
            ?? throw new InvalidOperationException($"Customer service request '{serviceRequestId}' was not found.");

        request.Respond(actorUserId, responseText);
        _dbContext.SaveChanges();
        return GetCustomerServiceRequest(request.CustomerPartyId, request.Id)!;
    }

    public ExternalCustomerServiceRequestView RejectCustomerServiceRequest(long serviceRequestId, long actorUserId, string responseText)
    {
        var request = _dbContext.CustomerServiceRequests.SingleOrDefault(entry => entry.Id == serviceRequestId)
            ?? throw new InvalidOperationException($"Customer service request '{serviceRequestId}' was not found.");

        request.Reject(actorUserId, responseText);
        _dbContext.SaveChanges();
        return GetCustomerServiceRequest(request.CustomerPartyId, request.Id)!;
    }

    public ExternalCustomerServiceRequestView CloseCustomerServiceRequest(long serviceRequestId, long actorUserId, string? resolutionNotes)
    {
        var request = _dbContext.CustomerServiceRequests.SingleOrDefault(entry => entry.Id == serviceRequestId)
            ?? throw new InvalidOperationException($"Customer service request '{serviceRequestId}' was not found.");

        request.Close(actorUserId, resolutionNotes);
        _dbContext.SaveChanges();
        return GetCustomerServiceRequest(request.CustomerPartyId, request.Id)!;
    }

    public ExternalStatementSummaryView GetStatementSummary(long partyId, DateOnly? dateFrom = null, DateOnly? dateTo = null, string? currencyCode = null)
    {
        var party = GetPartyContext(partyId);
        ValidateRange(dateFrom, dateTo);

        var normalizedCurrencyCode = NormalizeOptionalCurrencyCode(currencyCode);
        var quotations = FilterByDate(GetQuotations(partyId), dateFrom, dateTo, item => item.QuoteDate)
            .Where(item => normalizedCurrencyCode is null || string.Equals(item.CurrencyCode, normalizedCurrencyCode, StringComparison.OrdinalIgnoreCase))
            .ToArray();
        var orders = FilterByDate(GetOrders(partyId), dateFrom, dateTo, item => item.OrderDate)
            .Where(item => normalizedCurrencyCode is null || string.Equals(item.CurrencyCode, normalizedCurrencyCode, StringComparison.OrdinalIgnoreCase))
            .ToArray();
        var invoices = FilterByDate(GetInvoices(partyId), dateFrom, dateTo, item => item.InvoiceDate)
            .ToArray();
        var payments = FilterByDate(GetPayments(partyId), dateFrom, dateTo, item => item.PaymentDate)
            .ToArray();
        var resolvedCurrencyCode = ResolveStatementCurrencyCode(
            normalizedCurrencyCode,
            invoices.Select(item => item.CurrencyCode).Concat(payments.Select(item => item.CurrencyCode)));
        invoices = invoices.Where(item => string.Equals(item.CurrencyCode, resolvedCurrencyCode, StringComparison.OrdinalIgnoreCase)).ToArray();
        payments = payments.Where(item => string.Equals(item.CurrencyCode, resolvedCurrencyCode, StringComparison.OrdinalIgnoreCase)).ToArray();
        var statementLines = GetStatementLines(partyId, dateFrom, dateTo, resolvedCurrencyCode);

        return new ExternalStatementSummaryView(
            party.Id,
            party.PartyCode,
            party.PartyName,
            party.PartyType,
            dateFrom,
            dateTo,
            resolvedCurrencyCode,
            quotations.Length,
            orders.Length,
            invoices.Length,
            payments.Length,
            invoices.Where(item => item.OpenAmount > 0m).Sum(item => item.OpenAmount),
            invoices.Where(item => item.OpenAmount > 0m && item.DueDate.HasValue && item.DueDate.Value < DateOnly.FromDateTime(DateTime.UtcNow)).Sum(item => item.OpenAmount),
            payments.Sum(item => item.UnappliedAmount),
            statementLines.LastOrDefault()?.RunningBalanceAmount ?? 0m);
    }

    public IReadOnlyCollection<ExternalStatementLineView> GetStatementLines(long partyId, DateOnly? dateFrom = null, DateOnly? dateTo = null, string? currencyCode = null)
    {
        var party = GetPartyContext(partyId);
        ValidateRange(dateFrom, dateTo);
        var normalizedCurrencyCode = NormalizeOptionalCurrencyCode(currencyCode);

        var lines = IsSupplier(party)
            ? BuildSupplierStatementLines(partyId)
            : BuildCustomerStatementLines(partyId);
        var resolvedCurrencyCode = ResolveStatementCurrencyCode(
            normalizedCurrencyCode,
            lines.Select(item => item.CurrencyCode));

        var filtered = FilterByDate(lines, dateFrom, dateTo, item => item.BusinessDate)
            .Where(item => string.Equals(item.CurrencyCode, resolvedCurrencyCode, StringComparison.OrdinalIgnoreCase))
            .OrderBy(item => item.BusinessDate)
            .ThenBy(item => item.DocumentNumber)
            .ToArray();

        var runningBalance = 0m;
        var normalized = new List<ExternalStatementLineView>(filtered.Length);
        foreach (var line in filtered)
        {
            runningBalance += line.ChargeAmount - line.SettlementAmount;
            normalized.Add(line with { RunningBalanceAmount = runningBalance });
        }

        return normalized;
    }

    public IReadOnlyCollection<ExternalStatementDocumentView> GetStatementDocuments(long partyId)
    {
        var party = GetPartyContext(partyId);
        if (IsSupplier(party))
        {
            var deliveryLogCounts = _dbContext.SupplierStatementDeliveryLogs
                .AsNoTracking()
                .ToArray()
                .GroupBy(entry => entry.SupplierStatementId)
                .ToDictionary(group => group.Key, group => group.Count());

            return _dbContext.SupplierStatements
                .AsNoTracking()
                .Where(entry => entry.SupplierId == partyId)
                .ToArray()
                .OrderByDescending(entry => entry.GeneratedAtUtc)
                .ThenByDescending(entry => entry.Id)
                .Select(entry => new ExternalStatementDocumentView(
                    "SUPPLIER_STATEMENT",
                    entry.Id,
                    entry.StatementNumber,
                    entry.DateFrom,
                    entry.DateTo,
                    entry.CurrencyCode,
                    entry.ClosingBalanceAmount,
                    entry.OpenPayableAmount,
                    entry.OverduePayableAmount,
                    deliveryLogCounts.GetValueOrDefault(entry.Id),
                    entry.GeneratedAtUtc))
                .ToArray();
        }

        var customerDeliveryLogCounts = _dbContext.CustomerStatementDeliveryLogs
            .AsNoTracking()
            .ToArray()
            .GroupBy(entry => entry.CustomerStatementId)
            .ToDictionary(group => group.Key, group => group.Count());

        return _dbContext.CustomerStatements
            .AsNoTracking()
            .Where(entry => entry.CustomerId == partyId)
            .ToArray()
            .OrderByDescending(entry => entry.GeneratedAtUtc)
            .ThenByDescending(entry => entry.Id)
            .Select(entry => new ExternalStatementDocumentView(
                "CUSTOMER_STATEMENT",
                entry.Id,
                entry.StatementNumber,
                entry.DateFrom,
                entry.DateTo,
                entry.CurrencyCode,
                entry.ClosingBalanceAmount,
                entry.OpenInvoiceAmount,
                entry.OverdueInvoiceAmount,
                customerDeliveryLogCounts.GetValueOrDefault(entry.Id),
                entry.GeneratedAtUtc))
                .ToArray();
    }

    public ExternalStatementDetailView? GetStatementDetail(long partyId, long statementId)
    {
        var statement = GetStatementDocuments(partyId)
            .SingleOrDefault(entry => entry.StatementId == statementId);

        if (statement is null)
        {
            return null;
        }

        var lines = GetStatementLines(partyId, statement.DateFrom, statement.DateTo, statement.CurrencyCode);
        var deliveryLogs = GetStatementDeliveryLogs(partyId, statementId);

        return new ExternalStatementDetailView(
            statement.StatementType,
            statement.StatementId,
            statement.StatementNumber,
            statement.DateFrom,
            statement.DateTo,
            statement.CurrencyCode,
            statement.ClosingBalanceAmount,
            statement.OpenAmount,
            statement.OverdueAmount,
            statement.DeliveryLogCount,
            statement.GeneratedAtUtc,
            lines,
            deliveryLogs);
    }

    public IReadOnlyCollection<ExternalStatementDeliveryLogView> GetStatementDeliveryLogs(long partyId, long statementId)
    {
        var party = GetPartyContext(partyId);
        if (IsSupplier(party))
        {
            var statementExists = _dbContext.SupplierStatements
                .AsNoTracking()
                .Any(entry => entry.Id == statementId && entry.SupplierId == partyId);
            if (!statementExists)
            {
                return [];
            }

            return _dbContext.SupplierStatementDeliveryLogs
                .AsNoTracking()
                .Where(entry => entry.SupplierStatementId == statementId)
                .OrderByDescending(entry => entry.DeliveredAtUtc)
                .ThenByDescending(entry => entry.Id)
                .Select(entry => new ExternalStatementDeliveryLogView(
                    "SUPPLIER_STATEMENT",
                    entry.SupplierStatementId,
                    entry.Id,
                    entry.DeliveredAtUtc,
                    entry.ChannelCode,
                    entry.RecipientAddress,
                    entry.DeliveryStatusCode,
                    entry.AcknowledgmentStatusCode,
                    entry.AcknowledgedAtUtc,
                    entry.AcknowledgedByUserId,
                    entry.Notes))
                .ToArray();
        }

        var customerStatementExists = _dbContext.CustomerStatements
            .AsNoTracking()
            .Any(entry => entry.Id == statementId && entry.CustomerId == partyId);
        if (!customerStatementExists)
        {
            return [];
        }

        return _dbContext.CustomerStatementDeliveryLogs
            .AsNoTracking()
            .Where(entry => entry.CustomerStatementId == statementId)
            .OrderByDescending(entry => entry.DeliveredAtUtc)
            .ThenByDescending(entry => entry.Id)
            .Select(entry => new ExternalStatementDeliveryLogView(
                "CUSTOMER_STATEMENT",
                entry.CustomerStatementId,
                entry.Id,
                entry.DeliveredAtUtc,
                entry.ChannelCode,
                entry.RecipientAddress,
                entry.DeliveryStatusCode,
                null,
                null,
                null,
                entry.Notes))
            .ToArray();
    }

    public IReadOnlyCollection<IslamicFinanceRequest> GetFinancingRequests(long partyId)
    {
        GetPartyContext(partyId);

        return _dbContext.IslamicFinanceRequests
            .AsNoTracking()
            .Where(entry => entry.ApplicantPartyId == partyId)
            .OrderByDescending(entry => entry.Id)
            .ToArray();
    }

    public IslamicFinanceRequest? GetFinancingRequest(long partyId, long financingRequestId)
    {
        GetPartyContext(partyId);

        return _dbContext.IslamicFinanceRequests
            .AsNoTracking()
            .SingleOrDefault(entry => entry.ApplicantPartyId == partyId && entry.Id == financingRequestId);
    }

    public IReadOnlyCollection<IslamicFinanceReceivablesCase> GetReceivablesCases(long partyId)
    {
        var party = GetPartyContext(partyId);
        if (IsSupplier(party))
        {
            return [];
        }

        return _dbContext.IslamicFinanceReceivablesCases
            .AsNoTracking()
            .Where(entry => entry.CustomerPartyId == partyId)
            .OrderByDescending(entry => entry.Id)
            .ToArray();
    }

    public IReadOnlyCollection<IslamicFinanceMurabahaCase> GetMurabahaCases(long partyId)
    {
        var party = GetPartyContext(partyId);
        var query = _dbContext.IslamicFinanceMurabahaCases.AsNoTracking();

        return (IsSupplier(party)
                ? query.Where(entry => entry.SupplierPartyId == partyId)
                : query.Where(entry => entry.CustomerPartyId == partyId))
            .OrderByDescending(entry => entry.Id)
            .ToArray();
    }

    public IReadOnlyCollection<IslamicFinanceMurabahaDisbursement> GetMurabahaDisbursements(long partyId)
    {
        var party = GetPartyContext(partyId);
        var query = _dbContext.IslamicFinanceMurabahaDisbursements.AsNoTracking();

        return (IsSupplier(party)
                ? query.Where(entry => entry.SupplierPartyId == partyId)
                : query.Where(entry => entry.CustomerPartyId == partyId))
            .OrderByDescending(entry => entry.Id)
            .ToArray();
    }

    public IReadOnlyCollection<IslamicFinanceSettlementHeader> GetSettlements(long partyId)
    {
        var party = GetPartyContext(partyId);
        var caseIds = GetMurabahaCases(partyId)
            .Select(entry => entry.Id)
            .ToArray();

        if (caseIds.Length == 0)
        {
            return [];
        }

        return _dbContext.IslamicFinanceSettlementHeaders
            .AsNoTracking()
            .Where(entry => caseIds.Contains(entry.MurabahaCaseId))
            .OrderByDescending(entry => entry.Id)
            .ToArray();
    }

    public SupplierProfileDetailView? GetSupplierProfileByPartyId(long partyId)
    {
        var party = GetPartyContext(partyId);
        if (!IsSupplier(party))
        {
            return null;
        }

        var profile = _dbContext.SupplierProfiles
            .AsNoTracking()
            .SingleOrDefault(entry => entry.BusinessPartyId == partyId);

        return profile is null ? null : new SupplierProfileRepository(_dbContext).GetSupplierProfile(profile.Id);
    }

    private IReadOnlyCollection<ExternalStatementLineView> BuildSupplierStatementLines(long partyId)
    {
        var invoiceLines = _dbContext.AccountsPayableBills
            .AsNoTracking()
            .Where(entry => entry.SupplierId == partyId && entry.StatusId == WorkflowStatusSeedIds.Posted)
            .ToArray()
            .Select(entry =>
            {
                var signedTotal = GetSupplierInvoiceSignedAmount(entry.BillType, entry.TotalAmount);
                return new ExternalStatementLineView(
                    "INVOICE",
                    entry.BillType,
                    entry.Id,
                    entry.BillNumber,
                    entry.BillDate,
                    entry.DueDate,
                    entry.CurrencyCode,
                    signedTotal > 0m ? signedTotal : 0m,
                    signedTotal < 0m ? Math.Abs(signedTotal) : 0m,
                    0m,
                    WorkflowStatusSeedIds.ToCode(entry.StatusId),
                    entry.PurchaseOrderId?.ToString());
            });

        var paymentLines = _dbContext.AccountsPayablePayments
            .AsNoTracking()
            .Where(entry => entry.SupplierId == partyId && entry.StatusId == WorkflowStatusSeedIds.Posted)
            .ToArray()
            .Select(entry => new ExternalStatementLineView(
                "PAYMENT",
                "AP_PAYMENT",
                entry.Id,
                entry.PaymentNumber,
                entry.PaymentDate,
                null,
                entry.CurrencyCode,
                0m,
                entry.AmountAmount,
                0m,
                WorkflowStatusSeedIds.ToCode(entry.StatusId),
                entry.ReferenceNumber));

        return invoiceLines.Concat(paymentLines).ToArray();
    }

    private IReadOnlyCollection<ExternalStatementLineView> BuildCustomerStatementLines(long partyId)
    {
        var invoiceLines = _dbContext.AccountsReceivableInvoices
            .AsNoTracking()
            .Where(entry => entry.CustomerId == partyId && entry.StatusId == WorkflowStatusSeedIds.Posted)
            .ToArray()
            .Select(entry =>
            {
                var signedTotal = GetCustomerInvoiceSignedAmount(entry.InvoiceType, entry.TotalAmount);
                return new ExternalStatementLineView(
                    "INVOICE",
                    entry.InvoiceType,
                    entry.Id,
                    entry.InvoiceNumber,
                    entry.InvoiceDate,
                    entry.DueDate,
                    entry.CurrencyCode,
                    signedTotal > 0m ? signedTotal : 0m,
                    signedTotal < 0m ? Math.Abs(signedTotal) : 0m,
                    0m,
                    WorkflowStatusSeedIds.ToCode(entry.StatusId),
                    entry.EtaUuid ?? entry.DispatchOrderId?.ToString() ?? entry.SalesOrderId?.ToString());
            });

        var receiptLines = _dbContext.AccountsReceivableReceipts
            .AsNoTracking()
            .Where(entry => entry.CustomerId == partyId && entry.StatusId == WorkflowStatusSeedIds.Posted)
            .ToArray()
            .Select(entry => new ExternalStatementLineView(
                "PAYMENT",
                "AR_RECEIPT",
                entry.Id,
                entry.ReceiptNumber,
                entry.ReceiptDate,
                null,
                entry.CurrencyCode,
                0m,
                entry.AmountAmount,
                0m,
                WorkflowStatusSeedIds.ToCode(entry.StatusId),
                entry.ReferenceNumber));

        var refundLines = GetPostedCustomerRefundsSafe(partyId)
            .Select(entry => new ExternalStatementLineView(
                "ADJUSTMENT",
                "CUSTOMER_REFUND",
                entry.Id,
                entry.RefundNumber,
                entry.RefundDate,
                null,
                entry.CurrencyCode,
                entry.AmountAmount,
                0m,
                0m,
                WorkflowStatusSeedIds.ToCode(entry.StatusId),
                entry.ReferenceNumber));

        return invoiceLines.Concat(receiptLines).Concat(refundLines).ToArray();
    }

    private PartyContext GetPartyContext(long partyId)
    {
        var party = _dbContext.BusinessParties
            .AsNoTracking()
            .Where(entry => entry.Id == partyId && entry.IsActive)
            .Select(entry => new PartyContext(entry.Id, entry.PartyCode, entry.PartyName, entry.PartyType))
            .SingleOrDefault();

        return party ?? throw new InvalidOperationException($"Active business party '{partyId}' was not found.");
    }

    private CounterpartyDocumentWorkflow GetRequiredDocumentWorkflow(long partyId, long workflowId) =>
        _dbContext.CounterpartyDocumentWorkflows.SingleOrDefault(entry => entry.Id == workflowId && entry.PartyId == partyId)
        ?? throw new InvalidOperationException("Counterparty document workflow was not found for the signed-in party.");

    private DocumentLink CreateManagedPartyDocument(
        long partyId,
        long actorUserId,
        string documentType,
        string fileName,
        string storageReference,
        string? contentType,
        int? fileSizeBytes)
    {
        EnsureNoDuplicateActiveStorageReference("PARTY", "BUSINESS_PARTY", partyId, documentType, storageReference);

        var document = new DocumentLink(
            moduleCode: "PARTY",
            entityName: "BUSINESS_PARTY",
            entityId: partyId,
            documentType: documentType.Trim().ToUpperInvariant(),
            attachmentCategoryCode: DocumentAttachmentCategoryCode.Compliance,
            fileName: fileName.Trim(),
            storageReference: storageReference.Trim(),
            storageProviderCode: DocumentStorageProviderCode.ManagedStorage,
            contentType: string.IsNullOrWhiteSpace(contentType) ? null : contentType.Trim(),
            fileSizeBytes: fileSizeBytes,
            checksumText: null,
            retentionUntilUtc: null,
            versionNumber: 1,
            uploadedByUserId: actorUserId);

        _dbContext.DocumentLinks.Add(document);
        _dbContext.SaveChanges();
        return document;
    }

    private void EnsureNoDuplicateActiveStorageReference(
        string moduleCode,
        string entityName,
        long entityId,
        string documentType,
        string storageReference,
        long? excludedDocumentLinkId = null)
    {
        var normalizedModuleCode = moduleCode.Trim().ToUpperInvariant();
        var normalizedEntityName = entityName.Trim().ToUpperInvariant();
        var normalizedDocumentType = documentType.Trim().ToUpperInvariant();
        var normalizedStorageReference = storageReference.Trim();

        var query = _dbContext.DocumentLinks.Where(document =>
            document.ModuleCode == normalizedModuleCode &&
            document.EntityName == normalizedEntityName &&
            document.EntityId == entityId &&
            document.DocumentType == normalizedDocumentType &&
            document.StorageReference == normalizedStorageReference &&
            document.LifecycleStatusCode == DocumentLifecycleStatusCode.Active);

        if (excludedDocumentLinkId.HasValue)
        {
            query = query.Where(document => document.Id != excludedDocumentLinkId.Value);
        }

        if (query.Any())
        {
            throw new InvalidOperationException("An active document attachment already uses the same storage reference for this party and document type.");
        }
    }

    private static ExternalPurchaseOrderView MapPurchaseOrder(PurchaseOrder entry) =>
        new(
            entry.Id,
            entry.PurchaseOrderNumber,
            entry.PurchaseOrderDate,
            entry.CurrencyCode,
            WorkflowStatusSeedIds.ToCode(entry.StatusId),
            entry.TotalAmount,
            entry.PurchaseRequisitionId,
            entry.SupplierAcknowledgmentStatusCode,
            entry.SupplierAcknowledgedAtUtc,
            entry.SupplierAcknowledgedByUserId,
            entry.SupplierAcknowledgmentNotes);

    private SupplierQuotationDetailView? GetSupplierQuotationDetail(long quotationId)
    {
        var quotation = _dbContext.SupplierQuotations
            .AsNoTracking()
            .Include(entry => entry.Lines)
            .SingleOrDefault(entry => entry.Id == quotationId);

        return quotation is null
            ? null
            : new SupplierQuotationDetailView(
                quotation.Id,
                quotation.QuotationNumber,
                quotation.RequestForQuotationId,
                quotation.SupplierId,
                quotation.QuoteDate,
                quotation.ValidUntilDate,
                quotation.CurrencyCode,
                quotation.FxRate,
                WorkflowStatusSeedIds.ToCode(quotation.StatusId),
                quotation.SubtotalAmount,
                quotation.DiscountAmount,
                quotation.TaxAmount,
                quotation.TotalAmount,
                quotation.IsSelected,
                quotation.Notes,
                quotation.Lines.OrderBy(line => line.LineNumber).Select(line => new SupplierQuotationLineView(
                    line.Id,
                    line.LineNumber,
                    line.RequestForQuotationLineId,
                    line.ItemId,
                    line.QuotedQuantity,
                    line.UnitOfMeasureCode,
                    line.UnitPrice,
                    line.DiscountPercent,
                    line.DiscountAmount,
                    line.TaxCode,
                    line.LineNetAmount,
                    line.LineTaxAmount,
                    line.LineTotalAmount,
                    line.LeadTimeDays)).ToArray());
    }

    private static ExternalDocumentWorkflowView? MapDocumentWorkflow(CounterpartyDocumentWorkflow workflow, DocumentLink? document)
    {
        if (document is null)
        {
            return null;
        }

        return new ExternalDocumentWorkflowView(
            workflow.Id,
            workflow.PartyId,
            workflow.PartyTypeCode,
            document.Id,
            document.DocumentType,
            document.FileName,
            document.StorageReference,
            document.ContentType,
            document.FileSizeBytes,
            document.VersionNumber,
            workflow.StatusCode,
            workflow.WorkflowNotesText,
            workflow.ReviewNotesText,
            document.UploadedAtUtc,
            document.UploadedByUserId,
            workflow.SubmittedAtUtc,
            workflow.SubmittedByUserId,
            workflow.ReviewedAtUtc,
            workflow.ReviewedByUserId,
            workflow.ReturnedAtUtc,
            workflow.ReturnedByUserId,
            workflow.RejectedAtUtc,
            workflow.RejectedByUserId);
    }

    private static ExternalCustomerServiceRequestView MapCustomerServiceRequest(CustomerServiceRequest entry) =>
        new(
            entry.Id,
            entry.CustomerPartyId,
            entry.RequestNumber,
            entry.RequestTypeCode,
            entry.SubjectText,
            entry.DetailsText,
            entry.PriorityCode,
            entry.StatusCode,
            entry.CommercialImpactCode,
            entry.RequestedResolutionCode,
            entry.AssignedBackofficeLaneCode,
            entry.EscalationLevel,
            entry.RelatedDocumentType,
            entry.RelatedDocumentId,
            entry.InternalResponseText,
            entry.EscalationReasonText,
            entry.CreatedAtUtc,
            entry.CreatedByUserId,
            entry.SubmittedAtUtc,
            entry.SubmittedByUserId,
            entry.ReviewedAtUtc,
            entry.ReviewedByUserId,
            entry.EscalatedAtUtc,
            entry.EscalatedByUserId,
            entry.RespondedAtUtc,
            entry.RespondedByUserId,
            entry.RejectedAtUtc,
            entry.RejectedByUserId,
            entry.ClosedAtUtc,
            entry.ClosedByUserId);

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

    private static decimal GetCustomerInvoiceSignedAmount(string invoiceType, decimal amount) =>
        invoiceType switch
        {
            "CREDIT" or "CREDIT_NOTE" or "CREDIT_MEMO" => -amount,
            _ => amount
        };

    private PaymentChannelConfiguration ResolveCustomerCollectionChannel(string integrationProviderCode, string currencyCode)
    {
        var normalizedProviderCode = integrationProviderCode.Trim().ToUpperInvariant();
        var normalizedCurrencyCode = currencyCode.Trim().ToUpperInvariant();
        var channel = _dbContext.PaymentChannelConfigurations
            .AsNoTracking()
            .Where(entry =>
                entry.IsActive
                && entry.UsageCode == "CUSTOMER_COLLECTION"
                && entry.IntegrationProviderCode == normalizedProviderCode
                && entry.CurrencyCode == normalizedCurrencyCode)
            .OrderByDescending(entry => entry.Id)
            .FirstOrDefault();

        return channel
            ?? throw new InvalidOperationException($"Customer collection channel for provider '{normalizedProviderCode}' and currency '{normalizedCurrencyCode}' was not found.");
    }

    private static bool IsSupplier(PartyContext party) =>
        string.Equals(party.PartyType, "SUPPLIER", StringComparison.OrdinalIgnoreCase);

    private static string BuildExternalNotificationRecipientReference(long partyId) => $"PARTY:{partyId}";

    private static ExternalFinancingTaskView BuildPrimaryFinancingTask(
        IslamicFinanceRequest request,
        int offerCount,
        int notificationCount,
        int partnerRoutingNotificationCount,
        bool hasMurabahaCase,
        bool hasReceivablesCase,
        bool hasPendingDisbursement,
        bool hasOpenSettlement,
        DateTime? dueAtUtc)
    {
        if (string.Equals(request.StatusCode, "DRAFT", StringComparison.OrdinalIgnoreCase)
            || string.Equals(request.StatusCode, "RETURNED", StringComparison.OrdinalIgnoreCase))
        {
            return new ExternalFinancingTaskView(
                request.Id,
                request.RequestNumber,
                request.ApplicantPartyTypeCode,
                "SUBMIT_FINANCING_APPLICATION",
                "Submit financing application",
                "APPLICATION",
                "OPEN",
                "Complete the request and send it back into the financing workflow.",
                notificationCount,
                dueAtUtc);
        }

        if (string.Equals(request.StatusCode, "SUBMITTED", StringComparison.OrdinalIgnoreCase))
        {
            return new ExternalFinancingTaskView(
                request.Id,
                request.RequestNumber,
                request.ApplicantPartyTypeCode,
                "WAIT_FOR_INTERNAL_REVIEW",
                "Wait for internal review",
                "REVIEW",
                "WAITING",
                "CashLine is reviewing the financing request and will publish the next outcome here.",
                notificationCount,
                dueAtUtc);
        }

        if (string.Equals(request.StatusCode, "REJECTED", StringComparison.OrdinalIgnoreCase))
        {
            return new ExternalFinancingTaskView(
                request.Id,
                request.RequestNumber,
                request.ApplicantPartyTypeCode,
                "REVIEW_REJECTION_OUTCOME",
                "Review rejection outcome",
                "REVIEW",
                "OPEN",
                "Review the rejection outcome and any return guidance before creating a replacement request if needed.",
                notificationCount,
                null);
        }

        if (!request.SelectedFinancingDecisionId.HasValue && offerCount > 0)
        {
            return new ExternalFinancingTaskView(
                request.Id,
                request.RequestNumber,
                request.ApplicantPartyTypeCode,
                "COMPARE_AND_SELECT_OFFER",
                "Compare and select lender offer",
                "OFFERS",
                "OPEN",
                "Review published lender offers and select the preferred financing option.",
                notificationCount,
                dueAtUtc);
        }

        if ((string.Equals(request.StatusCode, "APPROVED", StringComparison.OrdinalIgnoreCase)
                || string.Equals(request.StatusCode, "ASSIGNED", StringComparison.OrdinalIgnoreCase))
            && !request.SelectedFinancingDecisionId.HasValue
            && offerCount == 0
            && partnerRoutingNotificationCount > 0)
        {
            return new ExternalFinancingTaskView(
                request.Id,
                request.RequestNumber,
                request.ApplicantPartyTypeCode,
                "WAIT_FOR_PARTNER_OFFERS",
                "Wait for partner offers",
                "OFFERS",
                "WAITING",
                "CashLine has routed this request to compatible lender partners and is collecting financing offers.",
                notificationCount,
                dueAtUtc);
        }

        if ((string.Equals(request.StatusCode, "APPROVED", StringComparison.OrdinalIgnoreCase)
                || string.Equals(request.StatusCode, "ASSIGNED", StringComparison.OrdinalIgnoreCase))
            && !request.SelectedFinancingDecisionId.HasValue
            && offerCount == 0
            && request.AssignedPartnerLenderId.HasValue)
        {
            return new ExternalFinancingTaskView(
                request.Id,
                request.RequestNumber,
                request.ApplicantPartyTypeCode,
                "WAIT_FOR_PARTNER_RESPONSE",
                "Wait for partner response",
                "OFFERS",
                "WAITING",
                "CashLine has assigned the request to a financing partner and is waiting for offer publication.",
                notificationCount,
                dueAtUtc);
        }

        if (string.Equals(request.StatusCode, "APPROVED", StringComparison.OrdinalIgnoreCase)
            && !request.AssignedPartnerLenderId.HasValue
            && offerCount == 0)
        {
            return new ExternalFinancingTaskView(
                request.Id,
                request.RequestNumber,
                request.ApplicantPartyTypeCode,
                "WAIT_FOR_PARTNER_ROUTING",
                "Wait for partner routing",
                "OFFERS",
                "WAITING",
                "CashLine is preparing compatible partner-lender routing for this financing request.",
                notificationCount,
                dueAtUtc);
        }

        if (hasOpenSettlement)
        {
            return new ExternalFinancingTaskView(
                request.Id,
                request.RequestNumber,
                request.ApplicantPartyTypeCode,
                "TRACK_SETTLEMENT",
                "Track settlement progress",
                "EXECUTION",
                "OPEN",
                "Monitor settlement progress and watch for final closure updates.",
                notificationCount,
                null);
        }

        if (hasPendingDisbursement)
        {
            return new ExternalFinancingTaskView(
                request.Id,
                request.RequestNumber,
                request.ApplicantPartyTypeCode,
                "TRACK_DISBURSEMENT",
                "Track disbursement progress",
                "EXECUTION",
                "OPEN",
                "Disbursement is active for this request and should be monitored until execution completes.",
                notificationCount,
                null);
        }

        if (hasReceivablesCase)
        {
            return new ExternalFinancingTaskView(
                request.Id,
                request.RequestNumber,
                request.ApplicantPartyTypeCode,
                "TRACK_RECEIVABLES_CASE",
                "Track receivables financing case",
                "EXECUTION",
                "WAITING",
                "Receivables-finance execution is active and should be tracked through collection and settlement.",
                notificationCount,
                null);
        }

        if (hasMurabahaCase)
        {
            return new ExternalFinancingTaskView(
                request.Id,
                request.RequestNumber,
                request.ApplicantPartyTypeCode,
                "TRACK_EXECUTION_CASE",
                "Track execution case",
                "EXECUTION",
                "WAITING",
                "Execution is in progress for this financing request and new updates will appear here.",
                notificationCount,
                null);
        }

        if (request.SelectedFinancingDecisionId.HasValue)
        {
            return new ExternalFinancingTaskView(
                request.Id,
                request.RequestNumber,
                request.ApplicantPartyTypeCode,
                "WAIT_FOR_EXECUTION_START",
                "Wait for execution start",
                "EXECUTION",
                "WAITING",
                "The financing offer was selected and CashLine is preparing execution follow-up.",
                notificationCount,
                dueAtUtc);
        }

        return new ExternalFinancingTaskView(
            request.Id,
            request.RequestNumber,
            request.ApplicantPartyTypeCode,
            "MONITOR_FINANCING_WORKFLOW",
            "Monitor financing workflow",
            "REVIEW",
            "WAITING",
            "This request is moving through the financing workflow and will surface the next action when it changes.",
            notificationCount,
            dueAtUtc);
    }

    private static DateTime? ResolveExternalTaskDueAtUtc(IslamicFinanceRequest request)
    {
        if (string.Equals(request.StatusCode, "DRAFT", StringComparison.OrdinalIgnoreCase)
            || string.Equals(request.StatusCode, "RETURNED", StringComparison.OrdinalIgnoreCase))
        {
            return null;
        }

        var anchor = request.SubmittedAtUtc ?? request.CreatedAtUtc;
        return anchor.AddDays(2);
    }

    private static int ResolveTaskSortPriority(string taskStatusCode, string taskCategoryCode)
    {
        if (string.Equals(taskStatusCode, "OPEN", StringComparison.OrdinalIgnoreCase)
            && string.Equals(taskCategoryCode, "OFFERS", StringComparison.OrdinalIgnoreCase))
        {
            return 0;
        }

        if (string.Equals(taskStatusCode, "OPEN", StringComparison.OrdinalIgnoreCase))
        {
            return 1;
        }

        if (string.Equals(taskCategoryCode, "NOTIFICATIONS", StringComparison.OrdinalIgnoreCase))
        {
            return 2;
        }

        return 3;
    }

    private static string? NormalizeOptionalCurrencyCode(string? currencyCode) =>
        string.IsNullOrWhiteSpace(currencyCode) ? null : currencyCode.Trim().ToUpperInvariant();

    private static string ResolveStatementCurrencyCode(string? requestedCurrencyCode, IEnumerable<string> currencies)
    {
        if (!string.IsNullOrWhiteSpace(requestedCurrencyCode))
        {
            return requestedCurrencyCode;
        }

        var distinctCurrencies = currencies
            .Where(currency => !string.IsNullOrWhiteSpace(currency))
            .Select(currency => currency.Trim().ToUpperInvariant())
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .ToArray();

        return distinctCurrencies.Length switch
        {
            0 => "EGP",
            1 => distinctCurrencies[0],
            _ => throw new InvalidOperationException("External statement reporting requires currencyCode when multiple document currencies exist for the selected range.")
        };
    }

    private static IReadOnlyCollection<T> FilterByDate<T>(
        IReadOnlyCollection<T> items,
        DateOnly? dateFrom,
        DateOnly? dateTo,
        Func<T, DateOnly> selector) =>
        items.Where(item =>
                (!dateFrom.HasValue || selector(item) >= dateFrom.Value) &&
                (!dateTo.HasValue || selector(item) <= dateTo.Value))
            .ToArray();

    private static void ValidateRange(DateOnly? dateFrom, DateOnly? dateTo)
    {
        if (dateFrom.HasValue && dateTo.HasValue && dateFrom.Value > dateTo.Value)
        {
            throw new InvalidOperationException("External portal date range is invalid because dateFrom is after dateTo.");
        }
    }

    private sealed record PartyContext(long Id, string PartyCode, string PartyName, string PartyType);
}
