#nullable enable
using Baya.Application.Contracts.Common;
using Baya.Application.Contracts.Configuration;
using Baya.Application.Contracts.Persistence;
using Baya.Domain.Entities.Booking;

namespace Baya.Application.Features.Bookings;

/// <summary>
/// The single place a captured payment turns an <c>accepted_awaiting_payment</c> request into a confirmed
/// <c>bookings</c> row (through <see cref="BookingFactory"/>), shared by <b>both</b> capture rails — b10's card
/// <c>ConfirmPaymentAndPostLedger</c> and b12's BNPL settle — so the conversion/idempotency logic lives once.
/// It re-checks the tracked request against a racing cancel/expiry (a clean conflict, never a double
/// conversion), creates the booking, and commits it; the caller owns the ledger posting for its rail (card
/// capture vs the BNPL net-of-fee group) and any notifications. Pure orchestration — no capture, no
/// current-user gate, no card/BNPL specifics.
/// </summary>
internal static class BookingConversion
{
    /// <summary>Ensures the booking for <paramref name="bookingRequestId"/> exists (creating + committing it if
    /// the request is still convertible) and returns its frozen ledger amounts + participants. Null when the
    /// request can no longer be converted (already-terminal, racing cancel/expiry, or missing) — the caller
    /// maps that to a conflict. Idempotent: a booking created by a prior confirm is loaded, not re-created.</summary>
    public static async Task<BookingConversionResult?> EnsureBookingAsync(
        IUnitOfWork unitOfWork,
        IPlatformConfig platformConfig,
        IVariantSnapshotSerializer variantSnapshotSerializer,
        long bookingRequestId,
        DateTime now,
        CancellationToken cancellationToken)
    {
        var existingId = await unitOfWork.BookingRepository.GetBookingIdByRequestIdAsync(bookingRequestId, cancellationToken);
        if (existingId is { } eb)
        {
            var existingAmounts = await unitOfWork.BookingRepository.GetLedgerAmountsAsync(eb, cancellationToken);
            return existingAmounts is null
                ? null
                : new BookingConversionResult(eb, existingAmounts.NurseId, existingAmounts.GrossIrr, existingAmounts.CommissionIrr, existingAmounts.PayoutIrr, false, null, null);
        }

        var source = await unitOfWork.BookingRequestRepository.GetConversionSourceAsync(bookingRequestId, cancellationToken);
        if (source is null || source.Status != BookingRequestStatus.AcceptedAwaitingPayment)
            return null;

        var rate = await platformConfig.GetConfig<decimal>("platform_fee_rate", cancellationToken);
        var booking = BookingFactory.Create(source, rate, now, pspFeeAmount: null, variantSnapshotSerializer);

        // Re-check the tracked request so a racing cancel/expiry is a clean conflict, not a double conversion.
        var trackedRequest = await unitOfWork.BookingRequestRepository.GetTrackedByIdAsync(source.RequestId, cancellationToken);
        if (trackedRequest is null || !trackedRequest.CanTransitionTo(BookingRequestStatus.Converted))
            return null;

        trackedRequest.MarkConverted();
        await unitOfWork.BookingRepository.AddAsync(booking, cancellationToken);
        await unitOfWork.CommitAsync();

        return new BookingConversionResult(
            booking.Id, booking.NurseId, booking.GrossPriceIrr, booking.BalinyaarCommissionIrr, booking.NursePayoutAmount,
            true, source.CustomerUserId, source.NurseUserId);
    }
}

/// <summary>The confirmed booking's identity, frozen three-amount split, whether this call created it, and (only
/// on create) the participants to notify. Money is IRR <c>long</c>.</summary>
internal sealed record BookingConversionResult(
    long BookingId,
    long NurseId,
    long GrossIrr,
    long CommissionIrr,
    long PayoutIrr,
    bool Created,
    int? CustomerUserId,
    int? NurseUserId);
