using CashLineERP.Contracts.External;
using CashLineERP.Contracts.IslamicFinance;
using CashLineERP.Contracts.Parties;
using CashLineERP.Contracts.Procurement;
using CashLineERP.Application.External.Views;
using CashLineERP.Application.Customers;
using CashLineERP.Application.Finance;
using CashLineERP.Application.IslamicFinance.Disbursements;
using CashLineERP.Application.IslamicFinance.ProductProfiles;
using CashLineERP.Application.IslamicFinance.ReceivablesFinance;
using CashLineERP.Application.IslamicFinance.Requests;
using CashLineERP.Application.IslamicFinance.Settlements;
using CashLineERP.Application.Notifications;
using CashLineERP.Application.Parties;
using CashLineERP.Application.Procurement;
using CashLineERP.Application.Sales;
using CashLineERP.Application.Suppliers;
using CashLineERP.Contracts.Customers;
using CashLineERP.Contracts.Finance;
using CashLineERP.Contracts.Notifications;
using CashLineERP.Contracts.Sales;
using CashLineERP.Contracts.Suppliers;
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;

namespace CashLineERP.Application.External;

public sealed class ExternalPortalService : IExternalPortalService
{
    private const string ExternalFinancingRequestSubmittedTemplateCode = "NTTMP-000002";
    private const string OfficerFinancingRequestSubmittedTemplateCode = "NTTMP-000003";
    private const string ExternalFinancingOfferAcceptedTemplateCode = "NTTMP-000004";
    private const string OfficerFinancingOfferAcceptedTemplateCode = "NTTMP-000005";
    private const string OfficerFinancingRecipientReference = "ROLE:ISLAMIC_FINANCE_OFFICER";

    private readonly IExternalPortalRepository _repository;
    private readonly ISupplierProfileService _supplierProfileService;
    private readonly ICustomerProfileService _customerProfileService;
    private readonly IPartyOnboardingService _partyOnboardingService;
    private readonly ISalesOrderService _salesOrderService;
    private readonly IIslamicFinanceProductService _islamicFinanceProductService;
    private readonly IIslamicFinanceRequestService _islamicFinanceRequestService;
    private readonly IReceivablesFinanceService _receivablesFinanceService;
    private readonly IIslamicFinanceMurabahaDisbursementService _murabahaDisbursementService;
    private readonly IIslamicFinanceSettlementService _settlementService;
    private readonly INotificationTemplateService _notificationTemplateService;
    private readonly INotificationOrchestrationService _notificationOrchestrationService;
    private readonly IPaymentGatewayExecutionService _paymentGatewayExecutionService;

    public ExternalPortalService(
        IExternalPortalRepository repository,
        ISupplierProfileService supplierProfileService,
        ICustomerProfileService customerProfileService,
        IPartyOnboardingService partyOnboardingService,
        ISalesOrderService salesOrderService,
        IIslamicFinanceProductService islamicFinanceProductService,
        IIslamicFinanceRequestService islamicFinanceRequestService,
        IReceivablesFinanceService receivablesFinanceService,
        IIslamicFinanceMurabahaDisbursementService murabahaDisbursementService,
        IIslamicFinanceSettlementService settlementService,
        INotificationTemplateService notificationTemplateService,
        INotificationOrchestrationService notificationOrchestrationService,
        IPaymentGatewayExecutionService paymentGatewayExecutionService)
    {
        _repository = repository;
        _supplierProfileService = supplierProfileService;
        _customerProfileService = customerProfileService;
        _partyOnboardingService = partyOnboardingService;
        _salesOrderService = salesOrderService;
        _islamicFinanceProductService = islamicFinanceProductService;
        _islamicFinanceRequestService = islamicFinanceRequestService;
        _receivablesFinanceService = receivablesFinanceService;
        _murabahaDisbursementService = murabahaDisbursementService;
        _settlementService = settlementService;
        _notificationTemplateService = notificationTemplateService;
        _notificationOrchestrationService = notificationOrchestrationService;
        _paymentGatewayExecutionService = paymentGatewayExecutionService;
    }

    public IReadOnlyCollection<ExternalRfqInvitationResponse> GetRfqInvitations(long partyId) =>
        _repository.GetRfqInvitations(partyId)
            .Select(item => new ExternalRfqInvitationResponse(
                item.RfqId,
                item.RfqNumber,
                item.RfqType,
                item.RfqDate,
                item.ValidUntilDate,
                item.CurrencyCode,
                item.StatusCode,
                item.InviteStatusCode,
                item.InvitedAtUtc,
                item.RespondedAtUtc,
                item.PurchaseRequisitionId,
                item.LineCount,
                item.Notes))
            .ToArray();

    public ExternalRfqInvitationDetailResponse? GetRfqInvitation(long partyId, long rfqId)
    {
        var item = _repository.GetRfqInvitation(partyId, rfqId);
        return item is null ? null : MapRfqInvitation(item);
    }

    public ExternalRfqInvitationDetailResponse RespondToRfqInvitation(long partyId, long actorUserId, long rfqId, RespondExternalRfqInvitationRequest request) =>
        MapRfqInvitation(_repository.RespondToRfqInvitation(partyId, rfqId, request.ResponseStatusCode, request.DeclineReason));

    public IReadOnlyCollection<ExternalQuotationResponse> GetQuotations(long partyId) =>
        _repository.GetQuotations(partyId)
            .Select(item => new ExternalQuotationResponse(
                item.QuotationType,
                item.QuotationId,
                item.QuotationNumber,
                item.ReferenceNumber,
                item.QuoteDate,
                item.ValidUntilDate,
                item.CurrencyCode,
                item.StatusCode,
                item.TotalAmount,
                item.IsSelected,
                item.Notes))
            .ToArray();

    public SupplierQuotationResponse? GetQuotation(long partyId, long quotationId)
    {
        var quotation = _repository.GetQuotation(partyId, quotationId);
        return quotation is null ? null : MapQuotation(quotation);
    }

    public SupplierQuotationResponse CreateQuotation(long partyId, long actorUserId, CreateExternalSupplierQuotationRequest request)
    {
        var quotation = _repository.CreateQuotation(
            partyId,
            actorUserId,
            new CreateSupplierQuotationCommand(
                request.RequestForQuotationId,
                partyId,
                request.QuoteDate,
                request.ValidUntilDate,
                request.CurrencyCode,
                request.FxRate,
                request.Notes,
                request.Lines.Select(line => new CreateSupplierQuotationLineCommand(
                    line.RequestForQuotationLineId,
                    line.ItemId,
                    line.QuotedQuantity,
                    line.UnitOfMeasureCode,
                    line.UnitPrice,
                    line.DiscountPercent,
                    line.DiscountAmount,
                    line.TaxCode,
                    line.LeadTimeDays)).ToArray()));

        return MapQuotation(quotation);
    }

    public SupplierQuotationResponse SubmitQuotation(long partyId, long quotationId, long actorUserId) =>
        MapQuotation(_repository.SubmitQuotation(partyId, quotationId, actorUserId));

    public SupplierProfileResponse? GetSupplierProfile(long partyId) =>
        _supplierProfileService.GetSupplierProfiles(businessPartyId: partyId).SingleOrDefault();

    public SupplierProfileResponse CreateSupplierProfile(long partyId, CreateSupplierProfileRequest request)
    {
        if (request.BusinessPartyId != partyId)
        {
            throw new InvalidOperationException("External supplier onboarding must use the signed-in supplier party.");
        }

        return _supplierProfileService.CreateSupplierProfile(request);
    }

    public SupplierProfileResponse SubmitSupplierProfile(long partyId, long actorUserId)
    {
        var profile = _supplierProfileService.GetSupplierProfiles(businessPartyId: partyId).SingleOrDefault()
            ?? throw new InvalidOperationException("Supplier profile was not found for the signed-in supplier party.");

        return _supplierProfileService.SubmitSupplierProfile(profile.SupplierProfileId, actorUserId);
    }

    public CustomerProfileResponse? GetCustomerProfile(long partyId) =>
        _customerProfileService.GetCustomerProfiles(businessPartyId: partyId).SingleOrDefault();

