#nullable enable
using System;
using System.Collections.Generic;
using Baya.Application.Contracts.Common;
using Baya.Application.Contracts.Payments;
using Baya.Application.Contracts.Persistence;
using Baya.Application.Features.Bnpl.Commands.RevertBnplOrder;
using Baya.Application.Features.Bnpl.Commands.SettleBnplOrder;
using Baya.Application.Features.Bnpl.Commands.VerifyBnplOrder;
using Baya.Application.Features.Refunds.Commands.ConfirmRefundSettlement;
using Baya.Application.Models.Bnpl;
using Baya.Application.Models.Common;
using Baya.Domain.Entities.Bnpl;
using Baya.Domain.Entities.Payments;
using Mediator;
using Microsoft.EntityFrameworkCore;

namespace Baya.Application.Features.Bnpl.Commands.HandleBnplCallback;

internal sealed class HandleBnplCallbackCommandHandler(
    ISender sender,
    IUnitOfWork unitOfWork,
    IWebhookVerifier webhookVerifier,
    IDateTimeProvider dateTimeProvider)
    : IRequestHandler<HandleBnplCallbackCommand, OperationResult<BnplCallbackResult>>
{
    private enum CallbackAction { None, Verify, Settle, Revert, RefundConfirmed }

    public async ValueTask<OperationResult<BnplCallbackResult>> Handle(HandleBnplCallbackCommand 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);
        }

        var action = ResolveAction(verification.EventType);
        if (action == CallbackAction.None || string.IsNullOrEmpty(verification.GatewayReferenceCode))
        {
            // Nothing to drive (unknown event / no token) — acknowledged, no money moves.
            webhookEvent.MarkProcessed(null, now);
            return await PersistNewEventAsync(webhookEvent, cancellationToken);
        }

        var bnpl = await unitOfWork.BnplRepository.GetTrackedByTokenAsync(verification.GatewayReferenceCode!, cancellationToken);
        if (bnpl is null)
        {
            // No matching order for the token — retryable (failed), never a silent success.
            webhookEvent.MarkFailed(now);
            return await PersistNewEventAsync(webhookEvent, cancellationToken);
        }

        // Claim the idempotency key first (inside the same context that mutates 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);
        }

        var dispatched = await DispatchAsync(action, bnpl, verification.ExternalEventId, rawBody, cancellationToken);

        if (dispatched)
            webhookEvent.MarkProcessed(bnpl.PaymentTransactionId, now);
        else
            webhookEvent.MarkFailed(now);

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

    private async Task<bool> DispatchAsync(CallbackAction action, BnplTransaction bnpl, string eventId, string rawBody, CancellationToken cancellationToken)
    {
        switch (action)
        {
            case CallbackAction.Verify:
                return (await sender.Send(new VerifyBnplOrderCommand(bnpl.Id, rawBody), cancellationToken)).IsSuccess;
            case CallbackAction.Settle:
                return (await sender.Send(new SettleBnplOrderCommand(bnpl.Id, $"bnpl-settle-{bnpl.Id}-{eventId}", rawBody), cancellationToken)).IsSuccess;
            case CallbackAction.Revert:
                return (await sender.Send(new RevertBnplOrderCommand(bnpl.Id, RefundPercentage: 1m, TicketId: null, ReasonNotes: "provider_callback", rawBody), cancellationToken)).IsSuccess;
            case CallbackAction.RefundConfirmed:
            {
                // The provider confirmed the customer cash-back for an earlier revert. Resolve the processing
                // refund created against this order's payment_transaction and settle it (posts the deferred
                // refund_payable ↔ escrow_held clearing). No in-flight refund → retryable (not a silent success).
                var refundId = await unitOfWork.RefundRepository.GetProcessingRefundIdForTransactionAsync(bnpl.PaymentTransactionId, cancellationToken);
                if (refundId is null)
                    return false;
                return (await sender.Send(new ConfirmRefundSettlementCommand(refundId.Value), cancellationToken)).IsSuccess;
            }
            default:
                return false;
        }
    }

    private static CallbackAction ResolveAction(string eventType)
    {
        // A "revert/refund confirmed/completed/settled" (or "cashback") event closes an EARLIER revert's async
        // customer cash-back — distinct from the order-level "settle". Checked first so "revert_settled" doesn't
        // fall through to the order-settle branch below.
        var isRevertScoped = eventType.Contains("revert", StringComparison.OrdinalIgnoreCase)
                             || eventType.Contains("refund", StringComparison.OrdinalIgnoreCase);
        if (eventType.Contains("cashback", StringComparison.OrdinalIgnoreCase)
            || (isRevertScoped && (eventType.Contains("complet", StringComparison.OrdinalIgnoreCase)
                                   || eventType.Contains("confirm", StringComparison.OrdinalIgnoreCase)
                                   || eventType.Contains("settl", StringComparison.OrdinalIgnoreCase))))
            return CallbackAction.RefundConfirmed;
        if (eventType.Contains("settl", StringComparison.OrdinalIgnoreCase))
            return CallbackAction.Settle;
        if (eventType.Contains("verif", StringComparison.OrdinalIgnoreCase))
            return CallbackAction.Verify;
        if (isRevertScoped)
            return CallbackAction.Revert;
        return CallbackAction.None;
    }

    private async Task<OperationResult<BnplCallbackResult>> PersistNewEventAsync(PaymentWebhookEvent webhookEvent, CancellationToken cancellationToken)
    {
        await unitOfWork.PaymentRepository.AddWebhookEventAsync(webhookEvent, cancellationToken);
        try
        {
            await unitOfWork.CommitAsync();
        }
        catch (DbUpdateException)
        {
            await unitOfWork.RollBackAsync();
            return Success(webhookEvent.ProcessingStatus, isDuplicate: true);
        }

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

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