#nullable enable
using System.Globalization;
using Baya.Application.Contracts.Common;
using Baya.Application.Contracts.Persistence;
using Baya.Application.Features.Bookings;
using Baya.Application.Models.Common;
using Baya.Application.Models.Refunds;
using Baya.Domain.Entities.Booking;
using Baya.Domain.Entities.Payments;
using Baya.Domain.Entities.Refunds;
using Baya.Domain.Entities.User;
using Mediator;

namespace Baya.Application.Features.Refunds.Queries.GetCancellationPolicyPreview;

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

        if (currentUser.Roles?.Contains(RoleNames.Customer) != true)
            return OperationResult<CancellationPolicyPreviewDto>.ForbiddenResult("Only a customer can preview their own cancellation.");

        var customerId = await unitOfWork.CustomerProfileRepository.GetProfileIdByUserIdAsync(userId, cancellationToken);
        var booking = await unitOfWork.BookingRepository.GetTrackedWithSessionsAsync(request.BookingId, cancellationToken);
        if (booking is null || customerId is not { } cid || booking.CustomerId != cid)
            return OperationResult<CancellationPolicyPreviewDto>.NotFoundResult("Booking not found.");

        var now = dateTimeProvider.UtcNow.UtcDateTime;
        var cancellable = booking.CanTransitionTo(BookingStatus.Cancelled);

        var grossShares = BookingAmounts.SplitPayout(booking.GrossPriceIrr, booking.SessionCount);
        long refundableBase = 0;
        var sessions = booking.Sessions
            .OrderBy(s => s.SessionIndex)
            .Select(s =>
            {
                var refundable = s.Status == BookingSessionStatus.Scheduled;
                if (refundable)
                    refundableBase += grossShares[s.SessionIndex - 1];
                return new CancellationPreviewSessionDto(
                    s.Id, s.SessionIndex, s.ScheduledDate, refundable, refundable ? "un_started" : s.Status);
            })
            .ToList();

        var policies = await unitOfWork.CancellationPolicyRepository.GetActiveForActorAsync(CancellationActor.Customer, cancellationToken);
        var hoursBefore = CancellationHelper.HoursBeforeStart(booking.ScheduledDate, booking.ScheduledTimeStart, now);
        var policy = CancellationHelper.ResolvePolicy(policies, hoursBefore);

        var refundAmount = policy is null ? 0 : CancellationHelper.Refundable(refundableBase, policy.RefundPercentage);
        var feeAmount = refundableBase - refundAmount;

        // Split the refunded amount across the booking's frozen fee legs pro-rata (never client-derived).
        long platformFeeRefunded = 0;
        if (refundAmount > 0 && booking.GrossPriceIrr > 0)
            platformFeeRefunded = (long)decimal.Round(
                (decimal)refundAmount * booking.BalinyaarCommissionIrr / booking.GrossPriceIrr, MidpointRounding.AwayFromZero);
        var nursePayoutRefunded = refundAmount - platformFeeRefunded;

        var channelContext = await unitOfWork.RefundRepository.GetRefundContextAsync(request.BookingId, cancellationToken);
        var channel = channelContext?.GatewayType == PaymentGatewayType.Bnpl ? RefundChannel.BnplRevert : RefundChannel.PspCard;

        var dto = new CancellationPolicyPreviewDto(
            booking.Id,
            cancellable,
            policy?.Code,
            policy?.RefundPercentage,
            policy is null ? null : 100m - policy.RefundPercentage,
            Str(refundAmount),
            Str(feeAmount),
            Str(refundableBase),
            Str(platformFeeRefunded),
            Str(nursePayoutRefunded),
            CancellationActor.Customer,
            hoursBefore >= 24 ? "at_least_24h" : "less_than_24h",
            channel,
            // The BNPL ~7–10-business-day customer ETA is stamped on the actual refund; the preview leaves it null.
            null,
            sessions);

        return OperationResult<CancellationPolicyPreviewDto>.SuccessResult(dto);
    }

    private static string Str(long value) => value.ToString(CultureInfo.InvariantCulture);
}