    public CustomerProfileResponse CreateCustomerProfile(long partyId, CreateCustomerProfileRequest request)
    {
        if (request.BusinessPartyId != partyId)
        {
            throw new InvalidOperationException("External customer onboarding must use the signed-in customer party.");
        }

        return _customerProfileService.CreateCustomerProfile(request);
    }

    public CustomerProfileResponse SubmitCustomerProfile(long partyId, long actorUserId)
    {
        var profile = _customerProfileService.GetCustomerProfiles(businessPartyId: partyId).SingleOrDefault()
            ?? throw new InvalidOperationException("Customer profile was not found for the signed-in customer party.");

        return _customerProfileService.SubmitCustomerProfile(profile.CustomerProfileId, actorUserId);
    }

    public IReadOnlyCollection<ExternalOrderResponse> GetOrders(long partyId) =>
        _repository.GetOrders(partyId)
            .Select(item => new ExternalOrderResponse(
                item.OrderType,
                item.OrderId,
                item.OrderNumber,
                item.OrderDate,
                item.CurrencyCode,
                item.StatusCode,
                item.TotalAmount,
                item.BranchId,
                item.ReferenceNumber,
                item.SecondaryStatus,
                item.Notes))
            .ToArray();

    public IReadOnlyCollection<ExternalDocumentWorkflowResponse> GetDocumentWorkflows(long partyId) =>
        _repository.GetDocumentWorkflows(partyId).Select(MapDocumentWorkflow).ToArray();

    public ExternalDocumentWorkflowResponse? GetDocumentWorkflow(long partyId, long workflowId)
    {
        var item = _repository.GetDocumentWorkflow(partyId, workflowId);
        return item is null ? null : MapDocumentWorkflow(item);
    }

    public ExternalDocumentWorkflowResponse CreateDocumentWorkflow(long partyId, long actorUserId, CreateExternalDocumentWorkflowRequest request)
    {
        var party = RequireParty(partyId);
        return MapDocumentWorkflow(_repository.CreateDocumentWorkflow(
            partyId,
            actorUserId,
            party.PartyType,
            request.DocumentType,
            request.FileName,
            request.StorageReference,
            request.ContentType,
            request.FileSizeBytes,
            request.WorkflowNotes));
    }

    public ExternalDocumentWorkflowResponse SubmitDocumentWorkflow(long partyId, long workflowId, long actorUserId) =>
        MapDocumentWorkflow(_repository.SubmitDocumentWorkflow(partyId, workflowId, actorUserId));

    public ExternalDocumentWorkflowResponse ReplaceDocumentWorkflow(long partyId, long workflowId, long actorUserId, ReplaceExternalDocumentWorkflowRequest request) =>
        MapDocumentWorkflow(_repository.ReplaceDocumentWorkflow(
            partyId,
            workflowId,
            actorUserId,
            request.FileName,
            request.StorageReference,
            request.ContentType,
            request.FileSizeBytes,
            request.WorkflowNotes,
            request.ReplaceReason));

    public ExternalDocumentWorkflowResponse ApproveDocumentWorkflow(long workflowId, long actorUserId, ReviewExternalDocumentWorkflowRequest request) =>
        MapDocumentWorkflow(_repository.ApproveDocumentWorkflow(workflowId, actorUserId, request.ReviewNotes));

    public ExternalDocumentWorkflowResponse ReturnDocumentWorkflow(long workflowId, long actorUserId, ReviewExternalDocumentWorkflowRequest request) =>
        MapDocumentWorkflow(_repository.ReturnDocumentWorkflow(workflowId, actorUserId, request.ReviewNotes));

    public ExternalDocumentWorkflowResponse RejectDocumentWorkflow(long workflowId, long actorUserId, ReviewExternalDocumentWorkflowRequest request) =>
        MapDocumentWorkflow(_repository.RejectDocumentWorkflow(workflowId, actorUserId, request.ReviewNotes));

    public SalesOrderResponse CreateCustomerOrder(long partyId, CreateExternalCustomerOrderRequest request)
    {
        EnsurePartyType(partyId, "CUSTOMER");

        return _salesOrderService.CreateSalesOrder(new CreateSalesOrderRequest(
            request.BranchId,
            partyId,
            request.OrderDate,
            request.CurrencyCode,
            request.ExchangeRate,
            request.Lines.Select(line => new CreateSalesOrderLineRequest(
                line.ItemId,
                line.WarehouseId,
                line.OrderedQuantity,
                line.UnitOfMeasureCode,
                line.UnitPriceAmount,
                line.TaxCode)).ToArray()));
    }

    public SalesOrderResponse SubmitCustomerOrder(long partyId, long orderId, long actorUserId)
    {
        EnsurePartyType(partyId, "CUSTOMER");

        var order = GetOrders(partyId)
            .SingleOrDefault(item => string.Equals(item.OrderType, "SALES_ORDER", StringComparison.OrdinalIgnoreCase)
                && item.OrderId == orderId)
            ?? throw new InvalidOperationException("External customer order was not found for the signed-in customer party.");

        return _salesOrderService.SubmitSalesOrder(order.OrderId, actorUserId);
    }

    public IReadOnlyCollection<ExternalPurchaseOrderResponse> GetPurchaseOrders(long partyId) =>
        _repository.GetPurchaseOrders(partyId)
            .Select(item => new ExternalPurchaseOrderResponse(
                item.PurchaseOrderId,
                item.PurchaseOrderNumber,
                item.PurchaseOrderDate,
                item.CurrencyCode,
                item.StatusCode,
                item.TotalAmount,
                item.PurchaseRequisitionId,
                item.AcknowledgmentStatusCode,
                item.AcknowledgedAtUtc,
                item.AcknowledgedByUserId,
                item.AcknowledgmentNotes))
            .ToArray();

    public ExternalPurchaseOrderResponse AcknowledgePurchaseOrder(long partyId, long purchaseOrderId, long actorUserId, AcknowledgeExternalPurchaseOrderRequest request)
    {
        var entry = _repository.AcknowledgePurchaseOrder(
            partyId,
            purchaseOrderId,
            actorUserId,
            request.AcknowledgmentStatusCode,
            request.Notes);

        return new ExternalPurchaseOrderResponse(
            entry.PurchaseOrderId,
            entry.PurchaseOrderNumber,
            entry.PurchaseOrderDate,
            entry.CurrencyCode,
            entry.StatusCode,
            entry.TotalAmount,
            entry.PurchaseRequisitionId,
            entry.AcknowledgmentStatusCode,
            entry.AcknowledgedAtUtc,
            entry.AcknowledgedByUserId,
            entry.AcknowledgmentNotes);
    }

    public ExternalPurchaseOrderResponse RaisePurchaseOrderIssue(long partyId, long purchaseOrderId, long actorUserId, RaiseExternalPurchaseOrderIssueRequest request)
    {
        var entry = _repository.RaisePurchaseOrderIssue(
            partyId,
            purchaseOrderId,
            actorUserId,
            request.IssueCategoryCode,
            request.IssueNotes);

        return new ExternalPurchaseOrderResponse(
            entry.PurchaseOrderId,
            entry.PurchaseOrderNumber,
            entry.PurchaseOrderDate,
            entry.CurrencyCode,
            entry.StatusCode,
            entry.TotalAmount,
            entry.PurchaseRequisitionId,
            entry.AcknowledgmentStatusCode,
            entry.AcknowledgedAtUtc,
            entry.AcknowledgedByUserId,
            entry.AcknowledgmentNotes);
    }

    public IReadOnlyCollection<ExternalInvoiceResponse> GetInvoices(long partyId) =>
        _repository.GetInvoices(partyId)
            .Select(item => new ExternalInvoiceResponse(
                item.InvoiceType,
                item.InvoiceId,
                item.InvoiceNumber,
                item.InvoiceDate,
                item.DueDate,
                item.CurrencyCode,
                item.StatusCode,
                item.TotalAmount,
                item.OpenAmount,
                item.SecondaryStatus,
                item.ReferenceNumber))
            .ToArray();

