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

namespace CashLineERP.Infrastructure.Persistence.Repositories;

public sealed class PaymentGatewayExecutionRepository : IPaymentGatewayExecutionRepository
{
    private const string FawryPaymentStatusRelativePath = "/ECommerceWeb/Fawry/payments/status/v2";
    private const string FawryCancelUnpaidRelativePath = "/ECommerceWeb/api/orders/cancel-unpaid-order";
    private readonly CashLineErpDbContext _dbContext;

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

    public IReadOnlyCollection<PaymentGatewayExecutionView> GetExecutions(long? paymentChannelConfigurationId = null, string? statusCode = null)
    {
        var normalizedStatusCode = string.IsNullOrWhiteSpace(statusCode) ? null : statusCode.Trim().ToUpperInvariant();
        var channels = _dbContext.PaymentChannelConfigurations.AsNoTracking().ToDictionary(entry => entry.Id);
        var endpoints = _dbContext.IntegrationEndpointRegistryEntries.AsNoTracking().ToDictionary(entry => entry.Id);

        return _dbContext.PaymentGatewayExecutions
            .AsNoTracking()
            .Where(entry => !paymentChannelConfigurationId.HasValue || entry.PaymentChannelConfigurationId == paymentChannelConfigurationId.Value)
            .Where(entry => normalizedStatusCode == null || entry.StatusCode == normalizedStatusCode)
            .OrderBy(entry => entry.StatusCode)
            .ThenBy(entry => entry.CreatedAtUtc)
            .ThenBy(entry => entry.Id)
            .ToArray()
            .Select(entry => Map(entry, channels[entry.PaymentChannelConfigurationId], endpoints[entry.IntegrationEndpointId]))
            .ToArray();
    }

