#nullable enable
using System.Collections.Generic;
using Baya.Application.Contracts.Common;
using Baya.Application.Contracts.Payments;
using Baya.Application.Contracts.Persistence;
using Baya.Application.Features.Payments.Commands.ConfirmPaymentAndPostLedger;
using Baya.Application.Models.Common;
using Baya.Application.Models.Payments;
using Baya.Domain.Entities.Payments;
using Mediator;
using Microsoft.EntityFrameworkCore;

namespace Baya.Application.Features.Payments.Commands.HandlePaymentWebhook;

internal sealed class HandlePaymentWebhookCommandHandler(
    ISender sender,
    IUnitOfWork unitOfWork,
    IWebhookVerifier webhookVerifier,
    IDistributedLock distributedLock,
    IDateTimeProvider dateTimeProvider)
    : IRequestHandler<HandlePaymentWebhookCommand, OperationResult<WebhookIngestResult>>
{
    public async ValueTask<OperationResult<WebhookIngestResult>> Handle(HandlePaymentWebhookCommand request, CancellationToken cancellationToken)
    {
        var headers = request.Headers ?? new Dictionary<string, string>();
        var rawBody = request.RawBody ?? string.Empty;
        var verification = webhookVerifier.Verify(request.Provider, headers, rawBody);
        var now = dateTimeProvider.UtcNow.UtcDateTime;

        // Dedup FIRST on the idempotency key: a duplicate replay never mutates money state again.
        if (!string.IsNullOrEmpty(verification.ExternalEventId))
        {
            var duplicate = await unitOfWork.PaymentRepository.GetWebhookEventByKeyAsync(request.Provider, verification.ExternalEventId, cancellationToken);
            if (duplicate is not null)
                return Success(duplicate.ProcessingStatus, isDuplicate: true);
        }

        var webhookEvent = new PaymentWebhookEvent
        {
            ProviderCode = request.Provider,
            ExternalEventId = verification.ExternalEventId,
            EventType = verification.EventType,
            SignatureValid = verification.SignatureValid,
            PayloadJson = rawBody,
            ReceivedAt = now
        };

        // An unverified-signature callback mutates nothing — stored ignored and stopped.
        if (!verification.SignatureValid)
        {
            webhookEvent.MarkIgnored(now);
            return await PersistNewEventAsync(webhookEvent, cancellationToken);
        }

        // A non-success event (or one with no reference) has nothing to confirm — acknowledged, no money moves.
        if (!verification.IsSuccessEvent || string.IsNullOrEmpty(verification.GatewayReferenceCode))
        {
            webhookEvent.MarkProcessed(null, now);
            return await PersistNewEventAsync(webhookEvent, cancellationToken);
        }

        var transaction = await unitOfWork.PaymentRepository.GetTrackedTransactionByReferenceAsync(verification.GatewayReferenceCode!, cancellationToken);
        if (transaction is null)
        {
            // No matching pending attempt — retryable (failed), never a silent success.
            webhookEvent.MarkFailed(now);
            return await PersistNewEventAsync(webhookEvent, cancellationToken);
        }

        // The whole money mutation runs under the lock; the DB uniques remain the authoritative backstop if
        // the lock is lost/expired. Keyed on the request (a b9 booking exists only after this confirm).
        await using var _ = await distributedLock.AcquireAsync($"booking-request:{transaction.BookingRequestId}:payment", cancellationToken);

        // Claim the idempotency key first (inside the same context that mutates payment state). A racing
        // duplicate insert loses on the unique index and is treated as a no-op replay.
        await unitOfWork.PaymentRepository.AddWebhookEventAsync(webhookEvent, cancellationToken);
        try
        {
            await unitOfWork.CommitAsync();
        }
        catch (DbUpdateException)
        {
            await unitOfWork.RollBackAsync();
            return Success(WebhookProcessingStatus.Processed, isDuplicate: true);
        }

        // Re-verify server-side + capture + post the balanced ledger group + confirm the booking.
        var confirm = await sender.Send(new ConfirmPaymentAndPostLedgerCommand(transaction.Id), cancellationToken);

        if (confirm.IsSuccess)
            webhookEvent.MarkProcessed(transaction.Id, now);
        else
            webhookEvent.MarkFailed(now);

        await unitOfWork.CommitAsync();
        return Success(webhookEvent.ProcessingStatus, isDuplicate: false);
    }

    private async Task<OperationResult<WebhookIngestResult>> PersistNewEventAsync(PaymentWebhookEvent webhookEvent, CancellationToken cancellationToken)
    {
        await unitOfWork.PaymentRepository.AddWebhookEventAsync(webhookEvent, cancellationToken);
        try
        {
            await unitOfWork.CommitAsync();
        }
        catch (DbUpdateException)
        {
            // A concurrent insert of the same (provider, external_event_id) — the idempotency backstop.
            await unitOfWork.RollBackAsync();
            return Success(webhookEvent.ProcessingStatus, isDuplicate: true);
        }

        return Success(webhookEvent.ProcessingStatus, isDuplicate: false);
    }

    private static OperationResult<WebhookIngestResult> Success(string processingStatus, bool isDuplicate)
        => OperationResult<WebhookIngestResult>.SuccessResult(new WebhookIngestResult(processingStatus, isDuplicate));
}