    public ExternalPaymentResponse CreateCustomerPayment(long partyId, long actorUserId, CreateExternalCustomerPaymentRequest request)
    {
        var payment = _repository.CreateCustomerPaymentRequest(
            partyId,
            actorUserId,
            request.AccountsReceivableInvoiceId,
            request.IntegrationProviderCode,
            request.CurrencyCode,
            request.AmountAmount,
            request.PaymentDate,
            request.ReferenceNumber,
            request.Notes);
        _paymentGatewayExecutionService.QueueCustomerCollection(payment.PaymentId, actorUserId);
        return MapPayment(_repository.GetPayments(partyId).Single(item => item.PaymentType == "CUSTOMER_PAYMENT_REQUEST" && item.PaymentId == payment.PaymentId));
    }

    public IReadOnlyCollection<ExternalPaymentResponse> GetPayments(long partyId) =>
        _repository.GetPayments(partyId)
            .Select(MapPayment)
            .ToArray();

    public ExternalCustomerPaymentStatusResponse GetCustomerPaymentStatus(long partyId, long paymentId)
    {
        var item = _repository.GetCustomerPaymentStatus(partyId, paymentId);
        return new ExternalCustomerPaymentStatusResponse(
            item.PaymentId,
            item.PaymentNumber,
            item.AccountsReceivableInvoiceId,
            item.AccountsReceivableInvoiceNumber,
            item.PaymentDate,
            item.CurrencyCode,
            item.AmountAmount,
            item.StatusCode,
            item.IntegrationProviderCode,
            item.ReferenceNumber,
            item.CorrelationKey,
            item.SettledAccountsReceivableReceiptId,
            item.SubmittedAtUtc,
            item.CompletedAtUtc,
            item.LastErrorText,
            item.LatestExecutionId,
            item.LatestExecutionStatusCode,
            item.LatestProviderStatusCode,
            item.LatestProviderReference,
            new PaymentGatewayProviderContractResponse(
                item.ProviderContract.IntegrationProviderCode,
                item.ProviderContract.ContractTypeCode,
                item.ProviderContract.HttpMethodCode,
                item.ProviderContract.RequestUrl,
                item.ProviderContract.CallbackUrl,
                item.ProviderContract.AuthenticationModeCode,
                item.ProviderContract.CorrelationKey,
                item.ProviderContract.IdempotencyKey,
                item.ProviderContract.ProviderReference,
                item.ProviderContract.RequestBodyJson));
    }

    public IReadOnlyCollection<ExternalCustomerDeliveryResponse> GetCustomerDeliveries(long partyId) =>
        _repository.GetCustomerDeliveries(partyId)
            .Select(item => new ExternalCustomerDeliveryResponse(
                item.DispatchOrderId,
                item.DispatchNumber,
                item.SalesOrderId,
                item.SalesOrderNumber,
                item.DispatchStatusCode,
                item.SalesOrderStatusCode,
                item.PlannedShipAtUtc,
                item.ShippedAtUtc,
                item.DeliveredAtUtc,
                item.ProofOfDeliveryReceiver,
                item.DeliveryConditionCode,
                item.DeliveryExceptionCode,
                item.DeliveryRecoveryStatusCode))
            .ToArray();

    private static ExternalPaymentResponse MapPayment(ExternalPaymentView item) =>
        new(
            item.PaymentType,
            item.PaymentId,
            item.PaymentNumber,
            item.PaymentDate,
            item.CurrencyCode,
            item.StatusCode,
            item.Amount,
            item.UnappliedAmount,
            item.MethodCode,
            item.ReferenceNumber);

    public IReadOnlyCollection<ExternalCustomerServiceRequestResponse> GetCustomerServiceRequests(long partyId)
    {
        EnsurePartyType(partyId, "CUSTOMER");
        return _repository.GetCustomerServiceRequests(partyId).Select(MapCustomerServiceRequest).ToArray();
    }

    public ExternalCustomerServiceRequestResponse CreateCustomerServiceRequest(long partyId, long actorUserId, CreateExternalCustomerServiceRequestRequest request)
    {
        EnsurePartyType(partyId, "CUSTOMER");
        return MapCustomerServiceRequest(_repository.CreateCustomerServiceRequest(
            partyId,
            actorUserId,
            request.RequestTypeCode,
            request.SubjectText,
            request.DetailsText,
            request.PriorityCode,
            request.CommercialImpactCode,
            request.RequestedResolutionCode,
            request.AssignedBackofficeLaneCode,
            request.RelatedDocumentType,
            request.RelatedDocumentId));
    }

    public ExternalCustomerServiceRequestResponse SubmitCustomerServiceRequest(long partyId, long serviceRequestId, long actorUserId)
    {
        EnsurePartyType(partyId, "CUSTOMER");
        return MapCustomerServiceRequest(_repository.SubmitCustomerServiceRequest(partyId, serviceRequestId, actorUserId));
    }

    public ExternalCustomerServiceRequestResponse EscalateCustomerServiceRequest(long partyId, long serviceRequestId, long actorUserId, EscalateExternalCustomerServiceRequestRequest request)
    {
        EnsurePartyType(partyId, "CUSTOMER");
        return MapCustomerServiceRequest(_repository.EscalateCustomerServiceRequest(
            partyId,
            serviceRequestId,
            actorUserId,
            request.EscalationReason,
            request.AssignedBackofficeLaneCode,
            request.RequestedResolutionCode));
    }

    public ExternalCustomerServiceRequestResponse RouteCustomerServiceRequest(long serviceRequestId, long actorUserId, RouteExternalCustomerServiceRequestRequest request) =>
        MapCustomerServiceRequest(_repository.RouteCustomerServiceRequest(serviceRequestId, actorUserId, request.AssignedBackofficeLaneCode, request.RequestedResolutionCode));

    public ExternalCustomerServiceRequestResponse StartCustomerServiceRequestReview(long serviceRequestId, long actorUserId) =>
        MapCustomerServiceRequest(_repository.StartCustomerServiceRequestReview(serviceRequestId, actorUserId));

    public ExternalCustomerServiceRequestResponse RespondToCustomerServiceRequest(long serviceRequestId, long actorUserId, ReviewExternalCustomerServiceRequest request) =>
        MapCustomerServiceRequest(_repository.RespondToCustomerServiceRequest(serviceRequestId, actorUserId, request.ResponseText));

    public ExternalCustomerServiceRequestResponse RejectCustomerServiceRequest(long serviceRequestId, long actorUserId, ReviewExternalCustomerServiceRequest request) =>
        MapCustomerServiceRequest(_repository.RejectCustomerServiceRequest(serviceRequestId, actorUserId, request.ResponseText));

    public ExternalCustomerServiceRequestResponse CloseCustomerServiceRequest(long serviceRequestId, long actorUserId, CloseExternalCustomerServiceRequest request) =>
        MapCustomerServiceRequest(_repository.CloseCustomerServiceRequest(serviceRequestId, actorUserId, request.ResolutionNotes));

    public ExternalCustomerStatusTrackingResponse GetCustomerStatusTracking(long partyId)
    {
        var profile = GetCustomerProfile(partyId);
        var orders = GetOrders(partyId);
        var invoices = GetInvoices(partyId);
        var payments = GetPayments(partyId);
        var deliveries = GetCustomerDeliveries(partyId);
        var summary = GetStatementSummary(partyId);

        var latestOrder = orders.OrderByDescending(item => item.OrderDate).FirstOrDefault();
        var latestInvoice = invoices.OrderByDescending(item => item.InvoiceDate).FirstOrDefault();
        var latestDelivery = deliveries
            .OrderByDescending(item => item.DeliveredAtUtc ?? item.ShippedAtUtc ?? item.PlannedShipAtUtc)
            .FirstOrDefault();

        var deliveredCount = deliveries.Count(item => item.DeliveredAtUtc.HasValue);
        var pendingDeliveryCount = deliveries.Count(item => !item.DeliveredAtUtc.HasValue);

        return new ExternalCustomerStatusTrackingResponse(
            partyId,
            profile?.CustomerProfileId,
            profile?.CustomerProfileCode,
            profile?.StatusCode,
            profile?.CustomerClassificationCode,
            profile?.CustomerSegmentCode,
            profile?.SalesChannelCode,
            profile?.PriceTierCode,
            profile?.InvoiceDeliveryMethodCode,
            profile?.IsActive ?? false,
            profile?.CreditLimitAmount,
            profile?.CreditTermsDays,
            profile?.IsCreditHoldEnabled ?? false,
            profile?.CreditHoldReason,
            summary.CurrencyCode,
            orders.Count,
            invoices.Count,
            payments.Count,
            deliveries.Count,
            deliveredCount,
            pendingDeliveryCount,
            summary.OpenInvoiceAmount,
            summary.OverdueInvoiceAmount,
            summary.UnappliedPaymentAmount,
            summary.StatementBalanceAmount,
            latestOrder?.OrderNumber,
            latestOrder?.StatusCode,
            latestInvoice?.InvoiceNumber,
            latestInvoice?.StatusCode,
            latestDelivery?.DispatchNumber,
            latestDelivery?.DispatchStatusCode);
    }

