#nullable enable
using System.Text.Json;
using Baya.Application.Contracts.Common;
using Baya.Application.Contracts.Configuration;
using Baya.Application.Contracts.Persistence;
using Baya.Application.Models.Booking;
using Baya.Application.Models.Common;
using Baya.Domain.Entities.Booking;
using Mediator;

namespace Baya.Application.Features.Bookings.Commands.ConvertRequestToBooking;

internal sealed class ConvertRequestToBookingCommandHandler(
    ICurrentUser currentUser,
    IUnitOfWork unitOfWork,
    IPlatformConfig platformConfig,
    IDateTimeProvider dateTimeProvider,
    IPaymentCaptureSimulator paymentCapture,
    IVariantSnapshotSerializer variantSnapshotSerializer,
    INotificationDispatcher notifications)
    : IRequestHandler<ConvertRequestToBookingCommand, OperationResult<BookingDetailDto>>
{
    public async ValueTask<OperationResult<BookingDetailDto>> Handle(ConvertRequestToBookingCommand request, CancellationToken cancellationToken)
    {
        if (currentUser.UserId is not { } userId)
            return OperationResult<BookingDetailDto>.UnauthorizedResult("Not authenticated.");

        var source = await unitOfWork.BookingRequestRepository.GetConversionSourceAsync(request.BookingRequestId, cancellationToken);
        if (source is null)
            return OperationResult<BookingDetailDto>.NotFoundResult("Booking request not found.");

        // The payer (owning customer) or an admin may trigger the (mock) capture. Any other caller must not
        // even learn the request exists.
        var isAdmin = currentUser.Roles?.Any(BookingRoles.Admin.Contains) == true;
        var callerCustomerId = await unitOfWork.CustomerProfileRepository.GetProfileIdByUserIdAsync(userId, cancellationToken);
        if (!isAdmin && callerCustomerId != source.CustomerId)
            return OperationResult<BookingDetailDto>.NotFoundResult("Booking request not found.");

        // Idempotency: the UNIQUE booking_request_id means a replay can't create a second booking — return
        // the one already created.
        var existingId = await unitOfWork.BookingRepository.GetBookingIdByRequestIdAsync(source.RequestId, cancellationToken);
        if (existingId is { } existing)
        {
            var existingDetail = await unitOfWork.BookingRepository.GetDetailAsync(existing, cancellationToken);
            return OperationResult<BookingDetailDto>.SuccessResult(BookingMapper.ToDetailDto(existingDetail!, includeAddress: true));
        }

        if (source.Status != BookingRequestStatus.AcceptedAwaitingPayment)
            return OperationResult<BookingDetailDto>.ConflictResult("This request is not awaiting payment and cannot be converted.");

        // A booking exists ONLY on a successful capture — a failed capture creates nothing.
        var capture = await paymentCapture.ConfirmCaptureAsync(source.RequestId, cancellationToken);
        if (!capture.Succeeded)
            return OperationResult<BookingDetailDto>.FailureResult("Payment capture failed; no booking was created.");

        var now = dateTimeProvider.UtcNow.UtcDateTime;

        var rate = await platformConfig.GetConfig<decimal>("platform_fee_rate", cancellationToken);

        // The conversion/amount logic is shared with b10's real card capture — build through BookingFactory.
        var booking = BookingFactory.Create(source, rate, now, capture.PspFeeAmount, variantSnapshotSerializer);

        // Flip the request → converted in the same unit of work. Re-check the tracked state so a racing
        // cancel/expiry that already moved it 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 OperationResult<BookingDetailDto>.ConflictResult("This request can no longer be converted.");

        trackedRequest.MarkConverted();

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

        await notifications.DispatchAsync(
            new Notification(
                source.CustomerUserId,
                "booking_confirmed",
                "Booking confirmed",
                "Your payment was captured and your booking is confirmed.",
                JsonSerializer.Serialize(new { booking_id = booking.Id })),
            cancellationToken);

        await notifications.DispatchAsync(
            new Notification(
                source.NurseUserId,
                "booking_confirmed_nurse",
                "New confirmed booking",
                "A booking has been confirmed and paid. The care instructions and schedule are now available.",
                JsonSerializer.Serialize(new { booking_id = booking.Id })),
            cancellationToken);

        var detail = await unitOfWork.BookingRepository.GetDetailAsync(booking.Id, cancellationToken);
        return OperationResult<BookingDetailDto>.SuccessResult(BookingMapper.ToDetailDto(detail!, includeAddress: true));
    }
}