    public PaymentGatewayExecutionView? GetExecution(long paymentGatewayExecutionId)
    {
        var execution = _dbContext.PaymentGatewayExecutions
            .AsNoTracking()
            .SingleOrDefault(entry => entry.Id == paymentGatewayExecutionId);

        if (execution is null)
        {
            return null;
        }

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

    public IReadOnlyCollection<PaymentGatewayExecutionView> GetExecutionsForSourceDocument(string sourceDocumentType, long sourceDocumentId)
    {
        var normalizedSourceDocumentType = sourceDocumentType.Trim().ToUpperInvariant();
        return GetExecutions()
            .Where(entry => entry.SourceDocumentType == normalizedSourceDocumentType && entry.SourceDocumentId == sourceDocumentId)
            .ToArray();
    }

    public PaymentGatewayExecutionView QueueCustomerCollection(long customerPaymentRequestId, long actorUserId)
    {
        var request = _dbContext.CustomerPaymentRequests.SingleOrDefault(entry => entry.Id == customerPaymentRequestId)
            ?? throw new InvalidOperationException("Customer payment request was not found.");

        if (request.StatusCode != "PENDING_CUSTOMER_ACTION" && request.StatusCode != "FAILED")
        {
            throw new InvalidOperationException("Only pending or failed customer payment requests can be queued for provider execution.");
        }

        var existingActiveExecution = _dbContext.PaymentGatewayExecutions
            .AsNoTracking()
            .Any(entry =>
                entry.SourceDocumentType == "CUSTOMER_PAYMENT_REQUEST"
                && entry.SourceDocumentId == request.Id
                && entry.StatusCode != "FAILED");

        if (existingActiveExecution)
        {
            throw new InvalidOperationException("Customer payment request already has an active payment provider execution.");
        }

        var channel = ResolveCollectionPaymentChannel(request);
        var baseCorrelationKey = request.CorrelationKey ?? request.PaymentRequestNumber;
        return QueueExecution(
            channel,
            sourceDocumentType: "CUSTOMER_PAYMENT_REQUEST",
            sourceDocumentId: request.Id,
            sourceDocumentNumber: request.PaymentRequestNumber,
            partyId: request.CustomerId,
            currencyCode: request.CurrencyCode,
            amountAmount: request.AmountAmount,
            correlationKey: BuildExecutionCorrelationKey("CUSTOMER_PAYMENT_REQUEST", request.Id, baseCorrelationKey),
            actorUserId: actorUserId);
    }

    public PaymentGatewayExecutionView QueueSupplierPayout(long accountsPayablePaymentId, string integrationProviderCode, long actorUserId)
    {
        var payment = _dbContext.AccountsPayablePayments.SingleOrDefault(entry => entry.Id == accountsPayablePaymentId)
            ?? throw new InvalidOperationException("Accounts payable payment was not found.");

        if (payment.StatusId != WorkflowStatusSeedIds.Posted)
        {
            throw new InvalidOperationException("Only posted supplier payments can be queued for provider execution.");
        }

        var existingActiveExecution = _dbContext.PaymentGatewayExecutions
            .AsNoTracking()
            .Any(entry =>
                entry.SourceDocumentType == "AP_PAYMENT"
                && entry.SourceDocumentId == payment.Id
                && entry.StatusCode != "FAILED");

        if (existingActiveExecution)
        {
            throw new InvalidOperationException("Supplier payment already has an active payment provider execution.");
        }

        var channel = ResolveSupplierPayoutChannel(integrationProviderCode, payment.CurrencyCode);
        var baseCorrelationKey = payment.PaymentNumber;
        return QueueExecution(
            channel,
            sourceDocumentType: "AP_PAYMENT",
            sourceDocumentId: payment.Id,
            sourceDocumentNumber: payment.PaymentNumber,
            partyId: payment.SupplierId,
            currencyCode: payment.CurrencyCode,
            amountAmount: payment.AmountAmount,
            correlationKey: BuildExecutionCorrelationKey("AP_PAYMENT", payment.Id, baseCorrelationKey),
            actorUserId: actorUserId);
    }

    public PaymentGatewayExecutionView QueueCustomerRefund(long customerRefundId, long actorUserId)
    {
        var refund = _dbContext.CustomerRefunds.SingleOrDefault(entry => entry.Id == customerRefundId)
            ?? throw new InvalidOperationException("Customer refund was not found.");

        if (refund.StatusId != WorkflowStatusSeedIds.Posted)
        {
            throw new InvalidOperationException("Only posted customer refunds can be queued for provider execution.");
        }

        var existingActiveExecution = _dbContext.PaymentGatewayExecutions
            .AsNoTracking()
            .Any(entry =>
                entry.SourceDocumentType == "CUSTOMER_REFUND"
                && entry.SourceDocumentId == refund.Id
                && entry.StatusCode != "FAILED");

        if (existingActiveExecution)
        {
            throw new InvalidOperationException("Customer refund already has an active payment provider execution.");
        }

        var channel = ResolveRefundProviderContractChannel(refund);
        var baseCorrelationKey = refund.RefundNumber;
        return QueueExecution(
            channel,
            sourceDocumentType: "CUSTOMER_REFUND",
            sourceDocumentId: refund.Id,
            sourceDocumentNumber: refund.RefundNumber,
            partyId: refund.CustomerId,
            currencyCode: refund.CurrencyCode,
            amountAmount: refund.AmountAmount,
            correlationKey: BuildExecutionCorrelationKey("CUSTOMER_REFUND", refund.Id, baseCorrelationKey),
            actorUserId: actorUserId);
    }

    public PaymentGatewayProviderContractView GetCustomerCollectionProviderContract(long customerPaymentRequestId)
    {
        var request = _dbContext.CustomerPaymentRequests
            .AsNoTracking()
            .SingleOrDefault(entry => entry.Id == customerPaymentRequestId)
            ?? throw new InvalidOperationException("Customer payment request was not found.");

        var channel = ResolveCollectionPaymentChannel(request);
        var outboundEndpoint = ResolveActiveOutboundEndpoint(channel);
        var inboundEndpoint = channel.InboundIntegrationEndpointId.HasValue
            ? ResolveActiveInboundEndpoint(channel)
            : null;
        var latestExecution = _dbContext.PaymentGatewayExecutions
            .AsNoTracking()
            .Where(entry => entry.SourceDocumentType == "CUSTOMER_PAYMENT_REQUEST" && entry.SourceDocumentId == request.Id)
            .OrderByDescending(entry => entry.CreatedAtUtc)
            .ThenByDescending(entry => entry.Id)
            .FirstOrDefault();
        var invoice = _dbContext.AccountsReceivableInvoices
            .AsNoTracking()
            .Single(entry => entry.Id == request.AccountsReceivableInvoiceId);

        return BuildProviderContract(
            channel,
            outboundEndpoint,
            inboundEndpoint,
            paymentChannelConfigurationId: channel.Id,
            sourceDocumentType: "CUSTOMER_PAYMENT_REQUEST",
            sourceDocumentNumber: request.PaymentRequestNumber,
            referenceNumber: request.ReferenceNumber ?? invoice.InvoiceNumber,
            correlationKey: latestExecution?.CorrelationKey ?? request.CorrelationKey ?? request.PaymentRequestNumber,
            amountAmount: request.AmountAmount,
            currencyCode: request.CurrencyCode,
            partyId: request.CustomerId,
            providerReference: latestExecution?.ProviderReference);
    }

    public PaymentGatewayProviderContractView GetSupplierPayoutProviderContract(long accountsPayablePaymentId, string? integrationProviderCode = null)
    {
        var payment = _dbContext.AccountsPayablePayments
            .AsNoTracking()
            .SingleOrDefault(entry => entry.Id == accountsPayablePaymentId)
            ?? throw new InvalidOperationException("Accounts payable payment was not found.");

        if (payment.StatusId != WorkflowStatusSeedIds.Posted)
        {
            throw new InvalidOperationException("Only posted supplier payments can preview a payment provider contract.");
        }

        var latestExecution = _dbContext.PaymentGatewayExecutions
            .AsNoTracking()
            .Where(entry => entry.SourceDocumentType == "AP_PAYMENT" && entry.SourceDocumentId == payment.Id)
            .OrderByDescending(entry => entry.CreatedAtUtc)
            .ThenByDescending(entry => entry.Id)
            .FirstOrDefault();

        PaymentChannelConfiguration channel;
        if (latestExecution is not null)
        {
            channel = _dbContext.PaymentChannelConfigurations
                .AsNoTracking()
                .Single(entry => entry.Id == latestExecution.PaymentChannelConfigurationId);
        }
        else
        {
            channel = ResolveSupplierPayoutChannel(integrationProviderCode, payment.CurrencyCode);
        }

        var outboundEndpoint = ResolveActiveOutboundEndpoint(channel);
        var inboundEndpoint = channel.InboundIntegrationEndpointId.HasValue
            ? ResolveActiveInboundEndpoint(channel)
            : null;

        return BuildProviderContract(
            channel,
            outboundEndpoint,
            inboundEndpoint,
            paymentChannelConfigurationId: channel.Id,
            sourceDocumentType: "AP_PAYMENT",
            sourceDocumentNumber: payment.PaymentNumber,
            referenceNumber: payment.ReferenceNumber ?? payment.PaymentNumber,
            correlationKey: latestExecution?.CorrelationKey ?? payment.PaymentNumber,
            amountAmount: payment.AmountAmount,
            currencyCode: payment.CurrencyCode,
            partyId: payment.SupplierId,
            providerReference: latestExecution?.ProviderReference);
    }

    public PaymentGatewayProviderContractView GetCustomerRefundProviderContract(long customerRefundId)
    {
        var refund = _dbContext.CustomerRefunds
            .AsNoTracking()
            .SingleOrDefault(entry => entry.Id == customerRefundId)
            ?? throw new InvalidOperationException("Customer refund was not found.");

        var channel = ResolveRefundPaymentChannel(refund);
        var outboundEndpoint = ResolveActiveOutboundEndpoint(channel);
        var inboundEndpoint = channel.InboundIntegrationEndpointId.HasValue
            ? ResolveActiveInboundEndpoint(channel)
            : null;
        var latestExecution = _dbContext.PaymentGatewayExecutions
            .AsNoTracking()
            .Where(entry => entry.SourceDocumentType == "CUSTOMER_REFUND" && entry.SourceDocumentId == refund.Id)
            .OrderByDescending(entry => entry.CreatedAtUtc)
            .ThenByDescending(entry => entry.Id)
            .FirstOrDefault();

        return BuildProviderContract(
            channel,
            outboundEndpoint,
            inboundEndpoint,
            paymentChannelConfigurationId: channel.Id,
            sourceDocumentType: "CUSTOMER_REFUND",
            sourceDocumentNumber: refund.RefundNumber,
            referenceNumber: refund.ReferenceNumber ?? refund.RefundNumber,
            correlationKey: latestExecution?.CorrelationKey ?? refund.RefundNumber,
            amountAmount: refund.AmountAmount,
            currencyCode: refund.CurrencyCode,
            partyId: refund.CustomerId,
            providerReference: latestExecution?.ProviderReference);
    }

    public PaymentGatewayProviderContractView GetExecutionProviderStatusContract(long paymentGatewayExecutionId)
    {
        var (execution, channel, outboundEndpoint) = ResolveExecutionWithOutboundEndpoint(paymentGatewayExecutionId);
        var providerCode = channel.IntegrationProviderCode.Trim().ToUpperInvariant();

        return providerCode switch
        {
            "FAWRY" => BuildFawryStatusContract(execution, outboundEndpoint),
            _ => throw new InvalidOperationException($"Provider status contracts are not yet implemented for provider '{providerCode}'.")
        };
    }

    public PaymentGatewayProviderContractView GetExecutionProviderCancelContract(long paymentGatewayExecutionId)
    {
        var (execution, channel, outboundEndpoint) = ResolveExecutionWithOutboundEndpoint(paymentGatewayExecutionId);
        var providerCode = channel.IntegrationProviderCode.Trim().ToUpperInvariant();

        return providerCode switch
        {
            "FAWRY" => BuildFawryCancelContract(execution, outboundEndpoint),
            _ => throw new InvalidOperationException($"Provider cancellation contracts are not yet implemented for provider '{providerCode}'.")
        };
    }

    public DispatchPaymentGatewayExecutionBatchResultView DispatchPendingExecutions(DispatchPaymentGatewayExecutionBatchCommand command)
    {
        var processorCode = command.ProcessorCode?.Trim().ToUpperInvariant();
        if (string.IsNullOrWhiteSpace(processorCode))
        {
            throw new InvalidOperationException("Payment execution processor code is required.");
        }

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

        var pending = _dbContext.PaymentGatewayExecutions
            .Where(entry => (entry.StatusCode == "QUEUED" || entry.StatusCode == "RETRY")
                && (!command.PaymentChannelConfigurationId.HasValue || entry.PaymentChannelConfigurationId == command.PaymentChannelConfigurationId.Value))
            .OrderBy(entry => entry.CreatedAtUtc)
            .ThenBy(entry => entry.Id)
            .Take(command.BatchSize)
            .ToArray();

        foreach (var execution in pending)
        {
            var channel = _dbContext.PaymentChannelConfigurations.AsNoTracking().Single(entry => entry.Id == execution.PaymentChannelConfigurationId);
            var endpoint = ResolveActiveOutboundEndpoint(channel);
            var payloadText = JsonSerializer.Serialize(new
            {
                execution.SourceDocumentType,
                execution.SourceDocumentId,
                execution.SourceDocumentNumber,
                execution.AmountAmount,
                execution.CurrencyCode,
                execution.CorrelationKey,
                channel.ChannelCode,
                channel.IntegrationProviderCode
            });
            var signature = PaymentProviderPayloadSecurity.ComputeSignature(endpoint.CredentialReference ?? string.Empty, payloadText, execution.CorrelationKey);
            var providerReference = execution.ProviderReference ?? $"{channel.IntegrationProviderCode}-TX-{execution.Id:D6}";

            execution.Dispatch(
                processorCode,
                providerReference,
                JsonSerializer.Serialize(new
                {
                    requestAccepted = true,
                    providerCode = channel.IntegrationProviderCode,
                    endpoint = endpoint.EndpointCode,
                    signature
                }));

            OutboxEventWriter.Enqueue(
                _dbContext,
                sourceModuleCode: "FINANCE",
                aggregateType: "PAYMENT_GATEWAY_EXECUTION",
                aggregateId: execution.Id,
                eventType: "PAYMENT_PROVIDER_EXECUTION_DISPATCHED",
                payload: new
                {
                    execution.ExecutionNumber,
                    execution.SourceDocumentNumber,
                    execution.ProviderReference,
                    endpoint.EndpointCode,
                    channel.IntegrationProviderCode
                },
                actorUserId: null,
                correlationKey: execution.CorrelationKey);
        }

        _dbContext.SaveChanges();

        var views = pending.Select(entry => GetExecution(entry.Id)!).ToArray();
        return new DispatchPaymentGatewayExecutionBatchResultView(
            processorCode,
            command.BatchSize,
            views.Length,
            views);
    }

    private PaymentGatewayExecutionView QueueExecution(
        PaymentChannelConfiguration channel,
        string sourceDocumentType,
        long sourceDocumentId,
        string sourceDocumentNumber,
        long? partyId,
        string currencyCode,
        decimal amountAmount,
        string correlationKey,
        long actorUserId)
    {
        var nextSequence = (_dbContext.PaymentGatewayExecutions.Max(entry => (long?)entry.Id) ?? 0L) + 1L;
        var execution = new PaymentGatewayExecution(
            executionNumber: $"PAYEXEC-{nextSequence:D6}",
            paymentChannelConfigurationId: channel.Id,
            integrationEndpointId: channel.OutboundIntegrationEndpointId,
            sourceDocumentType: sourceDocumentType,
            sourceDocumentId: sourceDocumentId,
            sourceDocumentNumber: sourceDocumentNumber,
            partyId: partyId,
            currencyCode: currencyCode,
            amountAmount: amountAmount,
            correlationKey: correlationKey);
        execution.Touch(actorUserId);

        _dbContext.PaymentGatewayExecutions.Add(execution);
        OutboxEventWriter.Enqueue(
            _dbContext,
            sourceModuleCode: "FINANCE",
            aggregateType: "PAYMENT_GATEWAY_EXECUTION",
            aggregateId: nextSequence,
            eventType: "PAYMENT_PROVIDER_EXECUTION_QUEUED",
            payload: new
            {
                execution.ExecutionNumber,
                execution.PaymentChannelConfigurationId,
                execution.SourceDocumentType,
                execution.SourceDocumentId,
                execution.SourceDocumentNumber,
                execution.AmountAmount,
                execution.CurrencyCode
            },
            actorUserId: actorUserId,
            correlationKey: execution.CorrelationKey);
        _dbContext.SaveChanges();

        return GetExecution(execution.Id)!;
    }

    private string BuildExecutionCorrelationKey(string sourceDocumentType, long sourceDocumentId, string baseCorrelationKey)
    {
        var normalizedBaseCorrelationKey = baseCorrelationKey.Trim();
        var existingExecutionCount = _dbContext.PaymentGatewayExecutions
            .AsNoTracking()
            .Count(entry =>
                entry.SourceDocumentType == sourceDocumentType
                && entry.SourceDocumentId == sourceDocumentId);

        if (existingExecutionCount == 0)
        {
            return normalizedBaseCorrelationKey;
        }

        var retrySuffix = $"-R{existingExecutionCount + 1}";
        const int maxCorrelationKeyLength = 120;
        if (normalizedBaseCorrelationKey.Length + retrySuffix.Length <= maxCorrelationKeyLength)
        {
            return normalizedBaseCorrelationKey + retrySuffix;
        }

        return normalizedBaseCorrelationKey[..(maxCorrelationKeyLength - retrySuffix.Length)] + retrySuffix;
    }

    private PaymentChannelConfiguration ResolveCollectionPaymentChannel(CustomerPaymentRequest request)
    {
        var channel = _dbContext.PaymentChannelConfigurations
            .AsNoTracking()
            .SingleOrDefault(entry => entry.Id == request.PaymentChannelConfigurationId)
            ?? throw new InvalidOperationException("Customer payment request payment channel was not found.");

        if (!channel.IsActive)
        {
            throw new InvalidOperationException("Customer payment request payment channel must be active for provider execution.");
        }

        if (!string.Equals(channel.UsageCode, "CUSTOMER_COLLECTION", StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException("Payment provider execution requires a customer collection payment channel.");
        }

        if (!string.Equals(channel.CurrencyCode, request.CurrencyCode, StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException("Customer payment request payment channel currency does not match the request currency.");
        }

        ResolveActiveOutboundEndpoint(channel);
        return channel;
    }

    private PaymentChannelConfiguration ResolveRefundPaymentChannel(CustomerRefund refund)
    {
        if (!refund.PaymentChannelConfigurationId.HasValue)
        {
            throw new InvalidOperationException("Customer refund is not routed through a payment channel.");
        }

        var channel = _dbContext.PaymentChannelConfigurations
            .AsNoTracking()
            .SingleOrDefault(entry => entry.Id == refund.PaymentChannelConfigurationId.Value)
            ?? throw new InvalidOperationException("Customer refund payment channel was not found.");

        if (!channel.IsActive)
        {
            throw new InvalidOperationException("Customer refund payment channel must be active for provider execution.");
        }

        if (!string.Equals(channel.UsageCode, "CUSTOMER_REFUND", StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException("Payment provider execution currently supports customer refund channels only.");
        }

        if (!string.Equals(channel.CurrencyCode, refund.CurrencyCode, StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException("Customer refund payment channel currency does not match the refund currency.");
        }

        ResolveActiveOutboundEndpoint(channel);
        return channel;
    }

    private PaymentChannelConfiguration ResolveSupplierPayoutChannel(string? integrationProviderCode, string currencyCode)
    {
        var normalizedProviderCode = PaymentChannelCatalog.NormalizeIntegrationProviderCode(integrationProviderCode ?? string.Empty);
        var normalizedCurrencyCode = PaymentChannelCatalog.NormalizeCurrencyCode(currencyCode);

        var channel = _dbContext.PaymentChannelConfigurations
            .AsNoTracking()
            .Where(entry =>
                entry.IsActive
                && string.Equals(entry.UsageCode, "SUPPLIER_PAYOUT", StringComparison.OrdinalIgnoreCase)
                && string.Equals(entry.IntegrationProviderCode, normalizedProviderCode, StringComparison.OrdinalIgnoreCase)
                && string.Equals(entry.CurrencyCode, normalizedCurrencyCode, StringComparison.OrdinalIgnoreCase))
            .OrderByDescending(entry => entry.Id)
            .FirstOrDefault()
            ?? throw new InvalidOperationException($"Active supplier payout payment channel was not found for provider '{normalizedProviderCode}' and currency '{normalizedCurrencyCode}'.");

        ResolveActiveOutboundEndpoint(channel);
        return channel;
    }

    private PaymentChannelConfiguration ResolveRefundProviderContractChannel(CustomerRefund refund, bool requireActive = true)
    {
        if (!refund.PaymentChannelConfigurationId.HasValue)
        {
            throw new InvalidOperationException("Customer refund is not routed through a payment channel.");
        }

        var channel = _dbContext.PaymentChannelConfigurations
            .AsNoTracking()
            .SingleOrDefault(entry => entry.Id == refund.PaymentChannelConfigurationId.Value)
            ?? throw new InvalidOperationException("Customer refund payment channel was not found.");

        if (requireActive && !channel.IsActive)
        {
            throw new InvalidOperationException("Customer refund payment channel must be active for provider execution.");
        }

        if (!string.Equals(channel.UsageCode, "CUSTOMER_REFUND", StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException("Payment provider execution currently supports customer refund channels only.");
        }

        if (!string.Equals(channel.CurrencyCode, refund.CurrencyCode, StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException("Customer refund payment channel currency does not match the refund currency.");
        }

        return channel;
    }

    private IntegrationEndpointRegistryEntry ResolveActiveOutboundEndpoint(PaymentChannelConfiguration channel)
    {
        var endpoint = _dbContext.IntegrationEndpointRegistryEntries
            .AsNoTracking()
            .SingleOrDefault(entry => entry.Id == channel.OutboundIntegrationEndpointId)
            ?? throw new InvalidOperationException("Payment channel outbound integration endpoint was not found.");

        if (!endpoint.IsActive)
        {
            throw new InvalidOperationException("Payment provider execution requires an active outbound integration endpoint.");
        }

        if (!string.Equals(endpoint.DirectionCode, "OUTBOUND", StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException("Payment provider execution endpoint must be configured as outbound.");
        }

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

        if (string.IsNullOrWhiteSpace(endpoint.CredentialReference))
        {
            throw new InvalidOperationException("Payment provider execution requires an endpoint credential reference for signing.");
        }

        return endpoint;
    }

    private IntegrationEndpointRegistryEntry ResolveActiveInboundEndpoint(PaymentChannelConfiguration channel)
    {
        var endpointId = channel.InboundIntegrationEndpointId
            ?? throw new InvalidOperationException("Payment channel inbound integration endpoint was not found.");

        var endpoint = _dbContext.IntegrationEndpointRegistryEntries
            .AsNoTracking()
            .SingleOrDefault(entry => entry.Id == endpointId)
            ?? throw new InvalidOperationException("Payment channel inbound integration endpoint was not found.");

        if (!endpoint.IsActive)
        {
            throw new InvalidOperationException("Payment provider callback endpoint must be active.");
        }

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

        return endpoint;
    }

    private static PaymentGatewayProviderContractView BuildProviderContract(
        PaymentChannelConfiguration channel,
        IntegrationEndpointRegistryEntry outboundEndpoint,
        IntegrationEndpointRegistryEntry? inboundEndpoint,
        long paymentChannelConfigurationId,
        string sourceDocumentType,
        string sourceDocumentNumber,
        string referenceNumber,
        string correlationKey,
        decimal amountAmount,
        string currencyCode,
        long? partyId,
        string? providerReference)
    {
        var requestUrl = BuildEndpointUrl(outboundEndpoint);
        var callbackUrl = inboundEndpoint is null
            ? null
            : BuildCallbackUrl(inboundEndpoint, paymentChannelConfigurationId);
        var providerCode = channel.IntegrationProviderCode.Trim().ToUpperInvariant();

        return providerCode switch
        {
            "PAYMOB" => new PaymentGatewayProviderContractView(
                "PAYMOB",
                sourceDocumentType switch
                {
                    "CUSTOMER_REFUND" => "PAYMOB_REFUND",
                    "AP_PAYMENT" => "PAYMOB_PAYOUT_REQUEST",
                    _ => "PAYMOB_CHECKOUT_INTENTION"
                },
                outboundEndpoint.HttpMethodCode,
                requestUrl,
                callbackUrl,
                outboundEndpoint.AuthenticationModeCode,
                correlationKey,
                correlationKey,
                providerReference,
                JsonSerializer.Serialize(new
                {
                    merchantOrderId = sourceDocumentNumber,
                    paymentType = sourceDocumentType,
                    amountCents = decimal.ToInt64(decimal.Round(amountAmount * 100m, 0, MidpointRounding.AwayFromZero)),
                    currency = currencyCode,
                    customerReference = sourceDocumentType == "AP_PAYMENT" ? null : referenceNumber,
                    beneficiaryReference = sourceDocumentType == "AP_PAYMENT" ? referenceNumber : null,
                    callbackUrl,
                    partyId
                })),
            "FAWRY" => new PaymentGatewayProviderContractView(
                "FAWRY",
                sourceDocumentType switch
                {
                    "CUSTOMER_REFUND" => "FAWRY_REFUND_REQUEST",
                    "AP_PAYMENT" => "FAWRY_PAYOUT_REQUEST",
                    _ => "FAWRY_CHARGE_REQUEST"
                },
                outboundEndpoint.HttpMethodCode,
                requestUrl,
                callbackUrl,
                outboundEndpoint.AuthenticationModeCode,
                correlationKey,
                correlationKey,
                providerReference,
                JsonSerializer.Serialize(new
                {
                    merchantRefNum = sourceDocumentNumber,
                    paymentType = sourceDocumentType,
                    amount = amountAmount,
                    currencyCode = currencyCode,
                    customerProfileId = sourceDocumentType == "AP_PAYMENT" ? null : partyId,
                    supplierProfileId = sourceDocumentType == "AP_PAYMENT" ? partyId : null,
                    chargeItemDescription = sourceDocumentType == "AP_PAYMENT" ? null : referenceNumber,
                    payoutDescription = sourceDocumentType == "AP_PAYMENT" ? referenceNumber : null,
                    callbackUrl
                })),
            _ => throw new InvalidOperationException($"Payment provider '{providerCode}' is not yet supported for provider-contract generation.")
        };
    }

    private static PaymentGatewayProviderContractView BuildFawryStatusContract(
        PaymentGatewayExecution execution,
        IntegrationEndpointRegistryEntry outboundEndpoint)
    {
        return new PaymentGatewayProviderContractView(
            "FAWRY",
            "FAWRY_PAYMENT_STATUS_V2",
            "POST",
            BuildOperationUrl(outboundEndpoint.BaseUrl, FawryPaymentStatusRelativePath),
            null,
            outboundEndpoint.AuthenticationModeCode,
            execution.CorrelationKey,
            execution.CorrelationKey,
            execution.ProviderReference,
            JsonSerializer.Serialize(new
            {
                merchantRefNumber = execution.SourceDocumentNumber,
                providerReference = execution.ProviderReference,
                correlationKey = execution.CorrelationKey,
                signatureRecipe = "sha256(merchantCode + merchantRefNumber + secureKey)",
                credentialReference = outboundEndpoint.CredentialReference
            }));
    }

    private static PaymentGatewayProviderContractView BuildFawryCancelContract(
        PaymentGatewayExecution execution,
        IntegrationEndpointRegistryEntry outboundEndpoint)
    {
        if (string.IsNullOrWhiteSpace(execution.ProviderReference))
        {
            throw new InvalidOperationException("Fawry unpaid-order cancellation requires a dispatched execution with a provider reference.");
        }

        if (execution.StatusCode is "SETTLED" or "FAILED")
        {
            throw new InvalidOperationException("Fawry unpaid-order cancellation is only valid while the payment execution remains unresolved.");
        }

        return new PaymentGatewayProviderContractView(
            "FAWRY",
            "FAWRY_CANCEL_UNPAID_ORDER",
            "POST",
            BuildOperationUrl(outboundEndpoint.BaseUrl, FawryCancelUnpaidRelativePath),
            null,
            outboundEndpoint.AuthenticationModeCode,
            execution.CorrelationKey,
            execution.CorrelationKey,
            execution.ProviderReference,
            JsonSerializer.Serialize(new
            {
                orderRefNo = execution.ProviderReference,
                merchantRefNumber = execution.SourceDocumentNumber,
                lang = "en-gb",
                signatureRecipe = "sha256(orderRefNo + merchantAccount + lang + secureKey)",
                credentialReference = outboundEndpoint.CredentialReference
            }));
    }

    private static string BuildEndpointUrl(IntegrationEndpointRegistryEntry endpoint)
    {
        if (string.IsNullOrWhiteSpace(endpoint.RelativePath))
        {
            return endpoint.BaseUrl;
        }

        return $"{endpoint.BaseUrl.TrimEnd('/')}/{endpoint.RelativePath.TrimStart('/')}";
    }

    private static string BuildOperationUrl(string baseUrl, string relativePath) =>
        $"{baseUrl.TrimEnd('/')}/{relativePath.TrimStart('/')}";

    private static string BuildCallbackUrl(IntegrationEndpointRegistryEntry endpoint, long paymentChannelConfigurationId)
    {
        const string callbackRouteTemplate = "/api/v1/payments/channels/{channelId}/callback";

        if (string.IsNullOrWhiteSpace(endpoint.RelativePath))
        {
            return $"{endpoint.BaseUrl.TrimEnd('/')}{callbackRouteTemplate.Replace("{channelId}", paymentChannelConfigurationId.ToString())}";
        }

        var relativePath = endpoint.RelativePath.Trim();
        var normalizedRelativePath = relativePath.TrimStart('/');
        var channelIdText = paymentChannelConfigurationId.ToString();

        if (normalizedRelativePath.Contains("{channelId}", StringComparison.OrdinalIgnoreCase))
        {
            var resolvedPath = normalizedRelativePath.Replace("{channelId}", channelIdText, StringComparison.OrdinalIgnoreCase);
            return $"{endpoint.BaseUrl.TrimEnd('/')}/{resolvedPath}";
        }

        var expectedSuffix = $"api/v1/payments/channels/{channelIdText}/callback";
        if (normalizedRelativePath.EndsWith(expectedSuffix, StringComparison.OrdinalIgnoreCase))
        {
            return $"{endpoint.BaseUrl.TrimEnd('/')}/{normalizedRelativePath}";
        }

        if (normalizedRelativePath.StartsWith("api/v1/payments/channels/", StringComparison.OrdinalIgnoreCase))
        {
            return $"{endpoint.BaseUrl.TrimEnd('/')}/{normalizedRelativePath}";
        }

        return $"{endpoint.BaseUrl.TrimEnd('/')}{callbackRouteTemplate.Replace("{channelId}", channelIdText)}";
    }

    private static PaymentGatewayExecutionView Map(
        PaymentGatewayExecution execution,
        PaymentChannelConfiguration channel,
        IntegrationEndpointRegistryEntry endpoint) =>
        new(
            execution.Id,
            execution.ExecutionNumber,
            execution.PaymentChannelConfigurationId,
            channel.ChannelCode,
            channel.ChannelName,
            execution.IntegrationEndpointId,
            endpoint.EndpointCode,
            channel.IntegrationProviderCode,
            execution.SourceDocumentType,
            execution.SourceDocumentId,
            execution.SourceDocumentNumber,
            execution.PartyId,
            execution.CurrencyCode,
            execution.AmountAmount,
            execution.CorrelationKey,
            execution.StatusCode,
            execution.ProviderReference,
            execution.ProviderStatusCode,
            execution.ProviderResponseText,
            execution.AttemptNumber,
            execution.DispatchedAtUtc,
            execution.DispatchedByProcess,
            execution.CompletedAtUtc,
            execution.LastErrorText,
            execution.CreatedAtUtc);

    private (PaymentGatewayExecution Execution, PaymentChannelConfiguration Channel, IntegrationEndpointRegistryEntry OutboundEndpoint) ResolveExecutionWithOutboundEndpoint(long paymentGatewayExecutionId)
    {
        var execution = _dbContext.PaymentGatewayExecutions
            .AsNoTracking()
            .SingleOrDefault(entry => entry.Id == paymentGatewayExecutionId)
            ?? throw new InvalidOperationException("Payment gateway execution was not found.");

        var channel = _dbContext.PaymentChannelConfigurations
            .AsNoTracking()
            .Single(entry => entry.Id == execution.PaymentChannelConfigurationId);
        var outboundEndpoint = ResolveActiveOutboundEndpoint(channel);
        return (execution, channel, outboundEndpoint);
    }
}