    public IReadOnlyCollection<ExternalCustomerDocumentResponse> GetCustomerDocuments(long partyId)
    {
        var complianceDocuments = _partyOnboardingService.GetComplianceDocuments(partyId)
            .Select(item => new ExternalCustomerDocumentResponse(
                "COMPLIANCE_DOCUMENT",
                item.DocumentLinkId,
                item.DocumentType,
                item.DocumentType,
                item.FileName,
                item.StorageReference,
                item.ContentType,
                "ACTIVE",
                item.UploadedAtUtc,
                item.FileSizeBytes));

        var statementDocuments = GetStatementDocuments(partyId)
            .Select(item => new ExternalCustomerDocumentResponse(
                "STATEMENT_DOCUMENT",
                item.StatementId,
                item.StatementType,
                item.StatementNumber,
                item.StatementNumber,
                null,
                "application/pdf",
                "GENERATED",
                item.GeneratedAtUtc,
                null));

        return complianceDocuments
            .Concat(statementDocuments)
            .OrderByDescending(item => item.DocumentDateUtc)
            .ThenByDescending(item => item.DocumentId)
            .ToArray();
    }

    public BusinessPartyComplianceDocumentResponse RecordCustomerDocument(long partyId, long actorUserId, RecordBusinessPartyComplianceDocumentRequest request) =>
        _partyOnboardingService.RecordComplianceDocument(actorUserId, partyId, request);

    public ExternalStatementSummaryResponse GetStatementSummary(long partyId, DateOnly? dateFrom = null, DateOnly? dateTo = null, string? currencyCode = null)
    {
        var summary = _repository.GetStatementSummary(partyId, dateFrom, dateTo, currencyCode);
        return new ExternalStatementSummaryResponse(
            summary.PartyId,
            summary.PartyCode,
            summary.PartyName,
            summary.PartyType,
            summary.DateFrom,
            summary.DateTo,
            summary.CurrencyCode,
            summary.QuotationCount,
            summary.OrderCount,
            summary.InvoiceCount,
            summary.PaymentCount,
            summary.OpenInvoiceAmount,
            summary.OverdueInvoiceAmount,
            summary.UnappliedPaymentAmount,
            summary.StatementBalanceAmount);
    }

    public IReadOnlyCollection<ExternalStatementLineResponse> GetStatementLines(long partyId, DateOnly? dateFrom = null, DateOnly? dateTo = null, string? currencyCode = null) =>
        _repository.GetStatementLines(partyId, dateFrom, dateTo, currencyCode)
            .Select(item => new ExternalStatementLineResponse(
                item.EntryType,
                item.DocumentType,
                item.DocumentId,
                item.DocumentNumber,
                item.BusinessDate,
                item.DueDate,
                item.CurrencyCode,
                item.ChargeAmount,
                item.SettlementAmount,
                item.RunningBalanceAmount,
                item.StatusCode,
                item.ReferenceText))
            .ToArray();

    public IReadOnlyCollection<ExternalStatementDocumentResponse> GetStatementDocuments(long partyId) =>
        _repository.GetStatementDocuments(partyId)
            .Select(item => new ExternalStatementDocumentResponse(
                item.StatementType,
                item.StatementId,
                item.StatementNumber,
                item.DateFrom,
                item.DateTo,
                item.CurrencyCode,
                item.ClosingBalanceAmount,
                item.OpenAmount,
                item.OverdueAmount,
                item.DeliveryLogCount,
                item.GeneratedAtUtc))
            .ToArray();

    public ExternalStatementDetailResponse? GetStatementDetail(long partyId, long statementId)
    {
        var statement = _repository.GetStatementDetail(partyId, statementId);
        return statement is null
            ? null
            : new ExternalStatementDetailResponse(
                statement.StatementType,
                statement.StatementId,
                statement.StatementNumber,
                statement.DateFrom,
                statement.DateTo,
                statement.CurrencyCode,
                statement.ClosingBalanceAmount,
                statement.OpenAmount,
                statement.OverdueAmount,
                statement.DeliveryLogCount,
                statement.GeneratedAtUtc,
                statement.Lines.Select(item => new ExternalStatementLineResponse(
                    item.EntryType,
                    item.DocumentType,
                    item.DocumentId,
                    item.DocumentNumber,
                    item.BusinessDate,
                    item.DueDate,
                    item.CurrencyCode,
                    item.ChargeAmount,
                    item.SettlementAmount,
                    item.RunningBalanceAmount,
                    item.StatusCode,
                    item.ReferenceText)).ToArray(),
                statement.DeliveryLogs.Select(item => new ExternalStatementDeliveryLogResponse(
                    item.StatementType,
                    item.StatementId,
                    item.DeliveryLogId,
                    item.DeliveredAtUtc,
                    item.ChannelCode,
                    item.RecipientAddress,
                    item.DeliveryStatusCode,
                    item.AcknowledgmentStatusCode,
                    item.AcknowledgedAtUtc,
                    item.AcknowledgedByUserId,
                    item.Notes)).ToArray());
    }

    public IReadOnlyCollection<ExternalStatementDeliveryLogResponse> GetStatementDeliveryLogs(long partyId, long statementId) =>
        _repository.GetStatementDeliveryLogs(partyId, statementId)
            .Select(item => new ExternalStatementDeliveryLogResponse(
                item.StatementType,
                item.StatementId,
                item.DeliveryLogId,
                item.DeliveredAtUtc,
                item.ChannelCode,
                item.RecipientAddress,
                item.DeliveryStatusCode,
                item.AcknowledgmentStatusCode,
                item.AcknowledgedAtUtc,
                item.AcknowledgedByUserId,
                item.Notes))
            .ToArray();

    public IReadOnlyCollection<IslamicFinanceProductProfileResponse> GetFinancingProducts(long partyId)
    {
        EnsurePartyType(partyId, "CUSTOMER", "SUPPLIER");
        return _islamicFinanceProductService.GetProductProfiles(activeOnly: true);
    }

    public IReadOnlyCollection<IslamicFinanceFinancingRequestResponse> GetFinancingRequests(long partyId) =>
        _repository.GetFinancingRequests(partyId)
            .Select(MapFinancingRequest)
            .ToArray();

    public IslamicFinanceFinancingRequestResponse? GetFinancingRequest(long partyId, long financingRequestId)
    {
        var item = _repository.GetFinancingRequest(partyId, financingRequestId);
        return item is null ? null : MapFinancingRequest(item);
    }

    public IslamicFinanceFinancingRequestResponse CreateFinancingRequest(long partyId, CreateExternalFinancingApplicationRequest request)
    {
        EnsurePartyType(partyId, "CUSTOMER", "SUPPLIER");

        var requestNumber = string.IsNullOrWhiteSpace(request.RequestNumber)
            ? BuildExternalFinancingRequestNumber(partyId)
            : request.RequestNumber.Trim().ToUpperInvariant();

        return _islamicFinanceRequestService.CreateRequest(new CreateIslamicFinanceFinancingRequestRequest
        {
            RequestNumber = requestNumber,
            CustomerPartyId = partyId,
            ApplicantPartyId = partyId,
            ApplicantPartyTypeCode = string.IsNullOrWhiteSpace(request.ApplicantPartyTypeCode)
                ? RequireParty(partyId).PartyType
                : request.ApplicantPartyTypeCode.Trim().ToUpperInvariant(),
            ProductProfileId = request.ProductProfileId,
            FacilityId = request.FacilityId,
            FundingSourceMode = request.FundingSourceMode,
            SourceDocumentType = request.SourceDocumentType,
            SourceDocumentId = request.SourceDocumentId,
            TransactionCurrencyCode = request.TransactionCurrencyCode,
            RequestedAmount = request.RequestedAmount
        });
    }

