using JSMR.Application.Circles.Queries.Search; using JSMR.Application.Common.Search; using JSMR.Infrastructure.Common.Queries; using Microsoft.EntityFrameworkCore; using System.Linq.Expressions; namespace JSMR.Infrastructure.Data.Repositories.Circles; public class CircleSearchProvider(AppDbContext context) : SearchProvider, ICircleSearchProvider { protected override IQueryable GetBaseQuery() { // Project from Circles so we can use correlated subqueries per CircleId. var q = from c in context.Circles.AsNoTracking() select new CircleSearchItem { CircleId = c.CircleId, Name = c.Name, MakerId = c.MakerId, Favorite = c.Favorite, Blacklisted = c.Blacklisted, Spam = c.Spam, // Aggregates Downloads = context.VoiceWorks .Where(v => v.CircleId == c.CircleId) .Select(v => (int?)v.Downloads) // make nullable for Sum over empty set .Sum() ?? 0, Releases = context.VoiceWorks .Count(v => v.CircleId == c.CircleId && v.SalesDate != null), Pending = context.VoiceWorks .Count(v => v.CircleId == c.CircleId && v.ExpectedDate != null), FirstReleaseDate = context.VoiceWorks .Where(v => v.CircleId == c.CircleId) .Select(v => v.SalesDate) .Min(), LatestReleaseDate = context.VoiceWorks .Where(v => v.CircleId == c.CircleId) .Select(v => v.SalesDate) .Max(), // "Latest" by ProductId length, then value LatestProductId = context.VoiceWorks .Where(v => v.CircleId == c.CircleId) .OrderByDescending(v => v.ProductId.Length) .ThenByDescending(v => v.ProductId) .Select(v => v.ProductId) .FirstOrDefault(), // If you want these two in base query too: LatestVoiceWorkHasImage = context.VoiceWorks .Where(v => v.CircleId == c.CircleId) .OrderByDescending(v => v.ProductId.Length) .ThenByDescending(v => v.ProductId) .Select(v => (bool?)v.HasImage) .FirstOrDefault(), LatestVoiceWorkSalesDate = context.VoiceWorks .Where(v => v.CircleId == c.CircleId) .OrderByDescending(v => v.ProductId.Length) .ThenByDescending(v => v.ProductId) .Select(v => v.SalesDate) .FirstOrDefault() }; return q; } protected override IQueryable ApplyFilters(IQueryable query, CircleSearchCriteria criteria) { if (!string.IsNullOrWhiteSpace(criteria.Name)) { var term = $"%{criteria.Name.Trim()}%"; query = query.Where(x => EF.Functions.Like(x.Name, term) || EF.Functions.Like(x.MakerId, term)); } switch (criteria.Status) { case CircleStatus.NotBlacklisted: query = query.Where(x => !x.Blacklisted); break; case CircleStatus.Favorited: query = query.Where(x => x.Favorite); break; case CircleStatus.Blacklisted: query = query.Where(x => x.Blacklisted); break; case CircleStatus.Spam: query = query.Where(x => x.Spam); break; } return query; } protected override Expression> GetSortExpression(CircleSortField field) => field switch { CircleSortField.Favorite => x => !x.Favorite, CircleSortField.Blacklisted => x => !x.Blacklisted, CircleSortField.Spam => x => !x.Spam, _ => x => x.Name }; protected override IEnumerable<(Expression> Selector, SortDirection Dir)> GetDefaultSortChain() { yield return (x => x.Name, SortDirection.Ascending); yield return (x => x.MakerId, SortDirection.Ascending); } protected override IQueryable GetSelectQuery(IOrderedQueryable query) { // Join to VoiceWorks by LatestProductId to fill HasImage / SalesDate var selected = from item in query join vw in context.VoiceWorks.AsNoTracking() on item.LatestProductId equals vw.ProductId into vws from latest in vws.DefaultIfEmpty() select new CircleSearchItem { CircleId = item.CircleId, Name = item.Name, MakerId = item.MakerId, Favorite = item.Favorite, Blacklisted = item.Blacklisted, Spam = item.Spam, Downloads = item.Downloads, Releases = item.Releases, Pending = item.Pending, FirstReleaseDate = item.FirstReleaseDate, LatestReleaseDate = item.LatestReleaseDate, LatestProductId = item.LatestProductId, LatestVoiceWorkHasImage = latest != null ? latest.HasImage : (bool?)null, LatestVoiceWorkSalesDate = latest != null ? latest.SalesDate : null }; return selected; } }