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

namespace Baya.Infrastructure.Persistence.Repositories;

internal sealed class TicketRepository : BaseAsyncRepository<Ticket>, ITicketRepository
{
    public TicketRepository(ApplicationDbContext dbContext) : base(dbContext)
    {
    }

    public Task AddAsync(Ticket ticket, CancellationToken cancellationToken) => base.AddAsync(ticket);

    public Task<TicketMessageDto?> GetMessageByClientIdAsync(long ticketId, string clientMessageId, CancellationToken cancellationToken)
        => DbContext.Set<TicketMessage>().AsNoTracking()
            .Where(m => m.TicketId == ticketId && m.ClientMessageId == clientMessageId)
            .Select(m => new TicketMessageDto(m.Id, m.SenderId, m.Body, m.IsInternal, m.SentAt))
            .FirstOrDefaultAsync(cancellationToken);

    public async Task AddMessageAsync(TicketMessage message, CancellationToken cancellationToken)
        => await DbContext.Set<TicketMessage>().AddAsync(message, cancellationToken);

    public async Task AddParticipantAsync(TicketParticipant participant, CancellationToken cancellationToken)
        => await DbContext.Set<TicketParticipant>().AddAsync(participant, cancellationToken);

    public Task<Ticket?> GetTrackedAsync(long ticketId, CancellationToken cancellationToken)
        => Entities.FirstOrDefaultAsync(t => t.Id == ticketId, cancellationToken);

    public Task<bool> ReferenceCodeExistsAsync(string referenceCode, CancellationToken cancellationToken)
        => Entities.AnyAsync(t => t.ReferenceCode == referenceCode, cancellationToken);

    public Task<TicketParticipant?> GetParticipantAsync(long ticketId, int userId, CancellationToken cancellationToken)
        => DbContext.Set<TicketParticipant>().FirstOrDefaultAsync(p => p.TicketId == ticketId && p.UserId == userId, cancellationToken);

    public Task<bool> IsActiveParticipantAsync(long ticketId, int userId, CancellationToken cancellationToken)
        => DbContext.Set<TicketParticipant>().AsNoTracking()
            .AnyAsync(p => p.TicketId == ticketId && p.UserId == userId && p.RemovedAt == null, cancellationToken);

    public Task<bool> CoordinationTicketExistsForBookingAsync(long bookingId, CancellationToken cancellationToken)
        => Entities.AnyAsync(t => t.BookingId == bookingId && t.Category == TicketCategory.Coordination, cancellationToken);

    public async Task<BookingPartyUserIds?> GetBookingPartyUserIdsAsync(long bookingId, CancellationToken cancellationToken)
    {
        var parties = await DbContext.Set<Booking>().AsNoTracking()
            .Where(b => b.Id == bookingId)
            .Select(b => new
            {
                CustomerUserId = DbContext.Set<Domain.Entities.Identity.CustomerProfile>()
                    .Where(c => c.Id == b.CustomerId).Select(c => (int?)c.UserId).FirstOrDefault(),
                NurseUserId = DbContext.Set<Domain.Entities.Identity.NurseProfile>()
                    .Where(n => n.Id == b.NurseId).Select(n => (int?)n.UserId).FirstOrDefault()
            })
            .FirstOrDefaultAsync(cancellationToken);

        if (parties is null || parties.CustomerUserId is not { } customerUserId || parties.NurseUserId is not { } nurseUserId)
            return null;

        return new BookingPartyUserIds(customerUserId, nurseUserId);
    }

    public Task<TicketHeaderDto?> GetHeaderAsync(long ticketId, CancellationToken cancellationToken)
        => Entities.AsNoTracking()
            .Where(t => t.Id == ticketId)
            .Select(t => new TicketHeaderDto(
                t.Id, t.ReferenceCode, t.Subject, t.Status, t.Category, t.BookingId, t.RefundId, t.OpenedById, t.ClosedAt))
            .FirstOrDefaultAsync(cancellationToken);

    public Task<bool> IsUserPartyToBookingAsync(long bookingId, int userId, CancellationToken cancellationToken)
        => DbContext.Set<Booking>().AsNoTracking()
            .Where(b => b.Id == bookingId)
            .AnyAsync(b =>
                DbContext.Set<Domain.Entities.Identity.CustomerProfile>().Any(c => c.Id == b.CustomerId && c.UserId == userId) ||
                DbContext.Set<Domain.Entities.Identity.NurseProfile>().Any(n => n.Id == b.NurseId && n.UserId == userId),
                cancellationToken);

    public async Task<IReadOnlyList<TicketParticipantDto>> GetActiveParticipantsAsync(long ticketId, CancellationToken cancellationToken)
        => await DbContext.Set<TicketParticipant>().AsNoTracking()
            .Where(p => p.TicketId == ticketId && p.RemovedAt == null)
            .OrderBy(p => p.Id)
            .Select(p => new TicketParticipantDto(p.UserId, p.RoleOnTicket))
            .ToListAsync(cancellationToken);

