#nullable enable
using System.Text.Json;
using Baya.Application.Contracts.Common;
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.Booking.Commands.ExpireBookingRequests;

internal sealed class ExpireBookingRequestsCommandHandler(
    IUnitOfWork unitOfWork,
    IDateTimeProvider dateTimeProvider,
    INotificationDispatcher notifications)
    : IRequestHandler<ExpireBookingRequestsCommand, OperationResult<ExpireBookingRequestsResult>>
{
    private const int BatchSize = 100;

    public async ValueTask<OperationResult<ExpireBookingRequestsResult>> Handle(ExpireBookingRequestsCommand request, CancellationToken cancellationToken)
    {
        var expiredNoResponse = await SweepAsync(
            (now, ct) => unitOfWork.BookingRequestRepository.GetStalePendingAsync(now, BatchSize, ct),
            BookingRequestStatus.ExpiredNoResponse,
            r => r.ExpireNoResponse(),
            "booking_request_expired_no_response",
            "Booking request expired",
            "No nurse responded to your request in time. Please try another nurse.",
            cancellationToken);

        var paymentExpired = await SweepAsync(
            (now, ct) => unitOfWork.BookingRequestRepository.GetStaleAcceptedAsync(now, BatchSize, ct),
            BookingRequestStatus.PaymentDeadlineExpired,
            r => r.ExpirePaymentWindow(),
            "booking_request_payment_window_expired",
            "Payment window expired",
            "The payment window for your accepted request lapsed. Please request again to book.",
            cancellationToken);

        return OperationResult<ExpireBookingRequestsResult>.SuccessResult(
            new ExpireBookingRequestsResult(expiredNoResponse, paymentExpired));
    }

    private async Task<int> SweepAsync(
        Func<DateTime, CancellationToken, Task<IReadOnlyList<BookingRequest>>> loadStale,
        string targetStatus,
        Action<BookingRequest> transition,
        string notificationType,
        string title,
        string body,
        CancellationToken cancellationToken)
    {
        var total = 0;

        while (!cancellationToken.IsCancellationRequested)
        {
            var now = dateTimeProvider.UtcNow.UtcDateTime;
            var batch = await loadStale(now, cancellationToken);
            if (batch.Count == 0)
                break;

            var moved = new List<BookingRequest>(batch.Count);
            foreach (var row in batch)
            {
                // Re-entrant guard: a row a racing accept/cancel already moved out of the expected state is
                // skipped rather than clobbered.
                if (!row.CanTransitionTo(targetStatus))
                    continue;

                transition(row);
                moved.Add(row);
            }

            if (moved.Count == 0)
                break;

            await unitOfWork.CommitAsync();

            foreach (var row in moved)
                await notifications.DispatchAsync(
                    new Notification(
                        row.Customer.UserId,
                        notificationType,
                        title,
                        body,
                        JsonSerializer.Serialize(new { booking_request_id = row.Id })),
                    cancellationToken);

            total += moved.Count;

            // A short page means the stale set is drained; the next page would be empty.
            if (batch.Count < BatchSize)
                break;
        }

        return total;
    }
}
