#nullable enable
using Baya.Application.Contracts.Common;
using Baya.Application.Contracts.Configuration;
using Baya.Application.Contracts.Payments;
using Baya.Application.Contracts.Persistence;
using Baya.Application.Models.Bnpl;
using Baya.Application.Models.Common;
using Baya.Domain.Entities.Bnpl;
using Baya.Domain.Entities.Booking;
using Baya.Domain.Entities.Payments;
using Mediator;

namespace Baya.Application.Features.Bnpl.Commands.InitiateBnplOrder;

internal sealed class InitiateBnplOrderCommandHandler(
    ICurrentUser currentUser,
    IUnitOfWork unitOfWork,
    IBnplProviderResolver providerResolver,
    ICurrencyNormalizer currencyNormalizer,
    IPlatformConfig platformConfig,
    IDistributedLock distributedLock,
    IDateTimeProvider dateTimeProvider)
    : IRequestHandler<InitiateBnplOrderCommand, OperationResult<InitiateBnplResult>>
{
    public async ValueTask<OperationResult<InitiateBnplResult>> Handle(InitiateBnplOrderCommand request, CancellationToken cancellationToken)
    {
        if (currentUser.UserId is not { } userId)
            return OperationResult<InitiateBnplResult>.UnauthorizedResult("Not authenticated.");

        var customerId = await unitOfWork.CustomerProfileRepository.GetProfileIdByUserIdAsync(userId, cancellationToken);
        if (customerId is null)
            return OperationResult<InitiateBnplResult>.NotFoundResult("Booking request not found.");

        var ctx = await unitOfWork.BnplRepository.GetOrderContextAsync(request.BookingRequestId, cancellationToken);
        if (ctx is null || ctx.CustomerId != customerId)
            return OperationResult<InitiateBnplResult>.NotFoundResult("Booking request not found.");

        var provider = providerResolver.Resolve(request.ProviderCode);
        if (provider is null)
            return OperationResult<InitiateBnplResult>.FailureResult("provider_code", "The BNPL provider is not available.");

        var gatewayId = await unitOfWork.PaymentRepository.GetActiveGatewayIdAsync(PaymentGatewayType.Bnpl, cancellationToken);
        if (gatewayId is null)
            return OperationResult<InitiateBnplResult>.FailureResult("No active BNPL gateway is configured.");

        // The whole money mutation runs under the lock; the DB uniques/state machine remain the authoritative
        // backstop if the lock is lost. Keyed on the request (a b9 booking exists only after settle).
        await using var _ = await distributedLock.AcquireAsync($"booking-request:{request.BookingRequestId}:payment", cancellationToken);

        if (await unitOfWork.PaymentRepository.HasSucceededTransactionForRequestAsync(request.BookingRequestId, cancellationToken))
            return OperationResult<InitiateBnplResult>.ConflictResult("This booking has already been paid.");
        if (ctx.Status != BookingRequestStatus.AcceptedAwaitingPayment)
            return OperationResult<InitiateBnplResult>.ConflictResult("This request is not awaiting payment.");

        var now = dateTimeProvider.UtcNow.UtcDateTime;
        if (ctx.PaymentDeadlineAt is { } deadline && deadline < now)
            return OperationResult<InitiateBnplResult>.ConflictResult("The payment window for this request has lapsed.");

        // Conversion happens ONLY here, at the provider boundary. The gross is already IRR, so this is a no-op
        // today — but a real Toman-quoting provider is normalized through exactly this seam, never internally.
        var orderAmountIrr = currencyNormalizer.ToIrr(ctx.GrossIrr, "IRR");

        var merchantOfRecord = await platformConfig.GetConfig<string>("bnpl_merchant_of_record", cancellationToken);
        var bnpl = await BnplOrderInitializer.EnsureAsync(
            unitOfWork, ctx, gatewayId.Value, request.ProviderCode, merchantOfRecord ?? "platform",
            orderAmountIrr, "IRR", currentUser.IpAddress, cancellationToken);

        // Only an eligible or already-tokenized order can be (re-)initiated; a settled/reverted/cancelled/failed
        // one cannot. The provider token call is idempotent, so a replay returns the same token + redirect.
        if (bnpl.Status is not (BnplStatus.Eligible or BnplStatus.TokenIssued))
            return OperationResult<InitiateBnplResult>.ConflictResult("This BNPL order can no longer be started.");

        var idempotencyKey = string.IsNullOrWhiteSpace(request.IdempotencyKey)
            ? $"bnpl-br-{request.BookingRequestId}"
            : request.IdempotencyKey!;

        var token = await provider.CreatePaymentTokenAsync(ctx.CustomerMobile, orderAmountIrr, idempotencyKey, cancellationToken);
        if (token.Status != PaymentProviderStatus.Succeeded)
        {
            if (bnpl.Status == BnplStatus.Eligible)
            {
                bnpl.MarkFailed();
                await unitOfWork.CommitAsync();
            }
            return OperationResult<InitiateBnplResult>.FailureResult("The BNPL provider declined the order.");
        }

        // On the first initiate, bind the token to the pending payment_transaction so the callback can find it
        // (and the filtered UNIQUE(gateway_reference_code) guards it), then walk eligible → token_issued.
        if (bnpl.Status == BnplStatus.Eligible)
        {
            var transaction = await unitOfWork.PaymentRepository.GetTrackedTransactionByIdAsync(bnpl.PaymentTransactionId, cancellationToken);
            if (transaction is not null)
                transaction.GatewayReferenceCode = token.ExternalPaymentToken;

            bnpl.MarkTokenIssued(token.ExternalPaymentToken, token.ExternalTransactionId);
            await unitOfWork.CommitAsync();
        }

        return OperationResult<InitiateBnplResult>.SuccessResult(new InitiateBnplResult(
            bnpl.Id, bnpl.PaymentTransactionId, bnpl.Status, token.ExternalPaymentToken, token.RedirectUrl));
    }
}