    public IslamicFinanceFinancingRequestResponse SubmitFinancingRequest(long partyId, long financingRequestId, long actorUserId)
    {
        EnsurePartyType(partyId, "CUSTOMER", "SUPPLIER");

        var request = GetFinancingRequests(partyId)
            .SingleOrDefault(item => item.Id == financingRequestId)
            ?? throw new InvalidOperationException("External financing request was not found for the signed-in party.");

        var submittedRequest = _islamicFinanceRequestService.SubmitRequest(request.Id, actorUserId);
        EnqueueFinancingWorkflowNotifications(
            actorUserId,
            partyId,
            submittedRequest,
            ExternalFinancingRequestSubmittedTemplateCode,
            OfficerFinancingRequestSubmittedTemplateCode,
            null);
        return submittedRequest;
    }

    public IslamicFinanceFinancingRequestResponse AcceptFinancingOffer(long partyId, long financingRequestId, long actorUserId, AcceptExternalFinancingOfferRequest request)
    {
        EnsurePartyType(partyId, "CUSTOMER", "SUPPLIER");

        var financingRequest = _repository.GetFinancingRequest(partyId, financingRequestId)
            ?? throw new InvalidOperationException("External financing request was not found for the signed-in party.");

        var matchingOffer = _repository.GetFinancingOffers(partyId, financingRequestId)
            .SingleOrDefault(item => item.FinancingRequestId == financingRequestId && item.FinancingDecisionId == request.FinancingDecisionId)
            ?? throw new InvalidOperationException("Selected financing offer was not found for the signed-in party.");

        if (!matchingOffer.FinancingDecisionId.HasValue
            || (!matchingOffer.IsSelectedOffer
                && !string.Equals(matchingOffer.DecisionCode, "APPROVED", StringComparison.OrdinalIgnoreCase)
                && !string.Equals(matchingOffer.OfferSourceType, "REQUEST_ASSIGNMENT", StringComparison.OrdinalIgnoreCase)))
        {
            throw new InvalidOperationException("Only approved lender offers can be accepted from the external portal.");
        }

        var acceptedRequest = _islamicFinanceRequestService.SelectOffer(financingRequest.Id, matchingOffer.FinancingDecisionId.Value, actorUserId);
        EnqueueFinancingWorkflowNotifications(
            actorUserId,
            partyId,
            acceptedRequest,
            ExternalFinancingOfferAcceptedTemplateCode,
            OfficerFinancingOfferAcceptedTemplateCode,
            matchingOffer);
        return acceptedRequest;
    }

    public IReadOnlyCollection<ExternalFinancingOfferResponse> GetFinancingOffers(long partyId) =>
        _repository.GetFinancingOffers(partyId)
            .Select(MapFinancingOffer)
            .ToArray();

    public IReadOnlyCollection<ExternalFinancingOfferResponse> GetFinancingOffersForRequest(long partyId, long financingRequestId)
    {
        EnsurePartyType(partyId, "CUSTOMER", "SUPPLIER");

        var financingRequest = _repository.GetFinancingRequest(partyId, financingRequestId)
            ?? throw new InvalidOperationException("External financing request was not found for the signed-in party.");

        return _repository.GetFinancingOffers(partyId, financingRequest.Id)
            .Select(MapFinancingOffer)
            .ToArray();
    }

    public IReadOnlyCollection<ExternalFinancingNotificationResponse> GetFinancingNotifications(long partyId, long financingRequestId)
    {
        EnsurePartyType(partyId, "CUSTOMER", "SUPPLIER");

        var financingRequest = _repository.GetFinancingRequest(partyId, financingRequestId)
            ?? throw new InvalidOperationException("External financing request was not found for the signed-in party.");

        return _repository.GetFinancingNotifications(partyId, financingRequest.Id)
            .Select(item => new ExternalFinancingNotificationResponse(
                item.NotificationQueueItemId,
                item.TemplateCode,
                item.ChannelCode,
                item.ContextCode,
                item.Subject,
                item.Body,
                item.StatusCode,
                item.CreatedAtUtc,
                item.SentAtUtc))
            .ToArray();
    }

    public IReadOnlyCollection<ExternalFinancingTaskResponse> GetFinancingTasks(long partyId)
    {
        EnsurePartyType(partyId, "CUSTOMER", "SUPPLIER");

        return _repository.GetFinancingTasks(partyId)
            .Select(item => new ExternalFinancingTaskResponse(
                item.FinancingRequestId,
                item.FinancingRequestNumber,
                item.ApplicantPartyTypeCode,
                item.TaskCode,
                item.TaskTitle,
                item.TaskCategoryCode,
                item.TaskStatusCode,
                item.ContextSummary,
                item.RelatedNotificationCount,
                item.DueAtUtc))
            .ToArray();
    }

    public ExternalFinancingJourneySummaryResponse? GetFinancingJourneySummary(long partyId, long financingRequestId)
    {
        EnsurePartyType(partyId, "CUSTOMER", "SUPPLIER");

        var financingRequest = _repository.GetFinancingRequest(partyId, financingRequestId);
        if (financingRequest is null)
        {
            return null;
        }

        var productProfile = _islamicFinanceProductService.GetProductProfile(financingRequest.ProductProfileId);
        var productFamily = GetFinancingProductFamilies(partyId)
            .SingleOrDefault(item => string.Equals(item.ProductTypeCode, productProfile?.ProductType ?? string.Empty, StringComparison.OrdinalIgnoreCase));
        var offers = _repository.GetFinancingOffers(partyId, financingRequestId).ToArray();
        var selectedOffer = offers
            .Where(item => item.IsSelectedOffer)
            .OrderByDescending(item => item.ApprovedAtUtc ?? item.SubmittedAtUtc ?? item.CreatedAtUtc)
            .ThenByDescending(item => item.FinancingDecisionId ?? 0L)
            .FirstOrDefault()
            ?? offers.FirstOrDefault(item => item.FinancingDecisionId == financingRequest.SelectedFinancingDecisionId);
        var murabahaCase = _repository.GetMurabahaCases(partyId)
            .Where(item => item.FinancingRequestId == financingRequestId)
            .OrderByDescending(item => item.Id)
            .FirstOrDefault();
        var receivablesCase = _repository.GetReceivablesCases(partyId)
            .Where(item => item.FinancingRequestId == financingRequestId)
            .OrderByDescending(item => item.Id)
            .FirstOrDefault();
        var disbursement = murabahaCase is null
            ? null
            : _repository.GetMurabahaDisbursements(partyId)
                .Where(item => item.MurabahaCaseId == murabahaCase.Id)
                .OrderByDescending(item => item.Id)
                .FirstOrDefault();
        var settlement = _repository.GetSettlements(partyId)
            .Where(item => item.FinancingRequestId == financingRequestId)
            .OrderByDescending(item => item.Id)
            .FirstOrDefault();

        return new ExternalFinancingJourneySummaryResponse(
            financingRequest.Id,
            financingRequest.RequestNumber,
            financingRequest.ApplicantPartyTypeCode,
            productProfile?.ProductType ?? "UNKNOWN",
            productFamily?.ProductTypeCode ?? (productProfile?.ProductType ?? "UNKNOWN"),
            productFamily?.ProductFamilyName ?? HumanizeExternalCode(productProfile?.ProductType ?? "UNKNOWN"),
            productFamily?.FundingReadinessCode ?? ResolveFundingReadinessCode(productProfile),
            productFamily?.FutureReadinessCode ?? ResolveFutureReadinessCode(productProfile),
            financingRequest.FundingSourceMode,
            ResolveProviderModelCode(selectedOffer, financingRequest),
            ResolveProviderModelLabel(selectedOffer, financingRequest),
            financingRequest.StatusCode,
            ResolveNextExternalActionCode(financingRequest, offers, murabahaCase, receivablesCase, disbursement, settlement),
            ResolveExecutionStageCode(financingRequest, offers, murabahaCase, receivablesCase, disbursement, settlement),
            offers.Length,
            selectedOffer is not null || financingRequest.SelectedFinancingDecisionId.HasValue,
            financingRequest.SelectedFinancingDecisionId,
            murabahaCase?.Id,
            murabahaCase?.CaseNumber,
            receivablesCase?.Id,
            receivablesCase?.CaseNumber,
            disbursement?.Id,
            disbursement?.DisbursementNumber,
            settlement?.Id,
            settlement?.SettlementNumber);
    }

