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

namespace Baya.Infrastructure.Persistence.Repositories;

internal sealed class RefundRepository : BaseAsyncRepository<Refund>, IRefundRepository
{
    public RefundRepository(ApplicationDbContext dbContext) : base(dbContext)
    {
    }

    public Task<RefundMoneyContext?> GetRefundContextAsync(long bookingId, CancellationToken cancellationToken)
        => (from t in DbContext.Set<PaymentTransaction>().AsNoTracking()
            where t.BookingId == bookingId && t.Status == PaymentTransactionStatus.Succeeded
            join b in DbContext.Set<Booking>() on t.BookingId equals b.Id
            join c in DbContext.Set<CustomerProfile>() on b.CustomerId equals c.Id
            join g in DbContext.Set<PaymentGateway>() on t.GatewayId equals g.Id
            select new RefundMoneyContext(
                b.Id,
                b.CustomerId,
                c.UserId,
                b.NurseId,
                b.GrossPriceIrr,
                b.BalinyaarCommissionIrr,
                b.NursePayoutAmount,
                b.CancellationPolicyCode,
                b.CancellationRefundPercentage,
                b.RefundableAmountIrr,
                t.Id,
                t.GatewayReferenceCode,
                t.Amount,
                g.Type))
            .FirstOrDefaultAsync(cancellationToken);

    public async Task<long> GetRefundedSumForTransactionAsync(long paymentTransactionId, CancellationToken cancellationToken)
        => await TableNoTracking
            .Where(r => r.PaymentTransactionId == paymentTransactionId
                        && r.Status != RefundStatus.Failed && r.Status != RefundStatus.Rejected)
            .SumAsync(r => (long?)r.Amount, cancellationToken) ?? 0;

    public Task AddRefundAsync(Refund refund, CancellationToken cancellationToken)
        => base.AddAsync(refund);

    public Task AddClawbackAsync(NurseClawback clawback, CancellationToken cancellationToken)
        => DbContext.Set<NurseClawback>().AddAsync(clawback, cancellationToken).AsTask();

    public Task<NurseClawback?> GetTrackedClawbackByIdAsync(long id, CancellationToken cancellationToken)
        => DbContext.Set<NurseClawback>().FirstOrDefaultAsync(c => c.Id == id, cancellationToken);

    public Task<Refund?> GetTrackedRefundByIdAsync(long id, CancellationToken cancellationToken)
        => Table.FirstOrDefaultAsync(r => r.Id == id, cancellationToken);

    public Task<long?> GetProcessingRefundIdForTransactionAsync(long paymentTransactionId, CancellationToken cancellationToken)
        => TableNoTracking
            .Where(r => r.PaymentTransactionId == paymentTransactionId && r.Status == RefundStatus.Processing)
            .OrderByDescending(r => r.Id)
            .Select(r => (long?)r.Id)
            .FirstOrDefaultAsync(cancellationToken);

    public async Task<PagedResult<RefundListItemDto>> ListAsync(long? bookingId, string? status, int page, int pageSize, CancellationToken cancellationToken)
    {
        var query = TableNoTracking;
        if (bookingId is { } bid)
            query = query.Where(r => r.BookingId == bid);
        if (!string.IsNullOrWhiteSpace(status))
            query = query.Where(r => r.Status == status);

        var total = await query.CountAsync(cancellationToken);

        var rows = await query
            .OrderByDescending(r => r.Id)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Select(r => new
            {
                r.Id,
                r.BookingId,
                r.PaymentTransactionId,
                r.Amount,
                r.PlatformFeeRefundedIrr,
                r.NursePayoutRefundedIrr,
                r.RefundChannel,
                r.Status,
                r.RefundPercentage,
                r.ReasonCategory,
                r.CancellationPolicyCode,
                r.RefundPercentageApplied,
                r.ExpectedCustomerRefundEta,
                r.GatewayRefundReference,
                r.ExternalRevertReference,
                r.ProcessedAt,
                r.CreatedAt
            })
            .ToListAsync(cancellationToken);

        var items = rows
            .Select(r => new RefundListItemDto(
                r.Id, r.BookingId, r.PaymentTransactionId,
                r.Amount.ToString(), r.PlatformFeeRefundedIrr.ToString(), r.NursePayoutRefundedIrr.ToString(),
                r.RefundChannel, r.Status, r.RefundPercentage, r.ReasonCategory,
                r.CancellationPolicyCode, r.RefundPercentageApplied, r.ExpectedCustomerRefundEta,
                r.GatewayRefundReference, r.ExternalRevertReference, r.ProcessedAt, r.CreatedAt))
            .ToList();

        return new PagedResult<RefundListItemDto>(items, total, page, pageSize);
    }

    public Task<RefundStatusProjection?> GetStatusAsync(long id, CancellationToken cancellationToken)
        => StatusProjection(r => r.Id == id, cancellationToken);

    public Task<RefundStatusProjection?> GetStatusByBookingAsync(long bookingId, CancellationToken cancellationToken)
        // Newest refund for the booking (a re-refund is rare, but keep it deterministic).
        => StatusProjection(r => r.BookingId == bookingId, cancellationToken, latestFirst: true);

    private async Task<RefundStatusProjection?> StatusProjection(
        System.Linq.Expressions.Expression<Func<Refund, bool>> predicate, CancellationToken cancellationToken, bool latestFirst = false)
    {
        var query = from r in TableNoTracking.Where(predicate)
                    join b in DbContext.Set<Booking>() on r.BookingId equals b.Id
                    join c in DbContext.Set<CustomerProfile>() on b.CustomerId equals c.Id
                    select new
                    {
                        c.UserId,
                        r.Id,
                        r.BookingId,
                        r.Status,
                        r.RefundChannel,
                        r.Amount,
                        r.ExpectedCustomerRefundEta,
                        r.GatewayRefundReference,
                        r.ExternalRevertReference,
                        r.PlatformFeeRefundedIrr,
                        r.NursePayoutRefundedIrr,
                        r.RefundPercentageApplied,
                        r.CancellationPolicyCode,
                        r.CreatedAt,
                        r.ProcessedAt
                    };

        var row = await (latestFirst ? query.OrderByDescending(x => x.Id) : query).FirstOrDefaultAsync(cancellationToken);
        if (row is null)
            return null;

        var reference = Mask(row.GatewayRefundReference ?? row.ExternalRevertReference);
        var dto = new RefundStatusDto(
            row.Id, row.BookingId, row.Status, row.RefundChannel, row.Amount.ToString(),
            row.ExpectedCustomerRefundEta, reference,
            row.PlatformFeeRefundedIrr.ToString(), row.NursePayoutRefundedIrr.ToString(),
            row.RefundPercentageApplied, row.CancellationPolicyCode, row.CreatedAt, row.ProcessedAt);
        return new RefundStatusProjection(row.UserId, dto);
    }

    public Task<string?> GetExternalRevertReferenceAsync(long refundId, CancellationToken cancellationToken)
        => TableNoTracking
            .Where(r => r.Id == refundId)
            .Select(r => r.ExternalRevertReference)
            .FirstOrDefaultAsync(cancellationToken);

    // Show only the last 4 characters of an external reference to the customer — never the full PSP/BNPL id.
    private static string? Mask(string? reference)
    {
        if (string.IsNullOrEmpty(reference))
            return reference;
        return reference.Length <= 4
            ? new string('•', reference.Length)
            : $"{new string('•', reference.Length - 4)}{reference[^4..]}";
    }
}
