#nullable enable
using Baya.Application.Contracts.Common;
using Baya.Application.Contracts.Configuration;
using Baya.Application.Contracts.Holidays;
using Baya.Application.Contracts.Payments;
using Baya.Application.Contracts.Persistence;
using Baya.Application.Models.Common;
using Baya.Domain.Entities.Payouts;
using Mediator;

namespace Baya.Application.Features.Payouts.Commands.RetryFailedPayout;

/// <summary>
/// Re-submits one <c>failed</c> payout. Holiday-aware — it refuses on a bank-closed day (PAYA/SATNA would fail).
/// On acceptance it drives the same settlement as the batch execute (ledger post + clawback netting, both
/// idempotent) and re-settles the parent batch (<c>partially_failed → completed</c> when it was the last failure).
/// </summary>
internal sealed class RetryFailedPayoutCommandHandler(
    IUnitOfWork unitOfWork,
    IDistributedLock distributedLock,
    IBankTransferProvider bankTransfer,
    IHolidayCalendar holidays,
    IPlatformConfig platformConfig,
    IDateTimeProvider dateTimeProvider)
    : IRequestHandler<RetryFailedPayoutCommand, OperationResult<bool>>
{
    public async ValueTask<OperationResult<bool>> Handle(RetryFailedPayoutCommand request, CancellationToken cancellationToken)
    {
        var now = dateTimeProvider.UtcNow.UtcDateTime;

        await using var _ = await distributedLock.AcquireAsync("payout:batch", cancellationToken);

        var stub = await unitOfWork.PayoutRepository.GetTrackedPayoutAsync(request.PayoutId, cancellationToken);
        if (stub is null)
            return OperationResult<bool>.NotFoundResult("Payout not found.");

        // Load the full batch (tracked, with all payouts) so the retry can re-settle the batch status; EF identity
        // resolution returns the same tracked payout instance.
        var batch = await unitOfWork.PayoutRepository.GetTrackedBatchAsync(stub.BatchId, cancellationToken);
        var payout = batch!.Payouts.First(p => p.Id == request.PayoutId);

        // Idempotent: an already-paid payout needs no retry.
        if (payout.Status == PayoutStatus.Paid)
            return OperationResult<bool>.SuccessResult(true);
        if (payout.Status != PayoutStatus.Failed)
            return OperationResult<bool>.ConflictResult("Only a failed payout can be retried.");

        // Holiday-aware: a real PAYA/SATNA transfer won't settle on a bank-closed day (or the Friday weekend).
        var today = DateOnly.FromDateTime(now);
        var nextOpen = await holidays.NextBusinessDay(today, cancellationToken);
        if (nextOpen != today)
            return OperationResult<bool>.FailureResult(
                "processing_date", "Banks are closed today; retry on the next business day.");

        var satnaThreshold = (long)await platformConfig.GetConfig<decimal>("payout_satna_threshold_irr", cancellationToken);

        var submit = await bankTransfer.SubmitPayoutBatchAsync(
            batch.Id, [PayoutSettlement.ToInstruction(payout, satnaThreshold)],
            idempotencyKey: $"payout:{payout.Id}:retry", cancellationToken);

        var result = submit.Results.FirstOrDefault(r => r.PayoutId == payout.Id);
        if (result is null || result.Status == BankTransferStatus.Failed)
        {
            payout.MarkFailed(result?.FailureReason ?? "provider_declined");
            await unitOfWork.CommitAsync();
            return OperationResult<bool>.FailureResult("channel", "The bank rail declined the transfer again.");
        }

        payout.MarkSubmitted(result.TransferReference ?? $"payout:{batch.Id}:{payout.Id}");
        if (result.Status == BankTransferStatus.Paid)
        {
            payout.MarkPaid(now);
            await PayoutSettlement.PostPayoutLedgerAsync(unitOfWork, payout, now, cancellationToken);
            await PayoutSettlement.RecoverClawbacksAsync(unitOfWork, payout, now, cancellationToken);
        }

        batch.RecomputeSettlement(now);
        await unitOfWork.CommitAsync();

        return OperationResult<bool>.SuccessResult(true);
    }
}