    public IReadOnlyCollection<ExternalFinancingProductFamilyView> GetFinancingProductFamilies(long partyId)
    {
        RequireParty(partyId);

        return _islamicFinanceProductService.GetProductProfiles(activeOnly: true)
            .GroupBy(entry => entry.ProductType, StringComparer.OrdinalIgnoreCase)
            .OrderBy(group => group.Key, StringComparer.OrdinalIgnoreCase)
            .Select(group =>
            {
                var allowsPartnerFunding = group.Any(entry => entry.AllowsPartnerFunding);
                var allowsCashLineFunding = group.Any(entry => entry.AllowsCashLineFunding);

                return new ExternalFinancingProductFamilyView(
                    group.Key,
                    group.Key.Replace('_', ' '),
                    group.Count(),
                    group.Any(entry => entry.RequiresShariahReview),
                    allowsPartnerFunding,
                    allowsCashLineFunding,
                    allowsPartnerFunding && allowsCashLineFunding
                        ? "HYBRID_READY"
                        : allowsCashLineFunding
                            ? "CASHLINE_READY"
                            : "PARTNER_READY",
                    allowsCashLineFunding
                        ? "MICROFINANCE_READY"
                        : "PARTNER_EXPANSION_READY");
            })
            .ToArray();
    }

    public IReadOnlyCollection<IslamicFinanceReceivablesCaseResponse> GetReceivablesCases(long partyId) =>
        _repository.GetReceivablesCases(partyId)
            .Select(MapReceivablesCase)
            .ToArray();

    public IslamicFinanceReceivablesCaseResponse? GetReceivablesCase(long partyId, long receivablesCaseId)
    {
        var visible = _repository.GetReceivablesCases(partyId).Any(entry => entry.Id == receivablesCaseId);
        return !visible ? null : _receivablesFinanceService.GetCase(receivablesCaseId);
    }

    public IReadOnlyCollection<IslamicFinanceMurabahaCaseResponse> GetMurabahaCases(long partyId) =>
        _repository.GetMurabahaCases(partyId)
            .Select(MapMurabahaCase)
            .ToArray();

    public IslamicFinanceMurabahaCaseResponse? GetMurabahaCase(long partyId, long murabahaCaseId)
    {
        var item = _repository.GetMurabahaCases(partyId).SingleOrDefault(entry => entry.Id == murabahaCaseId);
        return item is null ? null : MapMurabahaCase(item);
    }

    public IReadOnlyCollection<IslamicFinanceMurabahaDisbursementResponse> GetMurabahaDisbursements(long partyId) =>
        _repository.GetMurabahaDisbursements(partyId)
            .Select(MapDisbursement)
            .ToArray();

    public IslamicFinanceMurabahaDisbursementResponse? GetMurabahaDisbursement(long partyId, long disbursementId)
    {
        var visible = _repository.GetMurabahaDisbursements(partyId).Any(entry => entry.Id == disbursementId);
        return !visible ? null : _murabahaDisbursementService.GetDisbursement(disbursementId);
    }

    public IReadOnlyCollection<IslamicFinanceSettlementResponse> GetSettlements(long partyId) =>
        _repository.GetSettlements(partyId)
            .Select(MapSettlement)
            .ToArray();

    public IslamicFinanceSettlementResponse? GetSettlement(long partyId, long settlementId)
    {
        var visible = _repository.GetSettlements(partyId).Any(entry => entry.Id == settlementId);
        return !visible ? null : _settlementService.GetSettlement(settlementId);
    }

    private static ExternalRfqInvitationDetailResponse MapRfqInvitation(ExternalRfqInvitationDetailView item) =>
        new(
            item.RfqId,
            item.RfqNumber,
            item.RfqType,
            item.RfqDate,
            item.ValidUntilDate,
            item.CurrencyCode,
            item.StatusCode,
            item.InviteStatusCode,
            item.InvitedAtUtc,
            item.RespondedAtUtc,
            item.PurchaseRequisitionId,
            item.Notes,
            item.Lines.Select(line => new ExternalRfqInvitationLineResponse(
                line.RfqLineId,
                line.LineNumber,
                line.ItemId,
                line.ItemDescription,
                line.RequestedQuantity,
                line.UnitOfMeasureCode,
                line.NeedByDate,
                line.TaxCode,
                line.TargetUnitPrice,
                line.Notes)).ToArray());

    private static IslamicFinanceFinancingRequestResponse MapFinancingRequest(IslamicFinanceRequest item) =>
        new()
        {
            Id = item.Id,
            RequestNumber = item.RequestNumber,
            CustomerPartyId = item.CustomerPartyId,
            ApplicantPartyId = item.ApplicantPartyId,
            ApplicantPartyTypeCode = item.ApplicantPartyTypeCode,
            ProductProfileId = item.ProductProfileId,
            FacilityId = item.FacilityId,
            FundingSourceMode = item.FundingSourceMode,
            SourceDocumentType = item.SourceDocumentType,
            SourceDocumentId = item.SourceDocumentId,
            TransactionCurrencyCode = item.TransactionCurrencyCode,
            RequestedAmount = item.RequestedAmount,
            StatusCode = item.StatusCode,
            WorkflowInstanceId = item.WorkflowInstanceId,
            ShariahReviewCaseId = item.ShariahReviewCaseId,
            AssignedPartnerLenderId = item.AssignedPartnerLenderId,
            SelectedFinancingDecisionId = item.SelectedFinancingDecisionId,
            SubmittedAtUtc = item.SubmittedAtUtc,
            SubmittedByUserId = item.SubmittedByUserId,
            ApprovedAtUtc = item.ApprovedAtUtc,
            ApprovedByUserId = item.ApprovedByUserId,
            SelectedOfferAtUtc = item.SelectedOfferAtUtc,
            SelectedOfferByUserId = item.SelectedOfferByUserId
        };

    private static ExternalFinancingOfferResponse MapFinancingOffer(ExternalFinancingOfferView item) =>
        new(
            item.OfferSourceType,
            item.FinancingDecisionId,
            item.FinancingRequestId,
            item.FinancingRequestNumber,
            item.ApplicantPartyId,
            item.ApplicantPartyTypeCode,
            item.ProductTypeCode,
            item.ProductCode,
            item.ProductName,
            item.RequiresShariahReview,
            item.OfferFundingSourceMode,
            item.PartnerLenderId,
            item.PartnerLenderName,
            item.PartnerDefaultCurrencyCode,
            item.SupportsMurabaha,
            item.SupportsReceivablesFinance,
            item.SupportsTawarruq,
            item.DecisionCode,
            item.StatusCode,
            item.IsSelectedOffer,
            item.OfferCurrencyCode,
            item.OfferedAmount,
            item.TenorMonths,
            item.ProfitRatePercent,
            item.ProcessingFeeAmount,
            item.SecurityRequirementText,
            item.OfferRank,
            item.DecisionNotes,
            item.CreatedAtUtc,
            item.SubmittedAtUtc,
            item.ApprovedAtUtc);

