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

namespace Baya.Infrastructure.Persistence.Repositories;

internal sealed class BookingRequestRepository : BaseAsyncRepository<BookingRequest>, IBookingRequestRepository
{
    public BookingRequestRepository(ApplicationDbContext dbContext) : base(dbContext)
    {
    }

    public Task AddAsync(BookingRequest request, CancellationToken cancellationToken)
        => base.AddAsync(request);

    public Task<BookingRequest?> GetTrackedForCustomerAsync(long id, long customerId, CancellationToken cancellationToken)
        => Table.FirstOrDefaultAsync(r => r.Id == id && r.CustomerId == customerId, cancellationToken);

    public Task<BookingRequest?> GetTrackedForNurseAsync(long id, long nurseId, CancellationToken cancellationToken)
        => Table
            .Include(r => r.Customer)
            .FirstOrDefaultAsync(r => r.Id == id && r.NurseId == nurseId, cancellationToken);

    public async Task<IReadOnlyList<BookingRequest>> GetStalePendingAsync(DateTime now, int batchSize, CancellationToken cancellationToken)
        => await Table
            .Include(r => r.Customer)
            .Where(r => r.Status == BookingRequestStatus.PendingNurseResponse && r.NurseResponseDeadlineAt <= now)
            // Order by id (not the deadline): the whole batch is stale, order is irrelevant for correctness,
            // and DateTimeOffset is not sortable on the SQLite test provider.
            .OrderBy(r => r.Id)
            .Take(batchSize)
            .ToListAsync(cancellationToken);

    public async Task<IReadOnlyList<BookingRequest>> GetStaleAcceptedAsync(DateTime now, int batchSize, CancellationToken cancellationToken)
        => await Table
            .Include(r => r.Customer)
            .Where(r => r.Status == BookingRequestStatus.AcceptedAwaitingPayment
                        && r.PaymentDeadlineAt != null && r.PaymentDeadlineAt <= now)
            .OrderBy(r => r.Id)
            .Take(batchSize)
            .ToListAsync(cancellationToken);

