using CashLineERP.Application.Finance;
using CashLineERP.Application.Integration;
using CashLineERP.Contracts.Integration;
using CashLineERP.Domain.Finance;
using CashLineERP.Domain.Integration;
using CashLineERP.SharedKernel.Finance;
using Microsoft.EntityFrameworkCore;
using System.Text.Json;

namespace CashLineERP.Infrastructure.Persistence.Repositories;

public sealed class GatewayCallbackRepository : IGatewayCallbackRepository
{
    private const long GatewayCallbackSystemUserId = 1L;
    private const long GatewayCallbackSystemCheckerUserId = 2L;
    private readonly CashLineErpDbContext _dbContext;

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

    public IReadOnlyCollection<GatewayCallbackReceiptView> GetCallbacks(long paymentChannelConfigurationId, string? statusCode = null)
    {
        var channel = ResolveChannel(paymentChannelConfigurationId);
        var normalizedStatusCode = string.IsNullOrWhiteSpace(statusCode) ? null : statusCode.Trim().ToUpperInvariant();

        return (
            from message in _dbContext.InboxMessages.AsNoTracking()
            join endpoint in _dbContext.IntegrationEndpointRegistryEntries.AsNoTracking()
                on message.IntegrationEndpointId equals endpoint.Id
            where message.IntegrationEndpointId == channel.InboundIntegrationEndpointId!.Value
            where normalizedStatusCode == null || message.StatusCode == normalizedStatusCode
            orderby message.CreatedAtUtc descending, message.Id descending
            select Map(message, endpoint, channel))
            .ToArray();
    }

    public GatewayCallbackReceiptView? GetCallback(long paymentChannelConfigurationId, long inboxMessageId)
    {
        var channel = ResolveChannel(paymentChannelConfigurationId);

        return (
            from message in _dbContext.InboxMessages.AsNoTracking()
            join endpoint in _dbContext.IntegrationEndpointRegistryEntries.AsNoTracking()
                on message.IntegrationEndpointId equals endpoint.Id
            where message.Id == inboxMessageId
            where message.IntegrationEndpointId == channel.InboundIntegrationEndpointId!.Value
            select Map(message, endpoint, channel))
            .SingleOrDefault();
    }

