#nullable enable
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.Bookings.Commands.CancelBooking;

internal sealed class CancelBookingCommandHandler(
    ICurrentUser currentUser,
    IUnitOfWork unitOfWork,
    IDateTimeProvider dateTimeProvider)
    : IRequestHandler<CancelBookingCommand, OperationResult<CancellationResultDto>>
{
    public async ValueTask<OperationResult<CancellationResultDto>> Handle(CancelBookingCommand request, CancellationToken cancellationToken)
    {
        if (currentUser.UserId is not { } userId)
            return OperationResult<CancellationResultDto>.UnauthorizedResult("Not authenticated.");

        var booking = await unitOfWork.BookingRepository.GetTrackedWithSessionsAsync(request.BookingId, cancellationToken);
        if (booking is null)
            return OperationResult<CancellationResultDto>.NotFoundResult("Booking not found.");

        var isAdmin = currentUser.Roles?.Any(BookingRoles.Admin.Contains) == true;
        var customerId = await unitOfWork.CustomerProfileRepository.GetProfileIdByUserIdAsync(userId, cancellationToken);
        var nurseId = await unitOfWork.NurseProfileRepository.GetProfileIdByUserIdAsync(userId, cancellationToken);
        var isOwningCustomer = customerId == booking.CustomerId;
        var isAssignedNurse = nurseId == booking.NurseId;

        if (!isAdmin && !isOwningCustomer && !isAssignedNurse)
            return OperationResult<CancellationResultDto>.NotFoundResult("Booking not found.");

        if (!booking.CanTransitionTo(BookingStatus.Cancelled))
            return OperationResult<CancellationResultDto>.ConflictResult($"A booking in '{booking.Status}' can no longer be cancelled.");

        var now = dateTimeProvider.UtcNow.UtcDateTime;
        var actor = CancellationHelper.ResolveActor(isAdmin, isAssignedNurse && !isOwningCustomer);

        var policies = await unitOfWork.CancellationPolicyRepository.GetActiveForActorAsync(actor, cancellationToken);
        var hoursBefore = CancellationHelper.HoursBeforeStart(booking.ScheduledDate, booking.ScheduledTimeStart, now);
        var policy = CancellationHelper.ResolvePolicy(policies, hoursBefore);
        if (policy is null)
            return OperationResult<CancellationResultDto>.FailureResult("No cancellation policy applies to this booking.");

        // Only un-started (still scheduled) sessions are refundable; started/completed ones are not.
        var grossShares = BookingAmounts.SplitPayout(booking.GrossPriceIrr, booking.SessionCount);
        long refundableBase = 0;
        foreach (var session in booking.Sessions)
        {
            if (session.Status != BookingSessionStatus.Scheduled)
                continue;

            refundableBase += grossShares[session.SessionIndex - 1];
            session.TransitionTo(BookingSessionStatus.Cancelled);
        }

        var refundable = CancellationHelper.Refundable(refundableBase, policy.RefundPercentage);

        booking.TransitionTo(BookingStatus.Cancelled, now, actor, request.Reason);
        booking.RecordCancellationSnapshot(policy.Code, policy.RefundPercentage, refundable);

        await unitOfWork.CommitAsync();

        return OperationResult<CancellationResultDto>.SuccessResult(new CancellationResultDto(
            booking.Id, null, booking.Status, policy.Code, policy.RefundPercentage, refundable.ToString()));
    }
}