    public async Task<PagedResult<BookingRequestListItemDto>> ListForCustomerAsync(
        long customerId, string? status, int page, int pageSize, CancellationToken cancellationToken)
    {
        var query = TableNoTracking.Where(r => r.CustomerId == customerId);
        if (!string.IsNullOrWhiteSpace(status))
            query = query.Where(r => r.Status == status);

        var total = await query.CountAsync(cancellationToken);
        var rows = await OrderActionableFirst(query)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Select(r => new CustomerRow(
                r.Id,
                r.Status,
                r.Nurse.User.Name,
                r.Nurse.User.FamilyName,
                r.Nurse.AverageRating,
                r.RequiredCaregiverGender,
                r.RequestedDate,
                r.RequestedTimeStart,
                r.RequestedTimeEnd,
                r.NurseResponseDeadlineAt,
                r.PaymentDeadlineAt,
                r.Variant.DisplayName,
                r.Patient.BirthDate))
            .ToListAsync(cancellationToken);

        var items = rows
            .Select(r => new BookingRequestListItemDto(
                r.Id, r.Status, ComposeName(r.NurseName, r.NurseFamily), r.NurseRating,
                r.RequiredCaregiverGender, r.RequestedDate, r.RequestedTimeStart, r.RequestedTimeEnd,
                r.NurseResponseDeadlineAt, r.PaymentDeadlineAt, CustomerNotes: null,
                r.VariantLabel, AgeFrom(r.PatientBirthDate)))
            .ToList();

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

    public async Task<PagedResult<BookingRequestListItemDto>> ListForNurseAsync(
        long nurseId, string? status, int page, int pageSize, CancellationToken cancellationToken)
    {
        var query = TableNoTracking.Where(r => r.NurseId == nurseId);
        if (!string.IsNullOrWhiteSpace(status))
            query = query.Where(r => r.Status == status);

        var total = await query.CountAsync(cancellationToken);
        // Nurse inbox: counterparty is the patient, and stage-1 customer_notes is the ONLY clinical context
        // surfaced. No encrypted/care field is projected here, ever. Age is folded in memory (DateOnly
        // arithmetic is not translatable), so fetch a flat row first.
        var rows = await OrderActionableFirst(query)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Select(r => new NurseRow(
                r.Id,
                r.Status,
                r.Patient.DisplayName,
                r.RequiredCaregiverGender,
                r.RequestedDate,
                r.RequestedTimeStart,
                r.RequestedTimeEnd,
                r.NurseResponseDeadlineAt,
                r.PaymentDeadlineAt,
                r.CustomerNotes,
                r.Variant.DisplayName,
                r.Patient.BirthDate))
            .ToListAsync(cancellationToken);

        var items = rows
            .Select(r => new BookingRequestListItemDto(
                r.Id, r.Status, r.PatientName, null, r.RequiredCaregiverGender,
                r.RequestedDate, r.RequestedTimeStart, r.RequestedTimeEnd,
                r.NurseResponseDeadlineAt, r.PaymentDeadlineAt, r.CustomerNotes,
                r.VariantLabel, AgeFrom(r.PatientBirthDate)))
            .ToList();

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

    public async Task<BookingRequestDetailProjection?> GetDetailAsync(long id, CancellationToken cancellationToken)
    {
        var row = await TableNoTracking
            .Where(r => r.Id == id)
            .Select(r => new DetailRow(
                r.Id,
                r.Status,
                r.CustomerId,
                r.NurseId,
                r.Nurse.User.Name,
                r.Nurse.User.FamilyName,
                r.Nurse.AverageRating,
                r.Nurse.TotalReviews,
                r.PatientId,
                r.Patient.DisplayName,
                r.VariantId,
                r.Variant.DisplayName,
                r.Variant.PriceUnit,
                r.CustomerAddressId,
                r.CustomerAddress.Title,
                r.CustomerAddress.CityId,
                r.CustomerAddress.City.NameFa,
                r.CustomerAddress.City.NameEn,
                r.CustomerAddress.DistrictId,
                r.CustomerAddress.DistrictId == null ? null : r.CustomerAddress.District.NameFa,
                r.CustomerAddress.DistrictId == null ? null : r.CustomerAddress.District.NameEn,
                r.CustomerAddress.AddressLine,
                r.CustomerAddress.PostalCode,
                r.CustomerAddress.RecipientName,
                r.CustomerAddress.RecipientPhone,
                r.RequiredCaregiverGender,
                r.RequestedDate,
                r.RequestedTimeStart,
                r.RequestedTimeEnd,
                r.CustomerNotes,
                r.NurseResponseDeadlineAt,
                r.PaymentDeadlineAt,
                r.NurseRejectionReason,
                r.CreatedAt,
                r.Variant.Price,
                r.Nurse.AvatarUrl,
                DbContext.Set<BookingEntity>()
                    .Where(b => b.BookingRequestId == r.Id)
                    .Select(b => (long?)b.Id)
                    .FirstOrDefault()))
            .FirstOrDefaultAsync(cancellationToken);

        if (row is null)
            return null;

        return new BookingRequestDetailProjection(
            row.Id, row.Status, row.CustomerId, row.NurseId,
            ComposeName(row.NurseName, row.NurseFamily), row.NurseRating, row.NurseTotalReviews,
            row.PatientId, row.PatientName,
            row.VariantId, row.VariantLabel, row.VariantPriceUnit,
            row.CustomerAddressId, row.AddressTitle, row.CityId, row.CityNameFa, row.CityNameEn,
            row.DistrictId, row.DistrictNameFa, row.DistrictNameEn,
            row.AddressLine, row.PostalCode, row.RecipientName, row.RecipientPhone,
            row.RequiredCaregiverGender, row.RequestedDate, row.RequestedTimeStart, row.RequestedTimeEnd,
            row.CustomerNotes, row.NurseResponseDeadlineAt, row.PaymentDeadlineAt,
            row.NurseRejectionReason, row.CreatedAt,
            row.VariantPrice, row.NurseAvatarUrl, row.BookingId);
    }

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

    public Task<BookingConversionSource?> GetConversionSourceAsync(long id, CancellationToken cancellationToken)
        => TableNoTracking
            .Where(r => r.Id == id)
            .Select(r => new BookingConversionSource(
                r.Id,
                r.Status,
                r.CustomerId,
                r.Customer.UserId,
                r.NurseId,
                r.Nurse.UserId,
                r.PatientId,
                r.Patient.DisplayName,
                r.VariantId,
                r.CustomerAddressId,
                r.RequestedDate,
                r.RequestedTimeStart,
                r.RequestedTimeEnd,
                new VariantSnapshot(
                    r.Variant.Id,
                    r.Variant.ServiceCategoryId,
                    r.Variant.ServiceCategory.NameFa,
                    r.Variant.ServiceCategory.NameEn,
                    r.Variant.Price,
                    r.Variant.PriceUnit,
                    r.Variant.SessionCount,
                    r.Variant.DisplayName,
                    r.Variant.Options
                        .Select(o => new VariantOptionDto(
                            o.OptionGroupId,
                            o.OptionGroup.NameFa,
                            o.OptionGroup.NameEn,
                            o.OptionValueId,
                            o.OptionValue.NameFa,
                            o.OptionValue.NameEn))
                        .ToList()),
                new AddressSnapshot(
                    r.CustomerAddress.Id,
                    r.CustomerAddress.Title,
                    r.CustomerAddress.CityId,
                    r.CustomerAddress.City.NameFa,
                    r.CustomerAddress.City.NameEn,
                    r.CustomerAddress.DistrictId,
                    r.CustomerAddress.DistrictId == null ? null : r.CustomerAddress.District.NameFa,
                    r.CustomerAddress.DistrictId == null ? null : r.CustomerAddress.District.NameEn,
                    r.CustomerAddress.AddressLine,
                    r.CustomerAddress.PostalCode,
                    r.CustomerAddress.RecipientName,
                    r.CustomerAddress.RecipientPhone,
                    r.CustomerAddress.Latitude,
                    r.CustomerAddress.Longitude)))
            .FirstOrDefaultAsync(cancellationToken);

    public Task<BookingPaymentContext?> GetPaymentContextAsync(long id, CancellationToken cancellationToken)
        => TableNoTracking
            .Where(r => r.Id == id)
            .Select(r => new BookingPaymentContext(
                r.Id,
                r.CustomerId,
                r.Status,
                r.PaymentDeadlineAt,
                r.Variant.Price * (r.Variant.SessionCount != null ? r.Variant.SessionCount.Value : 1)))
            .FirstOrDefaultAsync(cancellationToken);

    public Task<CheckoutContext?> GetCheckoutContextAsync(long id, long customerId, CancellationToken cancellationToken)
        => TableNoTracking
            .Where(r => r.Id == id && r.CustomerId == customerId)
            .Select(r => new CheckoutContext(
                r.Id,
                r.Status,
                ((r.Nurse.User.Name ?? "") + " " + (r.Nurse.User.FamilyName ?? "")).Trim(),
                r.Patient.DisplayName,
                r.Variant.DisplayName,
                r.Variant.PriceUnit,
                r.Variant.SessionCount,
                r.Variant.Price,
                r.RequestedDate,
                r.RequestedTimeStart,
                r.RequestedTimeEnd,
                r.PaymentDeadlineAt))
            .FirstOrDefaultAsync(cancellationToken);

    // Actionable (awaiting a party's action) rows float above terminal ones, then most-recent first. Recency
    // (id) rather than the deadline is the secondary key: for a given status the deadline tracks creation
    // time anyway, and DateTimeOffset is not sortable on the SQLite test provider.
    private static IQueryable<BookingRequest> OrderActionableFirst(IQueryable<BookingRequest> query)
        => query
            .OrderByDescending(r => r.Status == BookingRequestStatus.PendingNurseResponse
                                    || r.Status == BookingRequestStatus.AcceptedAwaitingPayment)
            .ThenByDescending(r => r.Id);

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

    // Coarse display age; null when the birth date is unset (default). Uses the UTC calendar date — a
    // one-day boundary drift is immaterial for a triage age band.
    private static int? AgeFrom(DateOnly birthDate)
    {
        if (birthDate == default)
            return null;

        var today = DateOnly.FromDateTime(DateTime.UtcNow);
        var age = today.Year - birthDate.Year;
        if (birthDate > today.AddYears(-age))
            age--;
        return age < 0 ? null : age;
    }

    private sealed record CustomerRow(
        long Id, string Status, string? NurseName, string? NurseFamily, decimal NurseRating,
        string? RequiredCaregiverGender, DateOnly RequestedDate, TimeOnly RequestedTimeStart, TimeOnly RequestedTimeEnd,
        DateTime NurseResponseDeadlineAt, DateTime? PaymentDeadlineAt,
        string VariantLabel, DateOnly PatientBirthDate);

    private sealed record NurseRow(
        long Id, string Status, string PatientName, string? RequiredCaregiverGender,
        DateOnly RequestedDate, TimeOnly RequestedTimeStart, TimeOnly RequestedTimeEnd,
        DateTime NurseResponseDeadlineAt, DateTime? PaymentDeadlineAt, string? CustomerNotes,
        string VariantLabel, DateOnly PatientBirthDate);

    private sealed record DetailRow(
        long Id, string Status, long CustomerId, long NurseId,
        string? NurseName, string? NurseFamily, decimal NurseRating, int NurseTotalReviews,
        long PatientId, string PatientName,
        long VariantId, string VariantLabel, string VariantPriceUnit,
        long CustomerAddressId, string AddressTitle, long CityId, string CityNameFa, string CityNameEn,
        long? DistrictId, string? DistrictNameFa, string? DistrictNameEn,
        string? AddressLine, string? PostalCode, string? RecipientName, string? RecipientPhone,
        string? RequiredCaregiverGender, DateOnly RequestedDate, TimeOnly RequestedTimeStart, TimeOnly RequestedTimeEnd,
        string? CustomerNotes, DateTime NurseResponseDeadlineAt, DateTime? PaymentDeadlineAt,
        string? NurseRejectionReason, DateTimeOffset CreatedAt,
        long VariantPrice, string? NurseAvatarUrl, long? BookingId);
}
