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

namespace Baya.Infrastructure.Persistence.Repositories;

internal sealed class ReviewRepository : BaseAsyncRepository<Review>, IReviewRepository
{
    public ReviewRepository(ApplicationDbContext dbContext) : base(dbContext)
    {
    }

    public Task AddAsync(Review review, CancellationToken cancellationToken) => base.AddAsync(review);

    public Task<ReviewableBooking?> GetReviewableBookingAsync(long bookingId, CancellationToken cancellationToken)
        => DbContext.Set<Booking>().AsNoTracking()
            .Where(b => b.Id == bookingId)
            .Select(b => new ReviewableBooking(b.Id, b.CustomerId, b.NurseId, b.Status))
            .FirstOrDefaultAsync(cancellationToken);

    public Task<bool> ExistsForBookingAsync(long bookingId, CancellationToken cancellationToken)
        => Entities.AnyAsync(r => r.BookingId == bookingId, cancellationToken);

    public async Task<MyReviewProjection?> GetMyReviewForBookingAsync(long bookingId, CancellationToken cancellationToken)
    {
        var row = await (from r in Entities.AsNoTracking().Where(r => r.BookingId == bookingId)
                         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.ModerationStatus,
                             r.Rating,
                             r.Body,
                             TagCodes = r.TagLinks.Select(l => l.Tag.Code).ToList(),
                             r.CreatedAt
                         })
            .FirstOrDefaultAsync(cancellationToken);

        if (row is null)
            return null;

        return new MyReviewProjection(row.UserId,
            new MyReviewDto(row.ModerationStatus, row.Rating, row.Body, row.TagCodes, row.CreatedAt));
    }

    public Task<Review?> GetTrackedAsync(long reviewId, CancellationToken cancellationToken)
        => Entities.FirstOrDefaultAsync(r => r.Id == reviewId, cancellationToken);

    public Task<Review?> GetTrackedWithTagsAsync(long reviewId, CancellationToken cancellationToken)
        => Entities.Include(r => r.TagLinks).FirstOrDefaultAsync(r => r.Id == reviewId, cancellationToken);

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

        return await DbContext.Set<ReviewTagMaster>().AsNoTracking()
            .Where(t => t.IsActive && codes.Contains(t.Code))
            .ToDictionaryAsync(t => t.Code, t => t.Id, cancellationToken);
    }

    public async Task<(int Count, long Sum)> GetPublishedRatingStatsExcludingAsync(
        long nurseProfileId, long excludeReviewId, CancellationToken cancellationToken)
    {
        var stats = await Entities.AsNoTracking()
            .Where(r => r.NurseProfileId == nurseProfileId
                        && r.ModerationStatus == ReviewModerationStatus.Published
                        && r.Id != excludeReviewId)
            .GroupBy(_ => 1)
            .Select(g => new { Count = g.Count(), Sum = g.Sum(x => (long)x.Rating) })
            .FirstOrDefaultAsync(cancellationToken);

        return stats is null ? (0, 0) : (stats.Count, stats.Sum);
    }

    public async Task<NurseReviewAggregateDto> GetNurseAggregateAsync(long nurseProfileId, CancellationToken cancellationToken)
    {
        var aggregate = await DbContext.Set<NurseProfile>().AsNoTracking()
            .Where(p => p.Id == nurseProfileId)
            .Select(p => new NurseReviewAggregateDto(p.AverageRating, p.TotalReviews))
            .FirstOrDefaultAsync(cancellationToken);

        return aggregate ?? new NurseReviewAggregateDto(0m, 0);
    }

    public async Task<PagedResult<ReviewListItemDto>> ListPublishedForNurseAsync(
        long nurseProfileId, int page, int pageSize, CancellationToken cancellationToken)
    {
        var query = Entities.AsNoTracking()
            .Where(r => r.NurseProfileId == nurseProfileId && r.ModerationStatus == ReviewModerationStatus.Published);

        var total = await query.CountAsync(cancellationToken);
        var items = await query
            .OrderByDescending(r => r.Id)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Select(r => new ReviewListItemDto(
                r.Id, r.Rating, r.Body,
                r.TagLinks.Select(l => l.Tag.Code).ToList(),
                r.CreatedAt))
            .ToListAsync(cancellationToken);

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

    public async Task<PagedResult<ModerationQueueItemDto>> GetModerationQueueAsync(
        string? status, int page, int pageSize, CancellationToken cancellationToken)
    {
        var query = Entities.AsNoTracking().AsQueryable();
        if (!string.IsNullOrWhiteSpace(status))
            query = query.Where(r => r.ModerationStatus == status);

        var total = await query.CountAsync(cancellationToken);
        var items = await query
            .OrderByDescending(r => r.Id)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Select(r => new ModerationQueueItemDto(
                r.Id, r.BookingId, r.NurseProfileId, r.CustomerProfileId, r.Rating, r.Body,
                r.ModerationStatus, r.ModerationReason,
                DbContext.Set<SupportAlert>()
                    .Where(a => a.ReviewId == r.Id && a.Type == SupportAlertType.LowRating)
                    .Select(a => (long?)a.Id)
                    .FirstOrDefault(),
                r.TagLinks.Select(l => l.Tag.Code).ToList(),
                r.CreatedAt))
            .ToListAsync(cancellationToken);

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

    public async Task<NurseTagAggregatesResult> GetTagAggregatesAsync(long nurseProfileId, CancellationToken cancellationToken)
    {
        var publishedCount = await Entities.AsNoTracking()
            .CountAsync(r => r.NurseProfileId == nurseProfileId && r.ModerationStatus == ReviewModerationStatus.Published, cancellationToken);

        var tagCounts = await DbContext.Set<ReviewTagLink>().AsNoTracking()
            .Where(l => l.Review.NurseProfileId == nurseProfileId && l.Review.ModerationStatus == ReviewModerationStatus.Published)
            .GroupBy(l => l.ReviewTagMasterId)
            .Select(g => new { TagId = g.Key, Count = g.Count() })
            .ToDictionaryAsync(x => x.TagId, x => x.Count, cancellationToken);

        var masters = await DbContext.Set<ReviewTagMaster>().AsNoTracking()
            .Where(t => t.IsActive)
            .OrderBy(t => t.SortOrder)
            .Select(t => new { t.Id, t.Code, t.LabelFa, t.LabelEn })
            .ToListAsync(cancellationToken);

        var tags = masters.Select(m =>
        {
            var count = tagCounts.GetValueOrDefault(m.Id);
            var percentage = publishedCount > 0
                ? Math.Round(100m * count / publishedCount, 1, MidpointRounding.AwayFromZero)
                : 0m;
            return new TagAggregateDto(m.Code, m.LabelFa, m.LabelEn, count, percentage);
        }).ToList();

        return new NurseTagAggregatesResult(publishedCount, tags);
    }
}