    public GatewayCallbackReceiveResultView Receive(GatewayCallbackReceiveCommand command)
    {
        var channel = ResolveActiveChannel(command.PaymentChannelConfigurationId);
        var endpoint = ResolveActiveInboundEndpoint(channel);
        var externalMessageId = NormalizeOptional(command.ExternalMessageId);
        var correlationKey = NormalizeOptional(command.CorrelationKey);

        if (externalMessageId is null && correlationKey is null)
        {
            throw new InvalidOperationException("Gateway callback requires an external message ID or correlation key.");
        }

        if (!string.Equals(endpoint.AuthenticationModeCode, "SIGNATURE", StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException("Gateway callback hardening currently supports signature-authenticated inbound endpoints only.");
        }

        if (string.IsNullOrWhiteSpace(endpoint.CredentialReference))
        {
            throw new InvalidOperationException("Gateway callback signature validation requires an endpoint credential reference.");
        }

        var suppliedSignature = NormalizeOptional(command.CallbackSignature);
        var expectedSignature = GatewayCallbackSecurity.ComputeSignature(
            endpoint.CredentialReference,
            command.PayloadText,
            externalMessageId,
            correlationKey);

        if (!string.Equals(suppliedSignature, expectedSignature, StringComparison.OrdinalIgnoreCase))
        {
            return new GatewayCallbackReceiveResultView(
                "INVALID_SIGNATURE",
                "Gateway callback signature validation failed.",
                null);
        }

        var existing = FindExistingCallback(channel, endpoint.Id, externalMessageId, correlationKey);
        if (existing is not null)
        {
            return new GatewayCallbackReceiveResultView(
                "REPLAY_BLOCKED",
                "Gateway callback replay was blocked because the callback was already received for this channel.",
                existing);
        }

        var inboxRepository = new InboxMessageRepository(_dbContext);
        var received = inboxRepository.Receive(new ReceiveInboxMessageCommand(
            GatewayCallbackSystemUserId,
            endpoint.Id,
            NormalizeMessageType(command.MessageType),
            externalMessageId,
            correlationKey,
            command.PayloadText));

        return new GatewayCallbackReceiveResultView(
            "ACCEPTED",
            "Gateway callback accepted and queued for downstream processing.",
            Map(received, endpoint, channel));
    }

    public GatewayCallbackReceiptView Replay(long paymentChannelConfigurationId, long inboxMessageId, long actorUserId, ReplayDeadLetterQueueItemRequest request)
    {
        var channel = ResolveChannel(paymentChannelConfigurationId);
        var message = _dbContext.InboxMessages
            .SingleOrDefault(entry => entry.Id == inboxMessageId && entry.IntegrationEndpointId == channel.InboundIntegrationEndpointId!.Value)
            ?? throw new InvalidOperationException("Gateway callback was not found for the selected payment channel.");

        message.Replay(request.ReplayReasonText, DateTime.UtcNow.AddSeconds(request.DelaySeconds));
        message.Touch(actorUserId);
        _dbContext.SaveChanges();

        var endpoint = _dbContext.IntegrationEndpointRegistryEntries
            .AsNoTracking()
            .Single(entry => entry.Id == message.IntegrationEndpointId);
        return Map(message, endpoint, channel);
    }

    public ProcessGatewayCallbackBatchResultView ProcessCallbacks(ProcessGatewayCallbackBatchCommand command)
    {
        var processorCode = command.ProcessorCode?.Trim().ToUpperInvariant();
        if (string.IsNullOrWhiteSpace(processorCode))
        {
            throw new InvalidOperationException("Gateway callback processor code is required.");
        }

        if (command.BatchSize is < 1 or > 100)
        {
            throw new InvalidOperationException("Gateway callback batch size must be between 1 and 100.");
        }

        var channel = ResolveActiveChannel(command.PaymentChannelConfigurationId);
        var endpoint = ResolveActiveInboundEndpoint(channel);
        var messages = _dbContext.InboxMessages
            .Where(entry =>
                entry.IntegrationEndpointId == endpoint.Id
                && (entry.StatusCode == "RECEIVED" || entry.StatusCode == "RETRY")
                && entry.AvailableAtUtc <= DateTime.UtcNow)
            .OrderBy(entry => entry.AvailableAtUtc)
            .ThenBy(entry => entry.Id)
            .Take(command.BatchSize)
            .ToArray();

        var updatedExecutionIds = new HashSet<long>();
        var settlementBatchIds = new HashSet<long>();

        foreach (var message in messages)
        {
            try
            {
                var mutation = ApplyProviderCallback(channel, message);
                if (mutation.PaymentGatewayExecutionId.HasValue)
                {
                    updatedExecutionIds.Add(mutation.PaymentGatewayExecutionId.Value);
                }

                if (mutation.PaymentSettlementBatchId.HasValue)
                {
                    settlementBatchIds.Add(mutation.PaymentSettlementBatchId.Value);
                }

                message.MarkProcessed(processorCode);
            }
            catch (InvalidOperationException ex)
            {
                if (message.AttemptNumber < endpoint.MaxRetryCount)
                {
                    message.ScheduleRetry(ex.Message, DateTime.UtcNow.AddMinutes(5));
                }
                else
                {
                    message.MarkFailed(ex.Message);
                }
            }
        }

        _dbContext.SaveChanges();

        return new ProcessGatewayCallbackBatchResultView(
            channel.Id,
            processorCode,
            command.BatchSize,
            messages.Length,
            updatedExecutionIds.Count,
            settlementBatchIds.Count,
            updatedExecutionIds.Order().ToArray(),
            settlementBatchIds.Order().ToArray());
    }

    private PaymentChannelConfiguration ResolveChannel(long paymentChannelConfigurationId)
    {
        var channel = _dbContext.PaymentChannelConfigurations
            .AsNoTracking()
            .SingleOrDefault(entry => entry.Id == paymentChannelConfigurationId)
            ?? throw new InvalidOperationException("Payment channel was not found.");

        if (!channel.InboundIntegrationEndpointId.HasValue)
        {
            throw new InvalidOperationException("Payment channel does not have an inbound callback endpoint configured.");
        }

        return channel;
    }

    private PaymentChannelConfiguration ResolveActiveChannel(long paymentChannelConfigurationId)
    {
        var channel = ResolveChannel(paymentChannelConfigurationId);
        if (!channel.IsActive)
        {
            throw new InvalidOperationException("Gateway callbacks can only be received for active payment channels.");
        }

        return channel;
    }

    private IntegrationEndpointRegistryEntry ResolveActiveInboundEndpoint(PaymentChannelConfiguration channel)
    {
        var endpointId = channel.InboundIntegrationEndpointId
            ?? throw new InvalidOperationException("Payment channel does not have an inbound callback endpoint configured.");

        var endpoint = _dbContext.IntegrationEndpointRegistryEntries
            .AsNoTracking()
            .SingleOrDefault(entry => entry.Id == endpointId)
            ?? throw new InvalidOperationException("Gateway callback endpoint was not found.");

        if (!endpoint.IsActive)
        {
            throw new InvalidOperationException("Gateway callbacks require an active inbound integration endpoint.");
        }

        if (!string.Equals(endpoint.DirectionCode, "INBOUND", StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException("Gateway callback endpoint must be configured as inbound.");
        }

        if (!string.Equals(endpoint.IntegrationProviderCode, channel.IntegrationProviderCode, StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException("Gateway callback endpoint provider must match the payment channel provider.");
        }

        return endpoint;
    }

    private GatewayCallbackReceiptView? FindExistingCallback(
        PaymentChannelConfiguration channel,
        long integrationEndpointId,
        string? externalMessageId,
        string? correlationKey)
    {
        var existing = (
            from message in _dbContext.InboxMessages.AsNoTracking()
            join endpoint in _dbContext.IntegrationEndpointRegistryEntries.AsNoTracking()
                on message.IntegrationEndpointId equals endpoint.Id
            where message.IntegrationEndpointId == integrationEndpointId
            where (externalMessageId != null && message.ExternalMessageId == externalMessageId)
                || (correlationKey != null && message.CorrelationKey == correlationKey)
            orderby message.CreatedAtUtc descending, message.Id descending
            select new { message, endpoint })
            .FirstOrDefault();

        return existing is null ? null : Map(existing.message, existing.endpoint, channel);
    }

    private ProviderCallbackMutationResult ApplyProviderCallback(PaymentChannelConfiguration channel, InboxMessage message)
    {
        using var jsonDocument = JsonDocument.Parse(message.PayloadText);
        var payload = jsonDocument.RootElement;
        var correlationKey = FirstNonEmpty(
            message.CorrelationKey,
            ReadString(payload, "correlationKey"),
            ReadString(payload, "refundNumber"),
            ReadString(payload, "referenceNumber"));

        if (correlationKey is null)
        {
            throw new InvalidOperationException("Gateway callback could not resolve a correlation key to a payment execution.");
        }

        var execution = _dbContext.PaymentGatewayExecutions
            .SingleOrDefault(entry =>
                entry.PaymentChannelConfigurationId == channel.Id
                && entry.CorrelationKey == correlationKey)
            ?? throw new InvalidOperationException($"Gateway callback correlation key {correlationKey} does not match any payment execution.");

        var providerReference = FirstNonEmpty(
            ReadString(payload, "providerReference"),
            ReadString(payload, "transactionReference"),
            execution.ProviderReference);
        var providerStatusCode = FirstNonEmpty(
            ReadString(payload, "settlementStatus"),
            ReadString(payload, "transactionStatus"),
            ReadString(payload, "status"),
            message.MessageType)
            ?? throw new InvalidOperationException("Gateway callback did not include a recognizable provider status.");

        var normalizedProviderStatusCode = providerStatusCode.Trim().ToUpperInvariant();
        var payloadText = payload.GetRawText();
        long? settlementBatchId = null;

        if (normalizedProviderStatusCode is "FAILED" or "REJECTED" or "DECLINED")
        {
            execution.MarkFailed($"Provider callback returned status {normalizedProviderStatusCode}.", normalizedProviderStatusCode, payloadText);
            if (string.Equals(execution.SourceDocumentType, "CUSTOMER_PAYMENT_REQUEST", StringComparison.OrdinalIgnoreCase))
            {
                var paymentRequest = _dbContext.CustomerPaymentRequests.Single(entry => entry.Id == execution.SourceDocumentId);
                paymentRequest.MarkFailed($"Provider callback returned status {normalizedProviderStatusCode}.");
            }
        }
        else if (normalizedProviderStatusCode is "SETTLED" or "PAID")
        {
            execution.MarkSettled(normalizedProviderStatusCode, providerReference, payloadText);
            var settledAmount = ReadDecimal(payload, "settledAmount") ?? execution.AmountAmount;
            if (string.Equals(execution.SourceDocumentType, "CUSTOMER_PAYMENT_REQUEST", StringComparison.OrdinalIgnoreCase))
            {
                ApplyCustomerCollectionSettlement(
                    execution,
                    providerReference,
                    settledAmount,
                    ReadString(payload, "notes"));
            }
            else if (string.Equals(execution.SourceDocumentType, "AP_PAYMENT", StringComparison.OrdinalIgnoreCase))
            {
                var settlementDate = ReadDateOnly(payload, "settlementDate") ?? DateOnly.FromDateTime(DateTime.UtcNow);
                var feeAmount = ReadDecimal(payload, "feeAmount") ?? 0m;
                var netAmount = ReadDecimal(payload, "netAmount") ?? Math.Max(0m, settledAmount - feeAmount);
                settlementBatchId = UpsertSupplierPayoutSettlementFromCallback(
                    channel,
                    execution,
                    settlementDate,
                    ReadString(payload, "externalBatchReference"),
                    providerReference,
                    settledAmount,
                    feeAmount,
                    netAmount,
                    ReadString(payload, "notes"));
            }
            else
            {
                var settlementDate = ReadDateOnly(payload, "settlementDate") ?? DateOnly.FromDateTime(DateTime.UtcNow);
                var feeAmount = ReadDecimal(payload, "feeAmount") ?? 0m;
                var netAmount = ReadDecimal(payload, "netAmount") ?? Math.Max(0m, settledAmount - feeAmount);
                settlementBatchId = UpsertSettlementFromCallback(
                    channel,
                    execution,
                    settlementDate,
                    ReadString(payload, "externalBatchReference"),
                    providerReference,
                    settledAmount,
                    feeAmount,
                    netAmount,
                    ReadString(payload, "notes"));
            }
        }
        else
        {
            execution.Acknowledge(normalizedProviderStatusCode, providerReference, payloadText);
            if (string.Equals(execution.SourceDocumentType, "CUSTOMER_PAYMENT_REQUEST", StringComparison.OrdinalIgnoreCase))
            {
                var paymentRequest = _dbContext.CustomerPaymentRequests.Single(entry => entry.Id == execution.SourceDocumentId);
                paymentRequest.MarkProcessing();
            }
        }

        return new ProviderCallbackMutationResult(execution.Id, settlementBatchId);
    }

    private void ApplyCustomerCollectionSettlement(
        PaymentGatewayExecution execution,
        string? providerReference,
        decimal settledAmount,
        string? notes)
    {
        var paymentRequest = _dbContext.CustomerPaymentRequests.Single(entry => entry.Id == execution.SourceDocumentId);
        if (paymentRequest.SettledAccountsReceivableReceiptId.HasValue)
        {
            paymentRequest.MarkPaid(paymentRequest.SettledAccountsReceivableReceiptId.Value);
            return;
        }

        var normalizedAmount = NormalizeAmount(settledAmount);
        var receiptRepository = new AccountsReceivableReceiptRepository(_dbContext);
        var createdReceipt = receiptRepository.Create(new CreateAccountsReceivableReceiptCommand(
            paymentRequest.CustomerId,
            paymentRequest.PaymentDate,
            "PAYMENT_GATEWAY",
            paymentRequest.CurrencyCode,
            normalizedAmount,
            providerReference ?? paymentRequest.ReferenceNumber,
            BuildCustomerCollectionReceiptNotes(paymentRequest, notes),
            [
                new CreateAccountsReceivableReceiptAllocationCommand(
                    paymentRequest.AccountsReceivableInvoiceId,
                    normalizedAmount,
                    paymentRequest.PaymentDate)
            ]));
        receiptRepository.Submit(createdReceipt.AccountsReceivableReceiptId, GatewayCallbackSystemUserId);
        receiptRepository.Approve(createdReceipt.AccountsReceivableReceiptId, GatewayCallbackSystemCheckerUserId);
        var postedReceipt = receiptRepository.Post(createdReceipt.AccountsReceivableReceiptId, GatewayCallbackSystemCheckerUserId);
        paymentRequest.MarkPaid(postedReceipt.AccountsReceivableReceiptId);
    }

    private static string BuildCustomerCollectionReceiptNotes(CustomerPaymentRequest paymentRequest, string? notes)
    {
        var segments = new List<string>
        {
            $"Gateway collection settled for {paymentRequest.PaymentRequestNumber}."
        };

        if (!string.IsNullOrWhiteSpace(paymentRequest.Notes))
        {
            segments.Add(paymentRequest.Notes.Trim());
        }

        if (!string.IsNullOrWhiteSpace(notes))
        {
            segments.Add(notes.Trim());
        }

        return string.Join(" ", segments);
    }

    private long UpsertSettlementFromCallback(
        PaymentChannelConfiguration channel,
        PaymentGatewayExecution execution,
        DateOnly settlementDate,
        string? externalBatchReference,
        string? providerReference,
        decimal settledAmount,
        decimal feeAmount,
        decimal netAmount,
        string? notes)
    {
        var refund = _dbContext.CustomerRefunds.Single(entry => entry.Id == execution.SourceDocumentId);
        var normalizedBatchReference = NormalizeOptional(externalBatchReference);
        var batch = _dbContext.Set<PaymentSettlementBatch>()
            .Include(entry => entry.Lines)
            .Include(entry => entry.Exceptions)
            .FirstOrDefault(entry =>
                entry.PaymentChannelConfigurationId == channel.Id
                && entry.SettlementDate == settlementDate
                && entry.StatusId != CashLineERP.SharedKernel.Workflow.WorkflowStatusSeedIds.Posted
                && entry.ExternalBatchReference == normalizedBatchReference);

        if (batch is null)
        {
            var nextSequence = (_dbContext.Set<PaymentSettlementBatch>().Max(entry => (long?)entry.Id) ?? 0L) + 1L;
            batch = new PaymentSettlementBatch(
                batchNumber: $"PSET-{nextSequence:D6}",
                paymentChannelConfigurationId: channel.Id,
                settlementDate: settlementDate,
                currencyCode: channel.CurrencyCode,
                externalBatchReference: normalizedBatchReference);

            batch.AddLine(
                sourceDocumentType: "CUSTOMER_REFUND",
                sourceDocumentId: refund.Id,
                sourceDocumentNumber: refund.RefundNumber,
                partyId: refund.CustomerId,
                internalAmount: refund.AmountAmount,
                settledAmount: settledAmount,
                feeAmount: feeAmount,
                netAmount: netAmount,
                providerReference: providerReference,
                notes: notes,
                hasException: string.IsNullOrWhiteSpace(providerReference) || NormalizeAmount(refund.AmountAmount) != NormalizeAmount(settledAmount));

            _dbContext.Set<PaymentSettlementBatch>().Add(batch);
            _dbContext.SaveChanges();
        }

        var line = batch.Lines.SingleOrDefault(entry =>
            entry.SourceDocumentType == "CUSTOMER_REFUND"
            && entry.SourceDocumentId == refund.Id);

        if (line is null)
        {
            batch.AddLine(
                sourceDocumentType: "CUSTOMER_REFUND",
                sourceDocumentId: refund.Id,
                sourceDocumentNumber: refund.RefundNumber,
                partyId: refund.CustomerId,
                internalAmount: refund.AmountAmount,
                settledAmount: settledAmount,
                feeAmount: feeAmount,
                netAmount: netAmount,
                providerReference: providerReference,
                notes: notes,
                hasException: string.IsNullOrWhiteSpace(providerReference) || NormalizeAmount(refund.AmountAmount) != NormalizeAmount(settledAmount));
            _dbContext.SaveChanges();
            line = batch.Lines.Single(entry =>
                entry.SourceDocumentType == "CUSTOMER_REFUND"
                && entry.SourceDocumentId == refund.Id);
        }
        else
        {
            line.ApplyProviderSettlement(
                NormalizeAmount(settledAmount),
                NormalizeAmount(feeAmount),
                NormalizeAmount(netAmount),
                providerReference,
                notes,
                settlementStatusCode: "MATCHED");
            batch.RefreshTotals();
        }

        SyncSettlementExceptions(batch, line, refund, providerReference, settledAmount);
        return batch.Id;
    }

    private long UpsertSupplierPayoutSettlementFromCallback(
        PaymentChannelConfiguration channel,
        PaymentGatewayExecution execution,
        DateOnly settlementDate,
        string? externalBatchReference,
        string? providerReference,
        decimal settledAmount,
        decimal feeAmount,
        decimal netAmount,
        string? notes)
    {
        var payment = _dbContext.AccountsPayablePayments.Single(entry => entry.Id == execution.SourceDocumentId);
        var normalizedBatchReference = NormalizeOptional(externalBatchReference);
        var batch = _dbContext.Set<PaymentSettlementBatch>()
            .Include(entry => entry.Lines)
            .Include(entry => entry.Exceptions)
            .FirstOrDefault(entry =>
                entry.PaymentChannelConfigurationId == channel.Id
                && entry.SettlementDate == settlementDate
                && entry.StatusId != CashLineERP.SharedKernel.Workflow.WorkflowStatusSeedIds.Posted
                && entry.ExternalBatchReference == normalizedBatchReference);

        if (batch is null)
        {
            var nextSequence = (_dbContext.Set<PaymentSettlementBatch>().Max(entry => (long?)entry.Id) ?? 0L) + 1L;
            batch = new PaymentSettlementBatch(
                batchNumber: $"PSET-{nextSequence:D6}",
                paymentChannelConfigurationId: channel.Id,
                settlementDate: settlementDate,
                currencyCode: channel.CurrencyCode,
                externalBatchReference: normalizedBatchReference);

            batch.AddLine(
                sourceDocumentType: "AP_PAYMENT",
                sourceDocumentId: payment.Id,
                sourceDocumentNumber: payment.PaymentNumber,
                partyId: payment.SupplierId,
                internalAmount: payment.AmountAmount,
                settledAmount: settledAmount,
                feeAmount: feeAmount,
                netAmount: netAmount,
                providerReference: providerReference,
                notes: notes,
                hasException: string.IsNullOrWhiteSpace(providerReference) || NormalizeAmount(payment.AmountAmount) != NormalizeAmount(settledAmount));

            _dbContext.Set<PaymentSettlementBatch>().Add(batch);
            _dbContext.SaveChanges();
        }

        var line = batch.Lines.SingleOrDefault(entry =>
            entry.SourceDocumentType == "AP_PAYMENT"
            && entry.SourceDocumentId == payment.Id);

        if (line is null)
        {
            batch.AddLine(
                sourceDocumentType: "AP_PAYMENT",
                sourceDocumentId: payment.Id,
                sourceDocumentNumber: payment.PaymentNumber,
                partyId: payment.SupplierId,
                internalAmount: payment.AmountAmount,
                settledAmount: settledAmount,
                feeAmount: feeAmount,
                netAmount: netAmount,
                providerReference: providerReference,
                notes: notes,
                hasException: string.IsNullOrWhiteSpace(providerReference) || NormalizeAmount(payment.AmountAmount) != NormalizeAmount(settledAmount));
            _dbContext.SaveChanges();
            line = batch.Lines.Single(entry =>
                entry.SourceDocumentType == "AP_PAYMENT"
                && entry.SourceDocumentId == payment.Id);
        }
        else
        {
            line.ApplyProviderSettlement(
                NormalizeAmount(settledAmount),
                NormalizeAmount(feeAmount),
                NormalizeAmount(netAmount),
                providerReference,
                notes,
                settlementStatusCode: "MATCHED");
            batch.RefreshTotals();
        }

        SyncSupplierPayoutSettlementExceptions(batch, line, payment, providerReference, settledAmount);
        return batch.Id;
    }

    private void SyncSettlementExceptions(
        PaymentSettlementBatch batch,
        PaymentSettlementBatchLine line,
        CustomerRefund refund,
        string? providerReference,
        decimal settledAmount)
    {
        var lineExceptions = batch.Exceptions.Where(entry => entry.PaymentSettlementBatchLineId == line.Id).ToArray();
        var hasProviderReference = !string.IsNullOrWhiteSpace(providerReference);
        var hasAmountVariance = NormalizeAmount(refund.AmountAmount) != NormalizeAmount(settledAmount);

        var missingRefException = lineExceptions.SingleOrDefault(entry => entry.ExceptionCode == "MISSING_PROVIDER_REFERENCE" && entry.StatusCode == "OPEN");
        if (!hasProviderReference && missingRefException is null)
        {
            _dbContext.Set<PaymentSettlementException>().Add(new PaymentSettlementException(
                batch.Id,
                line.Id,
                line.SourceDocumentType,
                line.SourceDocumentId,
                line.SourceDocumentNumber,
                line.PartyId,
                "MISSING_PROVIDER_REFERENCE",
                0m,
                "Provider callback did not include a provider reference."));
        }
        else if (hasProviderReference && missingRefException is not null)
        {
            missingRefException.Confirm(GatewayCallbackSystemUserId, "Provider callback supplied the provider reference.");
        }

        var varianceException = lineExceptions.SingleOrDefault(entry => entry.ExceptionCode == "AMOUNT_VARIANCE" && entry.StatusCode == "OPEN");
        if (hasAmountVariance && varianceException is null)
        {
            _dbContext.Set<PaymentSettlementException>().Add(new PaymentSettlementException(
                batch.Id,
                line.Id,
                line.SourceDocumentType,
                line.SourceDocumentId,
                line.SourceDocumentNumber,
                line.PartyId,
                "AMOUNT_VARIANCE",
                NormalizeAmount(settledAmount) - NormalizeAmount(refund.AmountAmount),
                "Provider callback settled amount differs from the internal refund amount."));
        }
        else if (!hasAmountVariance && varianceException is not null)
        {
            varianceException.Confirm(GatewayCallbackSystemUserId, "Provider callback aligned the settled amount with the internal refund amount.");
        }

        _dbContext.SaveChanges();

        batch.RefreshExceptionSummary(
            openExceptionCount: _dbContext.Set<PaymentSettlementException>().Count(entry => entry.PaymentSettlementBatchId == batch.Id && entry.StatusCode == "OPEN"),
            resolvedExceptionCount: _dbContext.Set<PaymentSettlementException>().Count(entry => entry.PaymentSettlementBatchId == batch.Id && entry.StatusCode != "OPEN"));

        var remainingOpenLineExceptions = _dbContext.Set<PaymentSettlementException>().Count(entry => entry.PaymentSettlementBatchLineId == line.Id && entry.StatusCode == "OPEN");
        if (remainingOpenLineExceptions == 0)
        {
            line.MarkResolved("SYNCED");
        }
        else
        {
            line.MarkException();
        }
    }

    private void SyncSupplierPayoutSettlementExceptions(
        PaymentSettlementBatch batch,
        PaymentSettlementBatchLine line,
        AccountsPayablePayment payment,
        string? providerReference,
        decimal settledAmount)
    {
        var lineExceptions = batch.Exceptions.Where(entry => entry.PaymentSettlementBatchLineId == line.Id).ToArray();
        var hasProviderReference = !string.IsNullOrWhiteSpace(providerReference);
        var hasAmountVariance = NormalizeAmount(payment.AmountAmount) != NormalizeAmount(settledAmount);

        var missingRefException = lineExceptions.SingleOrDefault(entry => entry.ExceptionCode == "MISSING_PROVIDER_REFERENCE" && entry.StatusCode == "OPEN");
        if (!hasProviderReference && missingRefException is null)
        {
            _dbContext.Set<PaymentSettlementException>().Add(new PaymentSettlementException(
                batch.Id,
                line.Id,
                line.SourceDocumentType,
                line.SourceDocumentId,
                line.SourceDocumentNumber,
                line.PartyId,
                "MISSING_PROVIDER_REFERENCE",
                0m,
                "Provider callback did not include a provider reference."));
        }
        else if (hasProviderReference && missingRefException is not null)
        {
            missingRefException.Confirm(GatewayCallbackSystemUserId, "Provider callback supplied the provider reference.");
        }

        var varianceException = lineExceptions.SingleOrDefault(entry => entry.ExceptionCode == "AMOUNT_VARIANCE" && entry.StatusCode == "OPEN");
        if (hasAmountVariance && varianceException is null)
        {
            _dbContext.Set<PaymentSettlementException>().Add(new PaymentSettlementException(
                batch.Id,
                line.Id,
                line.SourceDocumentType,
                line.SourceDocumentId,
                line.SourceDocumentNumber,
                line.PartyId,
                "AMOUNT_VARIANCE",
                NormalizeAmount(settledAmount) - NormalizeAmount(payment.AmountAmount),
                "Provider callback settled amount differs from the internal supplier payment amount."));
        }
        else if (!hasAmountVariance && varianceException is not null)
        {
            varianceException.Confirm(GatewayCallbackSystemUserId, "Provider callback aligned the settled amount with the internal supplier payment amount.");
        }

        _dbContext.SaveChanges();

        batch.RefreshExceptionSummary(
            openExceptionCount: _dbContext.Set<PaymentSettlementException>().Count(entry => entry.PaymentSettlementBatchId == batch.Id && entry.StatusCode == "OPEN"),
            resolvedExceptionCount: _dbContext.Set<PaymentSettlementException>().Count(entry => entry.PaymentSettlementBatchId == batch.Id && entry.StatusCode != "OPEN"));

        var remainingOpenLineExceptions = _dbContext.Set<PaymentSettlementException>().Count(entry => entry.PaymentSettlementBatchLineId == line.Id && entry.StatusCode == "OPEN");
        if (remainingOpenLineExceptions == 0)
        {
            line.MarkResolved("SYNCED");
        }
        else
        {
            line.MarkException();
        }
    }

    private static decimal NormalizeAmount(decimal amount) => decimal.Round(amount, 2, MidpointRounding.AwayFromZero);

    private static string? FirstNonEmpty(params string?[] candidates) =>
        candidates.FirstOrDefault(candidate => !string.IsNullOrWhiteSpace(candidate))?.Trim();

    private static string? ReadString(JsonElement payload, string propertyName) =>
        payload.TryGetProperty(propertyName, out var element) && element.ValueKind == JsonValueKind.String
            ? element.GetString()
            : null;

    private static decimal? ReadDecimal(JsonElement payload, string propertyName)
    {
        if (!payload.TryGetProperty(propertyName, out var element))
        {
            return null;
        }

        return element.ValueKind switch
        {
            JsonValueKind.Number when element.TryGetDecimal(out var number) => number,
            JsonValueKind.String when decimal.TryParse(element.GetString(), out var parsed) => parsed,
            _ => null
        };
    }

    private static DateOnly? ReadDateOnly(JsonElement payload, string propertyName)
    {
        if (!payload.TryGetProperty(propertyName, out var element) || element.ValueKind != JsonValueKind.String)
        {
            return null;
        }

        return DateOnly.TryParse(element.GetString(), out var date) ? date : null;
    }

    private static string NormalizeMessageType(string? messageType) =>
        string.IsNullOrWhiteSpace(messageType)
            ? "PAYMENT_CALLBACK"
            : messageType.Trim().ToUpperInvariant();

    private static string? NormalizeOptional(string? value) =>
        string.IsNullOrWhiteSpace(value) ? null : value.Trim();

    private static GatewayCallbackReceiptView Map(
        InboxMessageDetailView message,
        IntegrationEndpointRegistryEntry endpoint,
        PaymentChannelConfiguration channel) =>
        new(
            message.InboxMessageId,
            message.MessageNumber,
            channel.Id,
            channel.ChannelCode,
            channel.ChannelName,
            channel.IntegrationProviderCode,
            message.IntegrationEndpointId,
            endpoint.EndpointCode,
            message.MessageType,
            message.ExternalMessageId,
            message.CorrelationKey,
            message.PayloadText,
            message.StatusCode,
            message.AttemptNumber,
            message.AvailableAtUtc,
            message.ProcessedAtUtc,
            message.ProcessedByProcess,
            message.LastErrorText,
            message.CreatedAtUtc);

    private static GatewayCallbackReceiptView Map(
        InboxMessage message,
        IntegrationEndpointRegistryEntry endpoint,
        PaymentChannelConfiguration channel) =>
        new(
            message.Id,
            message.MessageNumber,
            channel.Id,
            channel.ChannelCode,
            channel.ChannelName,
            channel.IntegrationProviderCode,
            message.IntegrationEndpointId,
            endpoint.EndpointCode,
            message.MessageType,
            message.ExternalMessageId,
            message.CorrelationKey,
            message.PayloadText,
            message.StatusCode,
            message.AttemptNumber,
            message.AvailableAtUtc,
            message.ProcessedAtUtc,
            message.ProcessedByProcess,
            message.LastErrorText,
            message.CreatedAtUtc);

    private sealed record ProviderCallbackMutationResult(
        long? PaymentGatewayExecutionId,
        long? PaymentSettlementBatchId);
}