    private static IslamicFinanceMurabahaCaseResponse MapMurabahaCase(IslamicFinanceMurabahaCase item) =>
        new()
        {
            Id = item.Id,
            CaseNumber = item.CaseNumber,
            FinancingRequestId = item.FinancingRequestId,
            CustomerPartyId = item.CustomerPartyId,
            PartnerLenderId = item.PartnerLenderId,
            SupplierPartyId = item.SupplierPartyId,
            ProductProfileId = item.ProductProfileId,
            ContractTemplateId = item.ContractTemplateId,
            TransactionCurrencyCode = item.TransactionCurrencyCode,
            PurchaseCostAmount = item.PurchaseCostAmount,
            ProfitAmount = item.ProfitAmount,
            SalePriceAmount = item.SalePriceAmount,
            StatusCode = item.StatusCode,
            WorkflowInstanceId = item.WorkflowInstanceId,
            SubmittedAtUtc = item.SubmittedAtUtc,
            SubmittedByUserId = item.SubmittedByUserId,
            ApprovedAtUtc = item.ApprovedAtUtc,
            ApprovedByUserId = item.ApprovedByUserId,
            ActivatedAtUtc = item.ActivatedAtUtc,
            ActivatedByUserId = item.ActivatedByUserId
        };

    private static IslamicFinanceMurabahaDisbursementResponse MapDisbursement(IslamicFinanceMurabahaDisbursement item) =>
        new()
        {
            Id = item.Id,
            DisbursementNumber = item.DisbursementNumber,
            MurabahaCaseId = item.MurabahaCaseId,
            CustomerPartyId = item.CustomerPartyId,
            PartnerLenderId = item.PartnerLenderId,
            SupplierPartyId = item.SupplierPartyId,
            FundingSourceMode = item.FundingSourceMode,
            TargetType = item.TargetType,
            TransactionCurrencyCode = item.TransactionCurrencyCode,
            InstructedAmount = item.InstructedAmount,
            ExecutedAmount = item.ExecutedAmount,
            OutstandingAmount = Math.Max(0m, item.InstructedAmount - item.ExecutedAmount),
            PlannedExecutionDate = item.PlannedExecutionDate,
            StatusCode = item.StatusCode,
            WorkflowInstanceId = item.WorkflowInstanceId,
            CreatedAtUtc = item.CreatedAtUtc,
            CreatedByUserId = item.CreatedByUserId,
            SubmittedAtUtc = item.SubmittedAtUtc,
            SubmittedByUserId = item.SubmittedByUserId,
            ApprovedAtUtc = item.ApprovedAtUtc,
            ApprovedByUserId = item.ApprovedByUserId,
            ExecutedAtUtc = item.ExecutedAtUtc,
            ExecutedByUserId = item.ExecutedByUserId,
            ExecutionReferenceNumber = item.ExecutionReferenceNumber,
            ExecutionNotes = item.ExecutionNotes,
            SuspendedAtUtc = item.SuspendedAtUtc,
            SuspendedByUserId = item.SuspendedByUserId,
            SuspensionReasonCode = item.SuspensionReasonCode,
            UpdatedAtUtc = item.UpdatedAtUtc,
            UpdatedByUserId = item.UpdatedByUserId
        };

    private static IslamicFinanceReceivablesCaseResponse MapReceivablesCase(IslamicFinanceReceivablesCase item) =>
        new()
        {
            Id = item.Id,
            CaseNumber = item.CaseNumber,
            FinancingRequestId = item.FinancingRequestId,
            CustomerPartyId = item.CustomerPartyId,
            PartnerLenderId = item.PartnerLenderId,
            FacilityId = item.FacilityId,
            ProductProfileId = item.ProductProfileId,
            TransactionCurrencyCode = item.TransactionCurrencyCode,
            TotalReceivableAmount = item.TotalReceivableAmount,
            FinancedAmount = item.FinancedAmount,
            AdvanceRatePercent = item.AdvanceRatePercent,
            StatusCode = item.StatusCode,
            WorkflowInstanceId = item.WorkflowInstanceId,
            CreatedAtUtc = item.CreatedAtUtc,
            CreatedByUserId = item.CreatedByUserId,
            SubmittedAtUtc = item.SubmittedAtUtc,
            SubmittedByUserId = item.SubmittedByUserId,
            ApprovedAtUtc = item.ApprovedAtUtc,
            ApprovedByUserId = item.ApprovedByUserId,
            ActivatedAtUtc = item.ActivatedAtUtc,
            ActivatedByUserId = item.ActivatedByUserId,
            UpdatedAtUtc = item.UpdatedAtUtc,
            UpdatedByUserId = item.UpdatedByUserId,
            Lines = []
        };

    private static IslamicFinanceSettlementResponse MapSettlement(IslamicFinanceSettlementHeader item) =>
        new()
        {
            Id = item.Id,
            SettlementNumber = item.SettlementNumber,
            MurabahaCaseId = item.MurabahaCaseId,
            FinancingRequestId = item.FinancingRequestId,
            PartnerLenderId = item.PartnerLenderId,
            FundingSourceMode = item.FundingSourceMode,
            SettlementDate = item.SettlementDate,
            SettlementCurrencyCode = item.SettlementCurrencyCode,
            TotalSettledAmount = item.TotalSettledAmount,
            StatusCode = item.StatusCode,
            WorkflowInstanceId = item.WorkflowInstanceId,
            CreatedAtUtc = item.CreatedAtUtc,
            CreatedByUserId = item.CreatedByUserId,
            SubmittedAtUtc = item.SubmittedAtUtc,
            SubmittedByUserId = item.SubmittedByUserId,
            ApprovedAtUtc = item.ApprovedAtUtc,
            ApprovedByUserId = item.ApprovedByUserId,
            ClosedAtUtc = item.ClosedAtUtc,
            ClosedByUserId = item.ClosedByUserId,
            UpdatedAtUtc = item.UpdatedAtUtc,
            UpdatedByUserId = item.UpdatedByUserId,
            Lines = []
        };

