#nullable enable
using Baya.Application.Contracts.Persistence;
using Baya.Application.Models.Bnpl;
using Baya.Domain.Entities.Booking;
using Baya.Domain.Entities.Bnpl;
using Baya.Domain.Entities.Identity;
using Baya.Domain.Entities.Payments;
using Baya.Domain.Entities.Refunds;
using Baya.Domain.Entities.User;
using Baya.Infrastructure.Persistence.Repositories.Common;
using Microsoft.EntityFrameworkCore;

namespace Baya.Infrastructure.Persistence.Repositories;

internal sealed class BnplRepository : BaseAsyncRepository<BnplTransaction>, IBnplRepository
{
    public BnplRepository(ApplicationDbContext dbContext) : base(dbContext)
    {
    }

    public Task<BnplOrderContext?> GetOrderContextAsync(long bookingRequestId, CancellationToken cancellationToken)
        => (from r in DbContext.Set<BookingRequest>().AsNoTracking()
            where r.Id == bookingRequestId
            join c in DbContext.Set<CustomerProfile>() on r.CustomerId equals c.Id
            join u in DbContext.Set<User>() on c.UserId equals u.Id
            select new BnplOrderContext(
                r.Id,
                r.CustomerId,
                u.Id,
                u.PhoneNumber,
                r.Status,
                r.PaymentDeadlineAt,
                r.Variant.Price * (r.Variant.SessionCount != null ? r.Variant.SessionCount.Value : 1)))
            .FirstOrDefaultAsync(cancellationToken);

    public Task AddAsync(BnplTransaction transaction, CancellationToken cancellationToken)
        => base.AddAsync(transaction);

    public Task<BnplTransaction?> GetTrackedByPaymentTransactionIdAsync(long paymentTransactionId, CancellationToken cancellationToken)
        => Table.FirstOrDefaultAsync(t => t.PaymentTransactionId == paymentTransactionId, cancellationToken);

    public Task<BnplTransaction?> GetTrackedByIdAsync(long id, CancellationToken cancellationToken)
        => Table.FirstOrDefaultAsync(t => t.Id == id, cancellationToken);

    public Task<BnplTransaction?> GetTrackedByBookingRequestIdAsync(long bookingRequestId, CancellationToken cancellationToken)
        // A correlated subquery keeps the root Table query tracking (a join to an AsNoTracking set would detach
        // the returned entity, so a later mutation would never persist).
        => Table.FirstOrDefaultAsync(
            t => DbContext.Set<PaymentTransaction>().Any(pt => pt.Id == t.PaymentTransactionId && pt.BookingRequestId == bookingRequestId),
            cancellationToken);

    public Task<BnplTransaction?> GetTrackedByTokenAsync(string externalPaymentToken, CancellationToken cancellationToken)
        => Table.FirstOrDefaultAsync(t => t.ExternalPaymentToken == externalPaymentToken, cancellationToken);

    public Task<bool> SettleLedgerExistsAsync(long bnplTransactionId, CancellationToken cancellationToken)
        => DbContext.Set<LedgerEntry>().AsNoTracking().AnyAsync(
            e => e.SourceRefType == LedgerSourceRefType.BnplTransaction && e.SourceRefId == bnplTransactionId,
            cancellationToken);

    public async Task<BnplOrderStatusProjection?> GetStatusByRequestAsync(long bookingRequestId, CancellationToken cancellationToken)
    {
        var id = await TableNoTracking
            .Where(t => DbContext.Set<PaymentTransaction>().Any(pt => pt.Id == t.PaymentTransactionId && pt.BookingRequestId == bookingRequestId))
            .OrderByDescending(t => t.Id)
            .Select(t => (long?)t.Id)
            .FirstOrDefaultAsync(cancellationToken);

        return id is null ? null : await GetStatusAsync(id.Value, cancellationToken);
    }

    public async Task<BnplOrderStatusProjection?> GetStatusAsync(long id, CancellationToken cancellationToken)
    {
        var row = await (from t in TableNoTracking
                         where t.Id == id
                         join pt in DbContext.Set<PaymentTransaction>() on t.PaymentTransactionId equals pt.Id
                         join c in DbContext.Set<CustomerProfile>() on pt.CustomerId equals c.Id
                         select new
                         {
                             c.UserId,
                             t.Id,
                             t.PaymentTransactionId,
                             pt.BookingId,
                             t.ProviderCode,
                             t.Status,
                             t.EligibilityStatus,
                             t.OrderAmountIrr,
                             t.SettledAmountIrr,
                             t.BnplCommissionIrr,
                             t.Currency,
                             t.InstallmentCount,
                             t.SettledAt,
                             t.RevertTransactionId,
                             t.RevertedAmountIrr,
                             t.RevertedAt,
                             t.ProviderCommissionReversedAmount,
                             t.RefundChannel,
                             t.CreatedAt
                         })
            .FirstOrDefaultAsync(cancellationToken);

        if (row is null)
            return null;

        // The async customer cash-back ETA lives on the linked bnpl_revert refund (b11), surfaced for the UI.
        DateOnly? eta = null;
        if (row.BookingId is { } bookingId && row.Status == BnplStatus.Reverted)
            eta = await DbContext.Set<Refund>().AsNoTracking()
                .Where(refund => refund.BookingId == bookingId && refund.RefundChannel == RefundChannel.BnplRevert)
                .OrderByDescending(refund => refund.Id)
                .Select(refund => refund.ExpectedCustomerRefundEta)
                .FirstOrDefaultAsync(cancellationToken);

        var dto = new BnplOrderStatusDto(
            row.Id, row.PaymentTransactionId, row.BookingId, row.ProviderCode, row.Status, row.EligibilityStatus,
            row.OrderAmountIrr.ToString(), row.SettledAmountIrr?.ToString(), row.BnplCommissionIrr?.ToString(),
            row.Currency, row.InstallmentCount, row.SettledAt,
            row.RevertTransactionId, row.RevertedAmountIrr?.ToString(), row.RevertedAt,
            row.ProviderCommissionReversedAmount?.ToString(), row.RefundChannel, eta, row.CreatedAt);

        return new BnplOrderStatusProjection(row.UserId, dto);
    }
}
