#nullable enable
using System.Text.Json;
using Baya.Application.Contracts.Common;
using Baya.Application.Contracts.Payments;
using Baya.Application.Contracts.Persistence;
using Baya.Application.Models.Common;
using Baya.Application.Models.Refunds;
using Baya.Domain.Entities.Payments;
using Baya.Domain.Entities.Refunds;
using Mediator;

namespace Baya.Application.Features.Refunds.Commands.ConfirmRefundSettlement;

/// <summary>
/// Closes the money loop left open by a BNPL/manual refund. A card refund clears its
/// <c>refund_payable ↔ escrow_held</c> leg immediately at create time; a BNPL/manual refund sits in
/// <c>processing</c> until the provider/bank confirms the customer actually got the cash — at which point this
/// posts the deferred clearing so the ledger reconciles with the bank. Runs under the same
/// <c>booking:{id}:refund</c> lock as <c>CreateRefundCommand</c>, and re-reads the refund row <b>inside</b> the
/// lock so a racing/replayed confirm sees the committed (already-succeeded) state and no-ops instead of
/// double-clearing.
/// </summary>
internal sealed class ConfirmRefundSettlementCommandHandler(
    IUnitOfWork unitOfWork,
    IDistributedLock distributedLock,
    IDateTimeProvider dateTimeProvider,
    INotificationDispatcher notifications)
    : IRequestHandler<ConfirmRefundSettlementCommand, OperationResult<RefundSettlementResult>>
{
    public async ValueTask<OperationResult<RefundSettlementResult>> Handle(ConfirmRefundSettlementCommand request, CancellationToken cancellationToken)
    {
        // A no-tracking read first to key the lock (and carry the customer's user id for the notification).
        var projection = await unitOfWork.RefundRepository.GetStatusAsync(request.RefundId, cancellationToken);
        if (projection is null)
            return OperationResult<RefundSettlementResult>.NotFoundResult("Refund not found.");

        await using var _ = await distributedLock.AcquireAsync($"booking:{projection.Refund.BookingId}:refund", cancellationToken);

        // Load the tracked row INSIDE the lock so its status reflects committed truth even under contention.
        var refund = await unitOfWork.RefundRepository.GetTrackedRefundByIdAsync(request.RefundId, cancellationToken);
        if (refund is null)
            return OperationResult<RefundSettlementResult>.NotFoundResult("Refund not found.");

        // Idempotent: an already-succeeded refund (a replayed callback, or a card refund that cleared at create
        // time) is a no-op success — the clearing must never post twice.
        if (refund.Status == RefundStatus.Succeeded)
            return Result(refund);

        if (refund.Status != RefundStatus.Processing)
            return OperationResult<RefundSettlementResult>.ConflictResult(
                $"Only a processing refund can be settled (was {refund.Status}).");

        var now = dateTimeProvider.UtcNow.UtcDateTime;
        refund.MarkSucceededReconciled(now);

        // The customer cash-back is confirmed → clear refund_payable ↔ escrow_held for the refunded total, in the
        // same commit as the status flip so the ledger can never observe a settled refund with an unposted clearing.
        var clearing = LedgerPosting.RefundPayableClearing(refund.BookingId, refund.Amount, refund.Id, now);
        await unitOfWork.PaymentRepository.AddLedgerEntriesAsync(clearing, cancellationToken);
        await unitOfWork.CommitAsync();

        await NotifyCustomerAsync(projection.CustomerUserId, refund, cancellationToken);

        return Result(refund);
    }

    private async Task NotifyCustomerAsync(int customerUserId, Refund refund, CancellationToken cancellationToken)
    {
        var payload = JsonSerializer.Serialize(new { booking_id = refund.BookingId, refund_id = refund.Id, refund.Status });
        await notifications.DispatchAsync(
            new Notification(customerUserId, "refund_completed", "Refund completed",
                "Your refund has been completed and the funds are on their way to you.", payload),
            cancellationToken);
    }

    private static OperationResult<RefundSettlementResult> Result(Refund refund)
        => OperationResult<RefundSettlementResult>.SuccessResult(
            new RefundSettlementResult(refund.Id, refund.BookingId, refund.Status, refund.ProcessedAt));
}
