#nullable enable
using System.Text.Json;
using Baya.Application.Contracts.Common;
using Baya.Application.Contracts.Configuration;
using Baya.Application.Contracts.Payments;
using Baya.Application.Contracts.Persistence;
using Baya.Application.Features.Bookings;
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.SettleBnplOrder;

/// <summary>
/// The BNPL money capture. Mirrors b10's <c>ConfirmPaymentAndPostLedger</c> — same shared booking conversion,
/// same idempotency shape — but posts the <b>net-of-fee</b> group (via <c>LedgerPosting.BnplSettle</c>) so escrow
/// reflects the real cash received, and verifies through the BNPL provider rather than the card PSP. The nurse's
/// <c>nurse_payable</c> accrual is <b>invariant to payment method</b>: it comes from the booking split, never
/// from <c>settled_amount_irr</c>.
/// </summary>
internal sealed class SettleBnplOrderCommandHandler(
    IUnitOfWork unitOfWork,
    IBnplProviderResolver providerResolver,
    IPlatformConfig platformConfig,
    IVariantSnapshotSerializer variantSnapshotSerializer,
    ISettlementSplitProvider settlementSplitProvider,
    IDistributedLock distributedLock,
    IDateTimeProvider dateTimeProvider,
    INotificationDispatcher notifications,
    ISender sender)
    : IRequestHandler<SettleBnplOrderCommand, OperationResult<bool>>
{
    public async ValueTask<OperationResult<bool>> Handle(SettleBnplOrderCommand request, CancellationToken cancellationToken)
    {
        // Key the lock on the BNPL order (1:1 with the booking) and load state under it, so a racing double
        // settle can't both read 'verified'. The webhook dedup + settle-ledger probe are the DB backstops.
        await using var _ = await distributedLock.AcquireAsync($"bnpl:{request.BnplTransactionId}:settle", cancellationToken);

        var bnpl = await unitOfWork.BnplRepository.GetTrackedByIdAsync(request.BnplTransactionId, cancellationToken);
        if (bnpl is null)
            return OperationResult<bool>.NotFoundResult("BNPL order not found.");

        // Already settled — idempotent no-op (a replayed settle must not re-post the ledger).
        if (bnpl.Status == BnplStatus.Settled)
            return OperationResult<bool>.SuccessResult(true);
        if (bnpl.Status != BnplStatus.Verified || string.IsNullOrEmpty(bnpl.ExternalPaymentToken))
            return OperationResult<bool>.ConflictResult("This BNPL order is not ready to settle.");

        var transaction = await unitOfWork.PaymentRepository.GetTrackedTransactionByIdAsync(bnpl.PaymentTransactionId, cancellationToken);
        if (transaction is null)
            return OperationResult<bool>.NotFoundResult("The BNPL payment transaction is missing.");

        var provider = providerResolver.Resolve(bnpl.ProviderCode);
        if (provider is null)
            return OperationResult<bool>.FailureResult("The BNPL provider is not available.");

        var idempotencyKey = string.IsNullOrWhiteSpace(request.IdempotencyKey)
            ? $"bnpl-settle-{bnpl.Id}"
            : request.IdempotencyKey!;

        var settle = await provider.SettleAsync(bnpl.ExternalPaymentToken!, bnpl.OrderAmountIrr, idempotencyKey, cancellationToken);
        if (settle.Status != PaymentProviderStatus.Succeeded)
        {
            bnpl.MarkFailed();
            await unitOfWork.CommitAsync();
            return OperationResult<bool>.FailureResult("The BNPL provider declined the settlement.");
        }

        // Never trust the settlement blindly — the net + commission must reconcile to the stored order amount.
        if (settle.SettledAmountIrr < 0 || settle.BnplCommissionIrr < 0
            || settle.SettledAmountIrr + settle.BnplCommissionIrr != bnpl.OrderAmountIrr)
            return OperationResult<bool>.FailureResult("The BNPL settlement does not reconcile with the order amount.");

        var now = dateTimeProvider.UtcNow.UtcDateTime;

        // Create/confirm the booking through the shared conversion (same path as the card capture).
        var conversion = await BookingConversion.EnsureBookingAsync(
            unitOfWork, platformConfig, variantSnapshotSerializer, transaction.BookingRequestId, now, cancellationToken);
        if (conversion is null)
            return OperationResult<bool>.ConflictResult("This request can no longer be converted to a booking.");

        transaction.MarkSucceeded(conversion.BookingId, BnplStatus.Settled, transaction.GatewayResponseJson);

        // Idempotent net-of-fee ledger: the card-capture legs PLUS the bnpl_fee_expense leg, under one balanced
        // group, so escrow_held reflects the NET cash. The nurse_payable leg equals the card-path amount.
        if (!await unitOfWork.BnplRepository.SettleLedgerExistsAsync(bnpl.Id, cancellationToken))
        {
            var legs = LedgerPosting.BnplSettle(
                conversion.BookingId, conversion.NurseId, conversion.GrossIrr, conversion.CommissionIrr,
                conversion.PayoutIrr, settle.BnplCommissionIrr, bnpl.Id, now);
            await unitOfWork.PaymentRepository.AddLedgerEntriesAsync(legs, cancellationToken);
        }

        bnpl.MarkSettled(settle.SettledAmountIrr, settle.BnplCommissionIrr, settle.SettledAt, request.CallbackPayloadJson);
        if (settle.ExternalTransactionId is not null)
            transaction.GatewayTransactionId ??= settle.ExternalTransactionId;

        try
        {
            await unitOfWork.CommitAsync();
        }
        catch (DbUpdateException)
        {
            // A concurrent confirm for the same booking hit the filtered UNIQUE(booking_id) WHERE
            // status='succeeded' — the DB backstop. Treat as already-captured.
            await unitOfWork.RollBackAsync();
            return OperationResult<bool>.SuccessResult(true);
        }

        // The lawful تسهیم split — the nurse's payout is invariant to payment method (from the booking split,
        // never from settled_amount). The provider commission is a platform expense, not the nurse's.
        await settlementSplitProvider.RegisterSplitAsync(
            conversion.BookingId,
            [
                new SettlementLeg("nurse-registered-sheba", conversion.PayoutIrr, "nurse"),
                new SettlementLeg("platform-registered-sheba", conversion.CommissionIrr, "platform")
            ],
            cancellationToken);

        if (conversion.Created && conversion.CustomerUserId is { } customerUserId && conversion.NurseUserId is { } nurseUserId)
            await NotifyConfirmedAsync(customerUserId, nurseUserId, conversion.BookingId, cancellationToken);

        // Open the booking-coordination ticket once the booking is confirmed (idempotent, one per booking) — b15.
        if (conversion.Created)
        {
            await sender.Send(new Messaging.Commands.AutoCreateCoordinationTicket.AutoCreateCoordinationTicketCommand(conversion.BookingId), cancellationToken);

            // Auto-issue the commission invoice so the customer can reach it right after settlement (idempotent).
            await sender.Send(new Invoices.Commands.IssueInvoice.IssueInvoiceCommand(conversion.BookingId), cancellationToken);
        }

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

    private async Task NotifyConfirmedAsync(int customerUserId, int nurseUserId, long bookingId, CancellationToken cancellationToken)
    {
        var payload = JsonSerializer.Serialize(new { booking_id = bookingId });

        await notifications.DispatchAsync(
            new Notification(customerUserId, "booking_confirmed", "Booking confirmed",
                "Your installment plan was approved and your booking is confirmed.", payload),
            cancellationToken);

        await notifications.DispatchAsync(
            new Notification(nurseUserId, "booking_confirmed_nurse", "New confirmed booking",
                "A booking has been confirmed and paid. The care instructions and schedule are now available.", payload),
            cancellationToken);
    }
}
