#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.Domain.Entities.Payouts;
using Mediator;

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

/// <summary>
/// Verifies the callback signature (an invalid signature mutates <b>nothing</b> — an irreversible-money callback
/// fails closed), parses the per-transfer outcomes, and under <c>lock(payout:batch)</c> flips each matched
/// <c>submitted</c> payout: <c>paid</c> posts the payout ledger + nets clawbacks (reusing <see cref="PayoutSettlement"/>),
/// <c>failed</c> records the reason. Matched by <c>transfer_reference</c> (the bank track id the submit persisted).
/// Idempotent by the forward-only status machine + the ledger-exists guard, so a replayed callback never
/// double-pays or double-posts.
/// </summary>
internal sealed class ReconcilePayoutBatchCommandHandler(
    IUnitOfWork unitOfWork,
    IDistributedLock distributedLock,
    IWebhookVerifier webhookVerifier,
    IDateTimeProvider dateTimeProvider)
    : IRequestHandler<ReconcilePayoutBatchCommand, OperationResult<bool>>
{
    public async ValueTask<OperationResult<bool>> Handle(ReconcilePayoutBatchCommand request, CancellationToken cancellationToken)
    {
        var verification = webhookVerifier.Verify(request.Provider, request.Headers, request.RawBody);
        if (!verification.SignatureValid)
            return OperationResult<bool>.FailureResult("signature", "Invalid payout callback signature; nothing was reconciled.");

        if (!TryParse(request.RawBody, out var batchId, out var outcomes))
            return OperationResult<bool>.FailureResult("body", "Malformed payout reconciliation callback.");

        var now = dateTimeProvider.UtcNow.UtcDateTime;

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

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

        foreach (var outcome in outcomes)
        {
            var payout = batch.Payouts.FirstOrDefault(p =>
                p.TransferReference is not null &&
                string.Equals(p.TransferReference, outcome.TransferReference, StringComparison.Ordinal));

            // Only a still-submitted payout is actionable; an already-paid/failed row (replay) is skipped.
            if (payout is null || payout.Status != PayoutStatus.Submitted)
                continue;

            if (outcome.Paid)
            {
                payout.MarkPaid(now);
                await PayoutSettlement.PostPayoutLedgerAsync(unitOfWork, payout, now, cancellationToken);
                await PayoutSettlement.RecoverClawbacksAsync(unitOfWork, payout, now, cancellationToken);
            }
            else
            {
                payout.MarkFailed(outcome.FailureReason ?? "provider_declined");
            }
        }

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

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

    private static bool TryParse(string rawBody, out long batchId, out List<TransferOutcome> outcomes)
    {
        batchId = 0;
        outcomes = [];
        try
        {
            using var doc = JsonDocument.Parse(rawBody);
            var root = doc.RootElement;
            if (!root.TryGetProperty("batch_id", out var batchEl) || !batchEl.TryGetInt64(out batchId))
                return false;

            if (root.TryGetProperty("transfers", out var transfers) && transfers.ValueKind == JsonValueKind.Array)
            {
                foreach (var t in transfers.EnumerateArray())
                {
                    var reference = t.TryGetProperty("transfer_reference", out var r) ? r.GetString() : null;
                    if (string.IsNullOrEmpty(reference))
                        continue;

                    var status = t.TryGetProperty("status", out var s) ? s.GetString() : null;
                    var reason = t.TryGetProperty("failure_reason", out var fr) ? fr.GetString() : null;
                    outcomes.Add(new TransferOutcome(reference, string.Equals(status, "paid", StringComparison.OrdinalIgnoreCase), reason));
                }
            }

            return true;
        }
        catch (JsonException)
        {
            return false;
        }
    }

    private readonly record struct TransferOutcome(string TransferReference, bool Paid, string? FailureReason);
}
