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

namespace Baya.Infrastructure.Persistence.Repositories;

internal sealed class BookingRepository : BaseAsyncRepository<Booking>, IBookingRepository
{
    public BookingRepository(ApplicationDbContext dbContext) : base(dbContext)
    {
    }

    public Task AddAsync(Booking booking, CancellationToken cancellationToken)
        => base.AddAsync(booking);

    public Task<long?> GetBookingIdByRequestIdAsync(long bookingRequestId, CancellationToken cancellationToken)
        => TableNoTracking
            .Where(b => b.BookingRequestId == bookingRequestId)
            .Select(b => (long?)b.Id)
            .FirstOrDefaultAsync(cancellationToken);

    public Task<BookingLedgerAmounts?> GetLedgerAmountsAsync(long id, CancellationToken cancellationToken)
        => TableNoTracking
            .Where(b => b.Id == id)
            .Select(b => new BookingLedgerAmounts(b.NurseId, b.GrossPriceIrr, b.BalinyaarCommissionIrr, b.NursePayoutAmount))
            .FirstOrDefaultAsync(cancellationToken);

    public async Task<BookingDetailProjection?> GetDetailAsync(long id, CancellationToken cancellationToken)
    {
        var row = await (
                from b in TableNoTracking
                where b.Id == id
                join n in DbContext.Set<NurseProfile>() on b.NurseId equals n.Id
                join p in DbContext.Set<Patient>() on b.PatientId equals p.Id
                select new
                {
                    Booking = b,
                    NurseName = n.User.Name,
                    NurseFamily = n.User.FamilyName,
                    PatientName = p.DisplayName,
                    Sessions = b.Sessions
                        .OrderBy(s => s.SessionIndex)
                        .Select(s => new BookingSessionProjection(
                            s.Id,
                            s.SessionIndex,
                            s.ScheduledDate,
                            s.ScheduledTimeStart,
                            s.ScheduledTimeEnd,
                            s.Status,
                            s.VisitPayoutAmount,
                            s.PayoutEligibleAt,
                            s.Verification == null ? null : s.Verification.Status,
                            s.Verification == null ? null : s.Verification.CheckInAt,
                            s.Verification == null ? null : s.Verification.CheckOutAt,
                            s.Verification == null ? null : s.Verification.CheckInAddressMatch))
                        .ToList()
                })
            .FirstOrDefaultAsync(cancellationToken);

        if (row is null)
            return null;

        var bk = row.Booking;
        return new BookingDetailProjection(
            bk.Id, bk.BookingRequestId, bk.Status, bk.CustomerId, bk.NurseId,
            ComposeName(row.NurseName, row.NurseFamily), bk.PatientId, row.PatientName,
            bk.VariantId, bk.VariantSnapshotJson, bk.CustomerAddressId, bk.AddressSnapshotJson,
            bk.GrossPriceIrr, bk.BalinyaarCommissionIrr, bk.PlatformFeeRate, bk.NursePayoutAmount, bk.PspFeeAmount,
            bk.SessionCount, bk.ScheduledDate, bk.ScheduledTimeStart, bk.ScheduledTimeEnd,
            bk.ConfirmedAt, bk.CompletedAt, bk.CancelledAt, bk.CancelledBy, bk.CancellationReason,
            bk.CancellationPolicyCode, bk.CancellationRefundPercentage, bk.RefundableAmountIrr,
            bk.DisputeWindowEndsAt, bk.CreatedAt, row.Sessions);
    }

    public Task<PagedResult<BookingListItemDto>> ListForCustomerAsync(long customerId, string? status, int page, int pageSize, CancellationToken cancellationToken)
        => ListAsync(TableNoTracking.Where(b => b.CustomerId == customerId), status, forNurse: false, page, pageSize, cancellationToken);

    public Task<PagedResult<BookingListItemDto>> ListForNurseAsync(long nurseId, string? status, int page, int pageSize, CancellationToken cancellationToken)
        => ListAsync(TableNoTracking.Where(b => b.NurseId == nurseId), status, forNurse: true, page, pageSize, cancellationToken);