    private static SupplierQuotationResponse MapQuotation(SupplierQuotationDetailView quotation) =>
        new(
            quotation.SupplierQuotationId,
            quotation.QuotationNumber,
            quotation.RequestForQuotationId,
            quotation.SupplierId,
            quotation.QuoteDate,
            quotation.ValidUntilDate,
            quotation.CurrencyCode,
            quotation.FxRate,
            quotation.StatusCode,
            quotation.SubtotalAmount,
            quotation.DiscountAmount,
            quotation.TaxAmount,
            quotation.TotalAmount,
            quotation.IsSelected,
            quotation.Notes,
            quotation.Lines.Select(line => new SupplierQuotationLineResponse(
                line.SupplierQuotationLineId,
                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 ExternalDocumentWorkflowResponse MapDocumentWorkflow(ExternalDocumentWorkflowView item) =>
        new(
            item.WorkflowId,
            item.PartyId,
            item.PartyTypeCode,
            item.DocumentLinkId,
            item.DocumentType,
            item.FileName,
            item.StorageReference,
            item.ContentType,
            item.FileSizeBytes,
            item.VersionNumber,
            item.StatusCode,
            item.WorkflowNotes,
            item.ReviewNotes,
            item.UploadedAtUtc,
            item.UploadedByUserId,
            item.SubmittedAtUtc,
            item.SubmittedByUserId,
            item.ReviewedAtUtc,
            item.ReviewedByUserId,
            item.ReturnedAtUtc,
            item.ReturnedByUserId,
            item.RejectedAtUtc,
            item.RejectedByUserId);

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

    private void EnsurePartyType(long partyId, params string[] allowedTypes)
    {
        var party = RequireParty(partyId);

        if (!allowedTypes.Any(item => string.Equals(item, party.PartyType, StringComparison.OrdinalIgnoreCase)))
        {
            throw new InvalidOperationException("The signed-in external party cannot perform this action from the current portal lane.");
        }
    }

    private static string ResolveFundingReadinessCode(IslamicFinanceProductProfileResponse? productProfile)
    {
        if (productProfile is null)
        {
            return "UNKNOWN";
        }

        if (productProfile.AllowsPartnerFunding && productProfile.AllowsCashLineFunding)
        {
            return "HYBRID_READY";
        }

        if (productProfile.AllowsCashLineFunding)
        {
            return "CASHLINE_READY";
        }

        return "PARTNER_READY";
    }

    private static string ResolveFutureReadinessCode(IslamicFinanceProductProfileResponse? productProfile) =>
        productProfile?.AllowsCashLineFunding == true ? "MICROFINANCE_READY" : "PARTNER_EXPANSION_READY";

    private static string ResolveProviderModelCode(ExternalFinancingOfferView? selectedOffer, IslamicFinanceRequest financingRequest)
    {
        var fundingSourceMode = selectedOffer?.OfferFundingSourceMode ?? financingRequest.FundingSourceMode;
        return fundingSourceMode.ToUpperInvariant() switch
        {
            "CASHLINE" => "CASHLINE_FOUNDATION",
            "HYBRID" => "HYBRID_ECOSYSTEM",
            "PARTNER" when selectedOffer?.PartnerLenderId is not null || financingRequest.AssignedPartnerLenderId.HasValue => "PARTNER_LENDER",
            _ => "PORTAL_REQUEST"
        };
    }

    private static string? ResolveProviderModelLabel(ExternalFinancingOfferView? selectedOffer, IslamicFinanceRequest financingRequest)
    {
        if (!string.IsNullOrWhiteSpace(selectedOffer?.PartnerLenderName))
        {
            return selectedOffer.PartnerLenderName;
        }

        return (selectedOffer?.OfferFundingSourceMode ?? financingRequest.FundingSourceMode).ToUpperInvariant() switch
        {
            "CASHLINE" => "CashLine Funding Desk",
            "HYBRID" => "Hybrid Funding Desk",
            _ => financingRequest.AssignedPartnerLenderId.HasValue ? "Assigned Partner Lender" : "Offer Not Yet Selected"
        };
    }

    private static string ResolveNextExternalActionCode(
        IslamicFinanceRequest financingRequest,
        IReadOnlyCollection<ExternalFinancingOfferView> offers,
        IslamicFinanceMurabahaCase? murabahaCase,
        IslamicFinanceReceivablesCase? receivablesCase,
        IslamicFinanceMurabahaDisbursement? disbursement,
        IslamicFinanceSettlementHeader? settlement)
    {
        if (string.Equals(financingRequest.StatusCode, "DRAFT", StringComparison.OrdinalIgnoreCase)
            || string.Equals(financingRequest.StatusCode, "RETURNED", StringComparison.OrdinalIgnoreCase))
        {
            return "SUBMIT_APPLICATION";
        }

        if (string.Equals(financingRequest.StatusCode, "SUBMITTED", StringComparison.OrdinalIgnoreCase))
        {
            return "AWAIT_INTERNAL_REVIEW";
        }

        if (string.Equals(financingRequest.StatusCode, "REJECTED", StringComparison.OrdinalIgnoreCase))
        {
            return "REVIEW_REJECTION_OUTCOME";
        }

        if (settlement is not null)
        {
            return string.Equals(settlement.StatusCode, "CLOSED", StringComparison.OrdinalIgnoreCase)
                ? "FINANCING_SETTLED"
                : "TRACK_SETTLEMENT";
        }

        if (disbursement is not null)
        {
            return string.Equals(disbursement.StatusCode, "EXECUTED", StringComparison.OrdinalIgnoreCase)
                ? "AWAIT_SETTLEMENT"
                : "TRACK_DISBURSEMENT";
        }

        if (receivablesCase is not null)
        {
            return "TRACK_RECEIVABLES_COLLECTION";
        }

        if (murabahaCase is not null)
        {
            return "TRACK_EXECUTION_CASE";
        }

        if (financingRequest.SelectedFinancingDecisionId.HasValue)
        {
            return "AWAIT_EXECUTION_CASE";
        }

        if (offers.Any())
        {
            return "COMPARE_AND_SELECT_OFFER";
        }

        if (string.Equals(financingRequest.StatusCode, "APPROVED", StringComparison.OrdinalIgnoreCase)
            || string.Equals(financingRequest.StatusCode, "ASSIGNED", StringComparison.OrdinalIgnoreCase))
        {
            return "AWAIT_OFFER_PUBLICATION";
        }

        return "MONITOR_WORKFLOW";
    }

    private static string ResolveExecutionStageCode(
        IslamicFinanceRequest financingRequest,
        IReadOnlyCollection<ExternalFinancingOfferView> offers,
        IslamicFinanceMurabahaCase? murabahaCase,
        IslamicFinanceReceivablesCase? receivablesCase,
        IslamicFinanceMurabahaDisbursement? disbursement,
        IslamicFinanceSettlementHeader? settlement)
    {
        if (settlement is not null)
        {
            return "SETTLEMENT";
        }

        if (disbursement is not null)
        {
            return "DISBURSEMENT";
        }

        if (receivablesCase is not null)
        {
            return "RECEIVABLES_CASE";
        }

        if (murabahaCase is not null)
        {
            return "MURABAHA_CASE";
        }

        if (financingRequest.SelectedFinancingDecisionId.HasValue)
        {
            return "OFFER_SELECTED";
        }

        if (offers.Any())
        {
            return "OFFER_COMPARISON";
        }

        return "APPLICATION";
    }

    private static string HumanizeExternalCode(string value) =>
        string.IsNullOrWhiteSpace(value)
            ? "-"
            : value.Replace('_', ' ').Trim();

    private void EnqueueFinancingWorkflowNotifications(
        long actorUserId,
        long partyId,
        IslamicFinanceFinancingRequestResponse financingRequest,
        string externalTemplateCode,
        string officerTemplateCode,
        ExternalFinancingOfferView? selectedOffer)
    {
        var party = RequireParty(partyId);
        var product = _islamicFinanceProductService.GetProductProfile(financingRequest.ProductProfileId);
        var variables = BuildFinancingNotificationVariables(party, financingRequest, product, selectedOffer);

        EnqueueNotificationByTemplateCode(
            actorUserId,
            externalTemplateCode,
            BuildExternalNotificationRecipientReference(partyId),
            variables);
        EnqueueNotificationByTemplateCode(
            actorUserId,
            officerTemplateCode,
            OfficerFinancingRecipientReference,
            variables);
    }

    private void EnqueueNotificationByTemplateCode(
        long actorUserId,
        string templateCode,
        string recipientReference,
        Dictionary<string, string?> variables)
    {
        var template = _notificationTemplateService
            .GetTemplates(activeOnly: true)
            .SingleOrDefault(entry => string.Equals(entry.TemplateCode, templateCode, StringComparison.OrdinalIgnoreCase));

        if (template is null)
        {
            return;
        }

        _notificationOrchestrationService.EnqueueNotification(actorUserId, new EnqueueNotificationRequest(
            template.NotificationTemplateId,
            recipientReference,
            "ISLAMIC_FINANCE",
            "FINANCING_REQUEST",
            long.TryParse(variables["financingRequestId"], out var relatedEntityId) ? relatedEntityId : null,
            variables));
    }

    private static Dictionary<string, string?> BuildFinancingNotificationVariables(
        BusinessPartyResponse party,
        IslamicFinanceFinancingRequestResponse financingRequest,
        IslamicFinanceProductProfileResponse? product,
        ExternalFinancingOfferView? selectedOffer)
    {
        return new Dictionary<string, string?>
        {
            ["financingRequestId"] = financingRequest.Id.ToString(),
            ["requestNumber"] = financingRequest.RequestNumber,
            ["applicantName"] = party.PartyName,
            ["applicantType"] = financingRequest.ApplicantPartyTypeCode,
            ["productName"] = product?.ProductName ?? $"Product {financingRequest.ProductProfileId}",
            ["fundingMode"] = financingRequest.FundingSourceMode,
            ["requestedAmount"] = financingRequest.RequestedAmount.ToString("0.00"),
            ["currencyCode"] = financingRequest.TransactionCurrencyCode,
            ["lenderName"] = selectedOffer?.PartnerLenderName ?? "CashLine",
            ["offerAmount"] = selectedOffer?.OfferedAmount?.ToString("0.00"),
            ["offerTenorMonths"] = selectedOffer?.TenorMonths?.ToString(),
            ["providerModel"] = selectedOffer?.OfferFundingSourceMode ?? financingRequest.FundingSourceMode
        };
    }

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

    private static string BuildExternalFinancingRequestNumber(long partyId) =>
        $"EXT-IF-{partyId}-{DateTime.UtcNow:yyyyMMddHHmmss}";

    private BusinessPartyResponse RequireParty(long partyId) =>
        _partyOnboardingService.GetParties()
            .SingleOrDefault(item => item.PartyId == partyId)
            ?? throw new InvalidOperationException($"Business party '{partyId}' was not found.");
}
