#nullable enable
using Baya.Application.Contracts.Persistence;
using Baya.Application.Models.Common;
using Baya.Application.Models.Payouts;
using Baya.Domain.Entities.Bnpl;
using Baya.Domain.Entities.Booking;
using Baya.Domain.Entities.Identity;
using Baya.Domain.Entities.Payments;
using Baya.Domain.Entities.Payouts;
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 PayoutRepository : BaseAsyncRepository<NursePayoutBatch>, IPayoutRepository
{
    public PayoutRepository(ApplicationDbContext dbContext) : base(dbContext)
    {
    }

    // Eligible = completed AND its dispute window closed by `now` (and by the period end, so period_end bounds the
    // run) AND no active refund reversed its money AND not already paid in a link row. No lower period bound, so a
    // booking that missed an earlier batch is still swept — it must eventually be paid. When
    // requireBnplSettlement is set, a BNPL-paid booking is held until its provider settlement is received.
    private IQueryable<EligibleBookingRow> EligibleBookingsQuery(DateOnly periodEnd, DateTime now, bool requireBnplSettlement)
    {
        var windowEnd = periodEnd.ToDateTime(TimeOnly.MaxValue);
        var query = from b in DbContext.Set<Booking>().AsNoTracking()
                    where b.Status == BookingStatus.Completed
                          && b.DisputeWindowEndsAt != null
                          && b.DisputeWindowEndsAt < now
                          && b.DisputeWindowEndsAt <= windowEnd
                          && !DbContext.Set<Refund>()
                              .Any(r => r.BookingId == b.Id && r.Status != RefundStatus.Failed && r.Status != RefundStatus.Rejected)
                          && !DbContext.Set<NursePayoutBookingLink>().IgnoreQueryFilters()
                              .Any(l => l.BookingId == b.Id)
                    select b;

        if (requireBnplSettlement)
            // Hold a BNPL-paid booking until its 1:1 bnpl_transaction reports a settlement (settled_at set).
            query = query.Where(b => !DbContext.Set<PaymentTransaction>()
                .Any(t => t.BookingId == b.Id && t.Status == PaymentTransactionStatus.Succeeded
                          && DbContext.Set<BnplTransaction>().Any(bt => bt.PaymentTransactionId == t.Id && bt.SettledAt == null)));

        return query.Select(b => new EligibleBookingRow(b.NurseId, b.Id, b.NursePayoutAmount));
    }

    public async Task<IReadOnlyList<EligibleBookingRow>> GetEligibleBookingsAsync(
        DateOnly periodStart, DateOnly periodEnd, DateTime now, bool requireBnplSettlement, CancellationToken cancellationToken)
        => await EligibleBookingsQuery(periodEnd, now, requireBnplSettlement).ToListAsync(cancellationToken);

    public async Task<PagedResult<EligibleNurseEarningsDto>> GetEligiblePreviewAsync(
        DateOnly periodStart, DateOnly periodEnd, DateTime now, bool requireBnplSettlement, int page, int pageSize, CancellationToken cancellationToken)
    {
        var rows = await EligibleBookingsQuery(periodEnd, now, requireBnplSettlement).ToListAsync(cancellationToken);

        var groups = rows
            .GroupBy(r => r.NurseId)
            .Select(g => new { NurseId = g.Key, Gross = g.Sum(x => x.PayoutAmountIrr), Count = g.Count() })
            .OrderBy(x => x.NurseId)
            .ToList();

        var total = groups.Count;
        var slice = groups.Skip((page - 1) * pageSize).Take(pageSize).ToList();
        var nurseIds = slice.Select(x => x.NurseId).ToList();

        var names = await GetNurseNamesAsync(nurseIds, cancellationToken);

        var clawbacks = await DbContext.Set<NurseClawback>().AsNoTracking()
            .Where(c => nurseIds.Contains(c.NurseId) && c.Status == ClawbackStatus.Pending)
            .GroupBy(c => c.NurseId)
            .Select(g => new { NurseId = g.Key, Sum = g.Sum(x => x.AmountIrr) })
            .ToDictionaryAsync(x => x.NurseId, x => x.Sum, cancellationToken);

        var verified = (await DbContext.Set<NurseBankAccount>().AsNoTracking()
                .Where(a => nurseIds.Contains(a.NurseId) && a.IsPrimary && a.IsVerified && a.MatchedNationalId == true)
                .Select(a => a.NurseId)
                .ToListAsync(cancellationToken))
            .ToHashSet();

        var items = slice.Select(x =>
        {
            var clawback = Math.Min(x.Gross, clawbacks.GetValueOrDefault(x.NurseId));
            var net = x.Gross - clawback;
            return new EligibleNurseEarningsDto(
                x.NurseId, names.GetValueOrDefault(x.NurseId), x.Count,
                x.Gross.ToString(), clawback.ToString(), net.ToString(), verified.Contains(x.NurseId));
        }).ToList();

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

    public Task<VerifiedPayoutAccount?> GetVerifiedPrimaryAccountAsync(long nurseId, CancellationToken cancellationToken)
        => DbContext.Set<NurseBankAccount>().AsNoTracking()
            .Where(a => a.NurseId == nurseId && a.IsPrimary && a.IsVerified && a.MatchedNationalId == true)
            .Select(a => new VerifiedPayoutAccount(a.Id, a.Iban))
            .FirstOrDefaultAsync(cancellationToken);

    public async Task<IReadOnlyDictionary<long, string>> GetNurseNamesAsync(IReadOnlyList<long> nurseIds, CancellationToken cancellationToken)
    {
        if (nurseIds.Count == 0)
            return new Dictionary<long, string>();

        var rows = await (from n in DbContext.Set<NurseProfile>().AsNoTracking()
                          where nurseIds.Contains(n.Id)
                          join u in DbContext.Set<User>() on n.UserId equals u.Id
                          select new { n.Id, u.Name, u.FamilyName })
            .ToListAsync(cancellationToken);

        return rows.ToDictionary(x => x.Id, x => $"{x.Name} {x.FamilyName}".Trim());
    }

    public async Task<long> GetPendingClawbackSumAsync(long nurseId, CancellationToken cancellationToken)
        => await DbContext.Set<NurseClawback>().AsNoTracking()
            .Where(c => c.NurseId == nurseId && c.Status == ClawbackStatus.Pending)
            .SumAsync(c => (long?)c.AmountIrr, cancellationToken) ?? 0;

    public async Task<IReadOnlyList<NurseClawback>> GetPendingClawbacksAsync(long nurseId, CancellationToken cancellationToken)
        => await DbContext.Set<NurseClawback>()
            .Where(c => c.NurseId == nurseId && c.Status == ClawbackStatus.Pending)
            .OrderBy(c => c.Id)
            .ToListAsync(cancellationToken);

    public Task AddBatchAsync(NursePayoutBatch batch, CancellationToken cancellationToken)
        => base.AddAsync(batch);

    public Task<NursePayoutBatch?> GetTrackedBatchAsync(long batchId, CancellationToken cancellationToken)
        => DbContext.Set<NursePayoutBatch>()
            .Include(b => b.Payouts).ThenInclude(p => p.BookingLinks)
            .FirstOrDefaultAsync(b => b.Id == batchId, cancellationToken);

    public Task<NursePayout?> GetTrackedPayoutAsync(long payoutId, CancellationToken cancellationToken)
        => DbContext.Set<NursePayout>()
            .Include(p => p.Batch)
            .FirstOrDefaultAsync(p => p.Id == payoutId, cancellationToken);

    public Task<bool> LedgerGroupExistsForPayoutAsync(long payoutId, CancellationToken cancellationToken)
        => DbContext.Set<LedgerEntry>().AsNoTracking()
            .AnyAsync(l => l.SourceRefType == LedgerSourceRefType.NursePayout && l.SourceRefId == payoutId, cancellationToken);

    public async Task<PagedResult<PayoutBatchDto>> ListBatchesAsync(string? status, int page, int pageSize, CancellationToken cancellationToken)
    {
        var query = DbContext.Set<NursePayoutBatch>().AsNoTracking().AsQueryable();
        if (!string.IsNullOrWhiteSpace(status))
            query = query.Where(b => b.Status == status);

        var total = await query.CountAsync(cancellationToken);
        var rows = await query
            .OrderByDescending(b => b.Id)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Select(b => new
            {
                b.Id, b.PeriodStart, b.PeriodEnd, b.ProcessingDate, b.TotalAmount, b.PayoutCount,
                b.Status, b.InitiatedByAdminId, b.ProcessedAt, b.FailureNotes, b.CreatedAt
            })
            .ToListAsync(cancellationToken);

        var items = rows.Select(b => new PayoutBatchDto(
                b.Id, b.PeriodStart, b.PeriodEnd, b.ProcessingDate, b.TotalAmount.ToString(), b.PayoutCount,
                b.Status, b.InitiatedByAdminId, b.ProcessedAt, b.FailureNotes, b.CreatedAt))
            .ToList();

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

    public async Task<PayoutBatchDetailDto?> GetBatchDetailAsync(long batchId, int page, int pageSize, CancellationToken cancellationToken)
    {
        var header = await DbContext.Set<NursePayoutBatch>().AsNoTracking()
            .Where(b => b.Id == batchId)
            .Select(b => new PayoutBatchDto(
                b.Id, b.PeriodStart, b.PeriodEnd, b.ProcessingDate, b.TotalAmount.ToString(), b.PayoutCount,
                b.Status, b.InitiatedByAdminId, b.ProcessedAt, b.FailureNotes, b.CreatedAt))
            .FirstOrDefaultAsync(cancellationToken);

        if (header is null)
            return null;

        var payoutsQuery = DbContext.Set<NursePayout>().AsNoTracking().Where(p => p.BatchId == batchId);
        var total = await payoutsQuery.CountAsync(cancellationToken);

        var rows = await payoutsQuery
            .OrderBy(p => p.Id)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Select(p => new
            {
                p.Id, p.NurseId, p.IbanSnapshot, p.GrossEarningsIrr, p.ClawbackAppliedIrr, p.NetAmountIrr,
                p.Amount, p.BookingCount, p.Status, p.TransferReference, p.PaidAt, p.FailureReason,
                Links = p.BookingLinks.Select(l => new { l.BookingId, l.SessionId, l.PayoutAmountIrr }).ToList()
            })
            .ToListAsync(cancellationToken);

        var names = await GetNurseNamesAsync(rows.Select(r => r.NurseId).Distinct().ToList(), cancellationToken);

        var payouts = rows.Select(p => new PayoutDto(
                p.Id, p.NurseId, names.GetValueOrDefault(p.NurseId), MaskIban(p.IbanSnapshot),
                p.GrossEarningsIrr.ToString(), p.ClawbackAppliedIrr.ToString(), p.NetAmountIrr.ToString(),
                p.Amount.ToString(), p.BookingCount, p.Status, p.TransferReference, p.PaidAt, p.FailureReason,
                p.Links.Select(l => new PayoutBookingLinkDto(l.BookingId, l.SessionId, l.PayoutAmountIrr.ToString())).ToList()))
            .ToList();

        return new PayoutBatchDetailDto(header, payouts, total, page, pageSize);
    }

    public async Task<PagedResult<NursePayoutHistoryDto>> GetNurseHistoryAsync(long nurseId, int page, int pageSize, CancellationToken cancellationToken)
    {
        var query = from p in DbContext.Set<NursePayout>().AsNoTracking()
                    where p.NurseId == nurseId
                    join b in DbContext.Set<NursePayoutBatch>() on p.BatchId equals b.Id
                    orderby p.Id descending
                    select new
                    {
                        p.Id, p.BatchId, p.Status, p.GrossEarningsIrr, p.ClawbackAppliedIrr, p.NetAmountIrr,
                        p.IbanSnapshot, p.TransferReference, p.PaidAt, p.FailureReason, b.PeriodStart, b.PeriodEnd
                    };

        var total = await query.CountAsync(cancellationToken);
        var rows = await query.Skip((page - 1) * pageSize).Take(pageSize).ToListAsync(cancellationToken);

        var items = rows.Select(p => new NursePayoutHistoryDto(
                p.Id, p.BatchId, p.Status, p.GrossEarningsIrr.ToString(), p.ClawbackAppliedIrr.ToString(),
                p.NetAmountIrr.ToString(), MaskIban(p.IbanSnapshot), p.TransferReference, p.PaidAt,
                p.PeriodStart, p.PeriodEnd, p.FailureReason))
            .ToList();

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

    public async Task<IReadOnlyList<NurseEarningsItemDto>> GetNurseEarningsAsync(long nurseId, DateTime now, CancellationToken cancellationToken)
    {
        // The nurse's terminal (completed/closed) bookings are the earnings-bearing set + the payout link (if any).
        var rows = await (from b in DbContext.Set<Booking>().AsNoTracking()
                          where b.NurseId == nurseId
                                && (b.Status == BookingStatus.Completed || b.Status == BookingStatus.Closed)
                          join p in DbContext.Set<Patient>() on b.PatientId equals p.Id
                          select new
                          {
                              b.Id,
                              PatientName = p.DisplayName,
                              b.ScheduledDate,
                              b.GrossPriceIrr,
                              b.BalinyaarCommissionIrr,
                              b.NursePayoutAmount,
                              b.DisputeWindowEndsAt,
                              PayoutId = DbContext.Set<NursePayoutBookingLink>()
                                  .Where(l => l.BookingId == b.Id).Select(l => (long?)l.PayoutId).FirstOrDefault(),
                              ClawbackIrr = DbContext.Set<NurseClawback>()
                                  .Where(c => c.BookingId == b.Id && c.NurseId == nurseId).Select(c => (long?)c.AmountIrr).FirstOrDefault()
                          })
            .ToListAsync(cancellationToken);

        var payoutIds = rows.Where(r => r.PayoutId is not null).Select(r => r.PayoutId!.Value).Distinct().ToList();
        var payouts = payoutIds.Count == 0
            ? new Dictionary<long, PayoutFacts>()
            : await DbContext.Set<NursePayout>().AsNoTracking()
                .Where(p => payoutIds.Contains(p.Id))
                .Select(p => new { p.Id, p.BatchId, p.Status, p.TransferReference, p.PaidAt, p.NetAmountIrr })
                .ToDictionaryAsync(p => p.Id, p => new PayoutFacts(p.BatchId, p.Status, p.TransferReference, p.PaidAt), cancellationToken);

        return rows.Select(r =>
        {
            PayoutFacts? payout = r.PayoutId is { } pid && payouts.TryGetValue(pid, out var f) ? f : null;
            var state = DeriveEarningsState(r.ClawbackIrr, payout, r.DisputeWindowEndsAt, now);
            return new NurseEarningsItemDto(
                r.Id, r.PatientName, r.ScheduledDate,
                r.GrossPriceIrr.ToString(), r.BalinyaarCommissionIrr.ToString(), r.NursePayoutAmount.ToString(),
                state, r.DisputeWindowEndsAt, null, payout?.PaidAt, payout?.TransferReference,
                r.PayoutId, payout?.BatchId, r.ClawbackIrr?.ToString(), null);
        }).ToList();
    }

    private static string DeriveEarningsState(long? clawbackIrr, PayoutFacts? payout, DateTime? disputeWindowEndsAt, DateTime now)
    {
        if (clawbackIrr is not null)
            return NurseEarningsState.ClawbackApplied;
        if (payout is not null)
            return NurseEarningsState.Paid;
        if (disputeWindowEndsAt is { } ends && ends < now)
            return NurseEarningsState.Eligible;
        return NurseEarningsState.Pending;
    }

    public async Task<long> GetPaidNetTotalAsync(long nurseId, CancellationToken cancellationToken)
        => await DbContext.Set<NursePayout>().AsNoTracking()
            .Where(p => p.NurseId == nurseId && p.Status == PayoutStatus.Paid)
            .SumAsync(p => (long?)p.NetAmountIrr, cancellationToken) ?? 0;

    public async Task<NursePayoutDetailDto?> GetNursePayoutDetailAsync(long payoutId, long nurseId, CancellationToken cancellationToken)
    {
        var p = await DbContext.Set<NursePayout>().AsNoTracking()
            .Where(x => x.Id == payoutId && x.NurseId == nurseId)
            .Select(x => new
            {
                x.Id, x.BatchId, x.Status, x.GrossEarningsIrr, x.ClawbackAppliedIrr, x.NetAmountIrr, x.Amount,
                x.IbanSnapshot, x.TransferReference, x.PaidAt, x.FailureReason,
                Links = x.BookingLinks.Select(l => new { l.BookingId, l.SessionId, l.PayoutAmountIrr }).ToList()
            })
            .FirstOrDefaultAsync(cancellationToken);

        if (p is null)
            return null;

        var batch = await DbContext.Set<NursePayoutBatch>().AsNoTracking()
            .Where(b => b.Id == p.BatchId)
            .Select(b => new PayoutBatchDto(
                b.Id, b.PeriodStart, b.PeriodEnd, b.ProcessingDate, b.TotalAmount.ToString(), b.PayoutCount,
                b.Status, b.InitiatedByAdminId, b.ProcessedAt, b.FailureNotes, b.CreatedAt))
            .FirstOrDefaultAsync(cancellationToken);

        return new NursePayoutDetailDto(
            p.Id, p.BatchId, p.Status, p.GrossEarningsIrr.ToString(), p.ClawbackAppliedIrr.ToString(),
            p.NetAmountIrr.ToString(), p.Amount.ToString(), MaskIban(p.IbanSnapshot), p.TransferReference, p.PaidAt,
            p.FailureReason, batch!,
            p.Links.Select(l => new PayoutBookingLinkDto(l.BookingId, l.SessionId, l.PayoutAmountIrr.ToString())).ToList());
    }

    private readonly record struct PayoutFacts(long BatchId, string Status, string? TransferReference, DateTime? PaidAt);

    // Show only the last 4 digits of the IBAN — the plaintext snapshot never leaves the server.
    private static string MaskIban(string iban)
    {
        if (string.IsNullOrEmpty(iban))
            return string.Empty;
        return iban.Length <= 4
            ? new string('•', iban.Length)
            : $"{new string('•', iban.Length - 4)}{iban[^4..]}";
    }
}