    public Task<PagedResult<BookingListItemDto>> ListAllAsync(string? status, int page, int pageSize, CancellationToken cancellationToken)
        => ListAsync(TableNoTracking, status, forNurse: false, page, pageSize, cancellationToken);

    private async Task<PagedResult<BookingListItemDto>> ListAsync(
        IQueryable<Booking> source, string? status, bool forNurse, int page, int pageSize, CancellationToken cancellationToken)
    {
        if (!string.IsNullOrWhiteSpace(status))
            source = source.Where(b => b.Status == status);

        var total = await source.CountAsync(cancellationToken);

        var rows = await (
                from b in source
                join n in DbContext.Set<NurseProfile>() on b.NurseId equals n.Id
                join p in DbContext.Set<Patient>() on b.PatientId equals p.Id
                orderby b.Id descending
                select new
                {
                    b.Id,
                    b.Status,
                    NurseName = n.User.Name,
                    NurseFamily = n.User.FamilyName,
                    PatientName = p.DisplayName,
                    b.ScheduledDate,
                    b.SessionCount,
                    b.GrossPriceIrr,
                    b.NursePayoutAmount,
                    b.DisputeWindowEndsAt,
                    b.CreatedAt
                })
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .ToListAsync(cancellationToken);

        var items = rows
            .Select(r => new BookingListItemDto(
                r.Id,
                r.Status,
                forNurse ? r.PatientName : ComposeName(r.NurseName, r.NurseFamily),
                r.ScheduledDate,
                r.SessionCount,
                (forNurse ? r.NursePayoutAmount : r.GrossPriceIrr).ToString(),
                r.DisputeWindowEndsAt,
                r.CreatedAt))
            .ToList();

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

    public Task<BookingParticipants?> GetParticipantsAsync(long bookingId, CancellationToken cancellationToken)
        => (
            from b in TableNoTracking
            where b.Id == bookingId
            join c in DbContext.Set<CustomerProfile>() on b.CustomerId equals c.Id
            join n in DbContext.Set<NurseProfile>() on b.NurseId equals n.Id
            select new BookingParticipants(c.UserId, n.UserId))
            .FirstOrDefaultAsync(cancellationToken);

    public Task<Booking?> GetTrackedWithSessionsAsync(long id, CancellationToken cancellationToken)
        => Table.Include(b => b.Sessions).FirstOrDefaultAsync(b => b.Id == id, cancellationToken);

    public Task<Booking?> GetTrackedWithCareAsync(long id, CancellationToken cancellationToken)
        => Table.Include(b => b.CareInstructions).FirstOrDefaultAsync(b => b.Id == id, cancellationToken);

    public async Task<Booking?> GetTrackedBookingBySessionAsync(long sessionId, CancellationToken cancellationToken)
    {
        var bookingId = await DbContext.Set<BookingSession>()
            .Where(s => s.Id == sessionId)
            .Select(s => (long?)s.BookingId)
            .FirstOrDefaultAsync(cancellationToken);

        if (bookingId is not { } bid)
            return null;

        return await Table
            .Include(b => b.Sessions).ThenInclude(s => s.Verification)
            .FirstOrDefaultAsync(b => b.Id == bid, cancellationToken);
    }

    public Task<CareInstructionsGate?> GetCareInstructionsGateAsync(long bookingId, CancellationToken cancellationToken)
        => TableNoTracking
            .Where(b => b.Id == bookingId)
            .Select(b => new CareInstructionsGate(
                b.Status,
                b.NurseId,
                b.CustomerId,
                b.CareInstructions == null
                    ? null
                    : new CareInstructionsDto(
                        b.Id,
                        b.CareInstructions.CurrentConditions,
                        b.CareInstructions.Medications,
                        b.CareInstructions.Allergies,
                        b.CareInstructions.SpecialInstructions,
                        b.CareInstructions.EmergencyContactName,
                        b.CareInstructions.EmergencyContactPhone)))
            .FirstOrDefaultAsync(cancellationToken);

    public async Task<PagedResult<BookingSessionListItemDto>> ListSessionsForNurseAsync(
        long nurseId, DateOnly? date, int page, int pageSize, CancellationToken cancellationToken)
    {
        var query = DbContext.Set<BookingSession>().AsNoTracking()
            .Where(s => s.Booking.NurseId == nurseId);

        if (date is { } d)
            query = query.Where(s => s.ScheduledDate == d);

        var total = await query.CountAsync(cancellationToken);

        var items = await (
                from s in query
                join p in DbContext.Set<Patient>() on s.Booking.PatientId equals p.Id
                orderby s.ScheduledDate, s.ScheduledTimeStart
                select new BookingSessionListItemDto(
                    s.Id,
                    s.BookingId,
                    s.SessionIndex,
                    p.DisplayName,
                    s.ScheduledDate,
                    s.ScheduledTimeStart,
                    s.ScheduledTimeEnd,
                    s.Status,
                    s.Verification == null ? VisitVerificationStatus.Pending : s.Verification.Status))
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .ToListAsync(cancellationToken);

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

    public Task<EvvGate?> GetEvvForSessionAsync(long sessionId, CancellationToken cancellationToken)
        => DbContext.Set<BookingSession>().AsNoTracking()
            .Where(s => s.Id == sessionId)
            .Select(s => new EvvGate(
                s.Booking.NurseId,
                s.Booking.CustomerId,
                s.Verification == null
                    ? null
                    : new VisitVerificationDto(
                        s.Verification.Id,
                        s.Id,
                        s.Verification.Status,
                        s.Verification.CheckInAt,
                        s.Verification.CheckInLat,
                        s.Verification.CheckInLng,
                        s.Verification.CheckOutAt,
                        s.Verification.CheckOutLat,
                        s.Verification.CheckOutLng,
                        s.Verification.CheckInAddressMatch,
                        s.Verification.CheckInDistanceMeters)))
            .FirstOrDefaultAsync(cancellationToken);

    public async Task<PagedResult<AdminEvvItemDto>> ListAdminEvvAsync(string type, int page, int pageSize, CancellationToken cancellationToken)
    {
        var query = DbContext.Set<BookingSession>().AsNoTracking().AsQueryable();

        // The two admin review lanes: an advisory location mismatch, or a no-show (missed) session.
        query = type == "no_show"
            ? query.Where(s => s.Status == BookingSessionStatus.Missed)
            : query.Where(s => s.Verification != null && s.Verification.CheckInAddressMatch == false);

        var total = await query.CountAsync(cancellationToken);

        var items = await query
            .OrderByDescending(s => s.Id)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Select(s => new AdminEvvItemDto(
                s.Id,
                s.BookingId,
                s.Booking.NurseId,
                s.Status,
                s.ScheduledDate,
                s.ScheduledTimeStart,
                s.Verification == null ? null : s.Verification.CheckInAt,
                s.Verification == null ? null : s.Verification.CheckInAddressMatch,
                s.Verification == null ? null : s.Verification.CheckInDistanceMeters))
            .ToListAsync(cancellationToken);

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

    public async Task<IReadOnlyList<BookingSession>> GetNoShowCandidatesAsync(DateOnly today, int batchSize, CancellationToken cancellationToken)
        => await DbContext.Set<BookingSession>()
            .Include(s => s.Booking)
            .Where(s => s.Status == BookingSessionStatus.Scheduled && s.ScheduledDate <= today)
            .OrderBy(s => s.Id)
            .Take(batchSize)
            .ToListAsync(cancellationToken);

    private static string ComposeName(string? name, string? familyName)
        => string.Join(' ', new[] { name, familyName }.Where(s => !string.IsNullOrWhiteSpace(s))).Trim();
}
