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

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

/// <summary>
/// The one irreversible money-out step. Under <c>lock(payout:batch)</c> it submits the batch's unpaid payouts to
/// the <see cref="IBankTransferProvider"/> (PAYA/SATNA by the config threshold), then for each accepted transfer
/// posts the balanced payout ledger group (<c>DEBIT nurse_payable / CREDIT escrow_held</c>) via b10's helper and
/// nets recovered clawbacks (<c>DEBIT nurse_payable / CREDIT nurse_clawback_receivable</c> + marks the row
/// <c>recovered</c>). The forward-only payout status machine + the ledger-exists guard + the batch idempotency key
/// make a retried execute never double-send a transfer or double-post the ledger.
/// </summary>
internal sealed class ExecutePayoutBatchCommandHandler(
    IUnitOfWork unitOfWork,
    IDistributedLock distributedLock,
    IBankTransferProvider bankTransfer,
    IPlatformConfig platformConfig,
    IDateTimeProvider dateTimeProvider)
    : IRequestHandler<ExecutePayoutBatchCommand, OperationResult<ExecutePayoutBatchResult>>
{
    public async ValueTask<OperationResult<ExecutePayoutBatchResult>> Handle(
        ExecutePayoutBatchCommand request, CancellationToken cancellationToken)
    {
        var now = dateTimeProvider.UtcNow.UtcDateTime;

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

        var batch = await unitOfWork.PayoutRepository.GetTrackedBatchAsync(request.BatchId, cancellationToken);
        if (batch is null)
            return OperationResult<ExecutePayoutBatchResult>.NotFoundResult("Payout batch not found.");

        // Idempotent: a fully-settled batch has nothing left to submit.
        if (batch.Status == PayoutBatchStatus.Completed)
            return OperationResult<ExecutePayoutBatchResult>.SuccessResult(Summarize(batch));

        if (batch.Status == PayoutBatchStatus.Failed)
            return OperationResult<ExecutePayoutBatchResult>.ConflictResult("This batch has already failed; open a new batch.");

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

        if (batch.Status == PayoutBatchStatus.Draft)
            batch.TransitionTo(PayoutBatchStatus.Processing, now);

        // Only unpaid payouts are (re)submitted — an already-paid row is skipped by the status machine, never
        // re-sent. A zero-net payout (fully netted against clawback) has no transfer but still realizes recovery.
        var unpaid = batch.Payouts.Where(p => p.Status != PayoutStatus.Paid).ToList();
        var transferable = unpaid.Where(p => p.Amount > 0).ToList();

        var resultsByPayout = new Dictionary<long, PayoutInstructionResult>();
        if (transferable.Count > 0)
        {
            var instructions = transferable
                .Select(p => PayoutSettlement.ToInstruction(p, satnaThreshold))
                .ToList();

            var submit = await bankTransfer.SubmitPayoutBatchAsync(
                batch.Id, instructions, idempotencyKey: $"payout-batch:{batch.Id}", cancellationToken);
            resultsByPayout = submit.Results.ToDictionary(r => r.PayoutId);
        }

        foreach (var payout in unpaid)
        {
            if (payout.Amount > 0)
            {
                var result = resultsByPayout.GetValueOrDefault(payout.Id);
                if (result is null || result.Status == BankTransferStatus.Failed)
                {
                    payout.MarkFailed(result?.FailureReason ?? "provider_declined");
                    continue;
                }

                payout.MarkSubmitted(result.TransferReference ?? $"payout:{batch.Id}:{payout.Id}");
                if (result.Status == BankTransferStatus.Paid)
                    payout.MarkPaid(now);
            }
            else
            {
                // Fully netted — no cash leaves, but the withheld earnings realize the clawback recovery now.
                payout.MarkSubmitted($"netted:{batch.Id}:{payout.Id}");
                payout.MarkPaid(now);
            }

            // Only a settled (paid) payout posts the ledger + nets clawbacks — a still-submitted row waits for the
            // reconciliation callback (the real rail), a failed one does neither.
            if (payout.Status == PayoutStatus.Paid)
            {
                await PayoutSettlement.PostPayoutLedgerAsync(unitOfWork, payout, now, cancellationToken);
                await PayoutSettlement.RecoverClawbacksAsync(unitOfWork, payout, now, cancellationToken);
            }
        }

        batch.RecomputeSettlement(now);

        await unitOfWork.CommitAsync();

        return OperationResult<ExecutePayoutBatchResult>.SuccessResult(Summarize(batch));
    }

    private static ExecutePayoutBatchResult Summarize(NursePayoutBatch batch)
    {
        var paid = batch.Payouts.Where(p => p.Status == PayoutStatus.Paid).ToList();
        var failed = batch.Payouts.Count(p => p.Status == PayoutStatus.Failed);
        var totalPaid = paid.Sum(p => p.Amount);
        return new ExecutePayoutBatchResult(batch.Id, batch.Status, paid.Count, failed, totalPaid.ToString());
    }
}
