#nullable enable
using Baya.Application.Contracts.Common;
using Baya.Application.Contracts.Configuration;
using Baya.Application.Contracts.Invoices;
using Baya.Application.Contracts.Payments;
using Baya.Application.Contracts.Persistence;
using Baya.Application.Models.Common;
using Baya.Application.Models.Invoices;
using Baya.Domain.Entities.Invoices;
using Mediator;
using Microsoft.EntityFrameworkCore;

namespace Baya.Application.Features.Invoices.Commands.IssueInvoice;

internal sealed class IssueInvoiceCommandHandler(
    IUnitOfWork unitOfWork,
    IPlatformConfig platformConfig,
    IDateTimeProvider dateTimeProvider,
    IDistributedLock distributedLock,
    IMoadianClient moadianClient,
    IObjectStorage objectStorage)
    : IRequestHandler<IssueInvoiceCommand, OperationResult<InvoiceDto>>
{
    public async ValueTask<OperationResult<InvoiceDto>> Handle(IssueInvoiceCommand request, CancellationToken cancellationToken)
    {
        // Idempotent per booking — re-issue returns the existing invoice, never a second number.
        var existing = await unitOfWork.InvoiceRepository.GetTrackedByBookingIdAsync(request.BookingId, cancellationToken);
        if (existing is not null)
            return OperationResult<InvoiceDto>.SuccessResult(InvoiceDtoFactory.FromEntity(existing, PdfUrl(existing)));

        var amounts = await unitOfWork.InvoiceRepository.GetBookingAmountsAsync(request.BookingId, cancellationToken);
        if (amounts is null)
            return OperationResult<InvoiceDto>.NotFoundResult("Booking not found.");

        // VAT applies to the commission line ONLY — never the nurse payout — at a config-driven rate. A
        // vat_rate = 0 exemption yields vat_irr = 0. Integer-only, no float path.
        var vatRate = await platformConfig.GetConfig<decimal>("vat_rate", cancellationToken);
        var vatIrr = (long)Math.Round(amounts.PlatformCommissionIrr * vatRate, MidpointRounding.AwayFromZero);
        var now = dateTimeProvider.UtcNow.UtcDateTime;

        await using var _ = await distributedLock.AcquireAsync("invoice:number", cancellationToken);

        // Re-check under the lock in case a concurrent issue won the race.
        existing = await unitOfWork.InvoiceRepository.GetTrackedByBookingIdAsync(request.BookingId, cancellationToken);
        if (existing is not null)
            return OperationResult<InvoiceDto>.SuccessResult(InvoiceDtoFactory.FromEntity(existing, PdfUrl(existing)));

        var sequence = await unitOfWork.InvoiceRepository.ReserveNextInvoiceNumberAsync(cancellationToken);
        var invoiceNumber = $"INV-{sequence:D10}";

        // Merchant-of-record resolution (b15): the invoice issuer + settlement target follow partner_centers, not
        // a hardcoded platform. GetCenterForBooking is the single resolver — it returns partner_center only when a
        // merchant-of-record center legally covers the booking, and platform otherwise.
        var issuer = await unitOfWork.PartnerCenterRepository.ResolveCenterForBookingAsync(request.BookingId, cancellationToken);

        var submission = new InvoiceSubmission(invoiceNumber, request.BookingId, amounts.GrossIrr, amounts.PlatformCommissionIrr, vatIrr);
        var moadian = await moadianClient.SubmitAsync(submission, cancellationToken);

        var invoice = new Invoice
        {
            BookingId = request.BookingId,
            InvoiceNumber = invoiceNumber,
            IssuingEntityType = issuer?.IssuingEntityType ?? InvoiceIssuingEntityType.Platform,
            PartnerCenterId = issuer?.PartnerCenterId,
            GrossIrr = amounts.GrossIrr,
            PlatformCommissionIrr = amounts.PlatformCommissionIrr,
            BnplCommissionIrr = amounts.BnplCommissionIrr,
            VatRate = vatRate,
            VatIrr = vatIrr,
            IssuedAt = now
        };
        invoice.ApplyMoadianResult(moadian.Status, moadian.ReferenceNumber);

        await unitOfWork.InvoiceRepository.AddInvoiceAsync(invoice, cancellationToken);

        try
        {
            // Commits the counter increment + the invoice row together, so the sequence stays gap-free.
            await unitOfWork.CommitAsync();
        }
        catch (DbUpdateException)
        {
            // Lost the UNIQUE(booking_id) race — return the invoice the winner created (idempotent no-op).
            await unitOfWork.RollBackAsync();
            var winner = await unitOfWork.InvoiceRepository.GetTrackedByBookingIdAsync(request.BookingId, cancellationToken);
            return winner is null
                ? OperationResult<InvoiceDto>.ConflictResult("An invoice already exists for this booking.")
                : OperationResult<InvoiceDto>.SuccessResult(InvoiceDtoFactory.FromEntity(winner, PdfUrl(winner)));
        }

        return OperationResult<InvoiceDto>.SuccessResult(InvoiceDtoFactory.FromEntity(invoice, PdfUrl(invoice)));
    }

    private string? PdfUrl(Invoice invoice)
        => invoice.PdfStorageKey is { } key ? objectStorage.GetUrl(key) : null;
}