    public async Task<IReadOnlyList<int>> GetActiveParticipantUserIdsAsync(long ticketId, CancellationToken cancellationToken)
        => await DbContext.Set<TicketParticipant>().AsNoTracking()
            .Where(p => p.TicketId == ticketId && p.RemovedAt == null)
            .Select(p => p.UserId)
            .ToListAsync(cancellationToken);

    public async Task<IReadOnlyList<TicketMessageDto>> GetMessagesAsync(long ticketId, bool includeInternal, CancellationToken cancellationToken)
    {
        var query = DbContext.Set<TicketMessage>().AsNoTracking().Where(m => m.TicketId == ticketId);

        // The hard visibility boundary: the user view strips every internal message in the projection.
        if (!includeInternal)
            query = query.Where(m => !m.IsInternal);

        // Order by the monotonic identity, not sent_at: it matches send order and, unlike a DateTimeOffset
        // ORDER BY, the SQLite test provider can translate it.
        return await query
            .OrderBy(m => m.Id)
            .Select(m => new TicketMessageDto(m.Id, m.SenderId, m.Body, m.IsInternal, m.SentAt))
            .ToListAsync(cancellationToken);
    }

    public async Task<PagedResult<TicketSummaryDto>> ListMyTicketsAsync(
        int userId, string? status, string? referenceCode, long? bookingId, int page, int pageSize, CancellationToken cancellationToken)
    {
        var participantTickets = DbContext.Set<TicketParticipant>().AsNoTracking()
            .Where(p => p.UserId == userId && p.RemovedAt == null)
            .Select(p => p.TicketId);

        var query = Entities.AsNoTracking().Where(t => participantTickets.Contains(t.Id));
        query = ApplyTicketFilters(query, status, referenceCode);
        if (bookingId is { } b)
            query = query.Where(t => t.BookingId == b);

        return await PageAsync(query, page, pageSize, userId, cancellationToken);
    }

    public async Task<PagedResult<TicketSummaryDto>> ListForAdminAsync(
        string? status, string? category, string? referenceCode, long? bookingId, long? refundId,
        int page, int pageSize, CancellationToken cancellationToken)
    {
        var query = Entities.AsNoTracking().AsQueryable();
        query = ApplyTicketFilters(query, status, referenceCode);

        if (!string.IsNullOrWhiteSpace(category))
            query = query.Where(t => t.Category == category);
        if (bookingId is { } b)
            query = query.Where(t => t.BookingId == b);
        if (refundId is { } r)
            query = query.Where(t => t.RefundId == r);

        return await PageAsync(query, page, pageSize, forUserId: null, cancellationToken);
    }

    private static IQueryable<Ticket> ApplyTicketFilters(IQueryable<Ticket> query, string? status, string? referenceCode)
    {
        if (!string.IsNullOrWhiteSpace(status))
            query = query.Where(t => t.Status == status);
        if (!string.IsNullOrWhiteSpace(referenceCode))
            query = query.Where(t => t.ReferenceCode == referenceCode);
        return query;
    }

    private async Task<PagedResult<TicketSummaryDto>> PageAsync(
        IQueryable<Ticket> query, int page, int pageSize, int? forUserId, CancellationToken cancellationToken)
    {
        var total = await query.CountAsync(cancellationToken);
        var headers = await query
            .OrderByDescending(t => t.Id)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Select(t => new { t.Id, t.ReferenceCode, t.Subject, t.Status, t.Category, t.BookingId, t.RefundId, t.CreatedAt })
            .ToListAsync(cancellationToken);

        var ids = headers.Select(h => h.Id).ToList();

        // Non-internal messages only feed activity + unread (an internal note never touches a user-facing count).
        var msgs = await DbContext.Set<TicketMessage>().AsNoTracking()
            .Where(m => ids.Contains(m.TicketId) && !m.IsInternal)
            .Select(m => new { m.TicketId, m.SenderId, m.SentAt })
            .ToListAsync(cancellationToken);

        var lastRead = forUserId is { } uid
            ? await DbContext.Set<TicketParticipant>().AsNoTracking()
                .Where(p => ids.Contains(p.TicketId) && p.UserId == uid)
                .Select(p => new { p.TicketId, p.LastReadAt })
                .ToDictionaryAsync(x => x.TicketId, x => x.LastReadAt, cancellationToken)
            : [];

        var byTicket = msgs.GroupBy(m => m.TicketId).ToDictionary(g => g.Key, g => g.ToList());

        var items = headers.Select(h =>
        {
            var list = byTicket.GetValueOrDefault(h.Id) ?? [];
            DateTimeOffset? lastMessageAt = list.Count == 0 ? null : list.Max(m => m.SentAt);
            var read = lastRead.GetValueOrDefault(h.Id);
            var unread = forUserId is { } u
                ? list.Count(m => m.SenderId != u && (read is null || m.SentAt > read))
                : 0;
            return new TicketSummaryDto(
                h.Id, h.ReferenceCode, h.Subject, h.Status, h.Category, h.BookingId, h.RefundId, h.CreatedAt,
                lastMessageAt, unread);
        }).ToList();

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