using Application.Abstractions.Data; using Application.Abstractions.Messaging; using Application.Features.Api.Feed.Shared; using Microsoft.EntityFrameworkCore; namespace Application.Features.Api.Feed.GetForYou; public sealed class Handler(IAppDbContext db) : IQueryHandler { public async Task Handle(Query request, CancellationToken ct) { var followingIDs = await db.MemberFollow.AsNoTracking() .Where(f => f.FollowerMemberID == request.MemberID && !f.IsMuted) .Select(f => f.FolloweeMemberID) .ToListAsync(ct); var since = DateTime.UtcNow.AddDays(-14); var take = Math.Max(20, request.PerPage * 3); var followingRows = followingIDs.Count > 0 ? await QueryRowsAsync(since, take, p => followingIDs.Contains(p.MemberID), byRecency: true, ct) : []; var trendingRows = await QueryRowsAsync(since, take, p => !followingIDs.Contains(p.MemberID), byRecency: false, ct); var merged = new List(); var seen = new HashSet(); var followTake = (int)Math.Round((followingRows.Count + trendingRows.Count) * 0.7); foreach (var r in followingRows.Take(followTake)) { if (seen.Add(r.ID)) { merged.Add(r); } } foreach (var r in trendingRows) { if (seen.Add(r.ID)) { merged.Add(r); } } merged = [..merged.OrderByDescending(r => r.CreatedAt)]; var total = merged.Count; var skip = (request.Page - 1) * request.PerPage; var list = merged.Skip(skip).Take(request.PerPage).Select(r => new FeedCardRow { PostID = r.ID, AuthorSID = r.AuthorSID, AuthorName = r.AuthorName, AuthorThumb = r.AuthorThumb, AuthorSummary = r.AuthorSummary, IsCreator = r.IsCreator, ChannelSID = r.ChannelSID, Subject = FeedContent.Excerpt(r.Content, 60), ContentExcerpt = FeedContent.Excerpt(r.Content, 200), Thumbnail = r.Thumbnail, Images = r.Images, Medias = r.Medias, Likes = r.Likes, Comments = r.Comments, Bookmarks = r.Bookmarks, Views = r.Views, Tags = [..r.Tags.Select(t => new FeedCardRow.TagRef { Name = t.Name, Slug = t.Slug })], CreatedAt = r.CreatedAt }).ToList(); return new Response { Total = total, List = list }; } private async Task> QueryRowsAsync( DateTime since, int take, System.Linq.Expressions.Expression> additionalFilter, bool byRecency, CancellationToken ct ) { var query = db.FeedPost.AsNoTracking() .Where(p => !p.IsDeleted && p.CreatedAt >= since) .Where(additionalFilter); var ordered = byRecency ? query.OrderByDescending(p => p.CreatedAt) : query.OrderByDescending(p => p.Likes + p.Comments * 2 + p.Views * 0.1).ThenByDescending(p => p.CreatedAt); return await ordered.Take(take).Select(p => new Row { ID = p.ID, AuthorSID = p.Member.SID, AuthorName = p.Member.Name, AuthorThumb = p.Member.Thumb, AuthorSummary = p.Member.Summary, IsCreator = p.Member.IsCreator, ChannelSID = p.Member.Channel != null ? p.Member.Channel.SID : null, Content = p.Content, Thumbnail = p.Thumbnail, Images = p.Images, Medias = p.Medias, Likes = p.Likes, Comments = p.Comments, Bookmarks = p.Bookmarks, Views = p.Views, Tags = p.FeedPostTag.Select(pt => new TagRow { Name = pt.FeedTag.Name, Slug = pt.FeedTag.Slug }).ToList(), CreatedAt = p.CreatedAt }).ToListAsync(ct); } private sealed class Row { public int ID { get; set; } public string? AuthorSID { get; set; } public string? AuthorName { get; set; } public string? AuthorThumb { get; set; } public string? AuthorSummary { get; set; } public bool IsCreator { get; set; } public string? ChannelSID { get; set; } public string Content { get; set; } = default!; public string? Thumbnail { get; set; } public byte Images { get; set; } public byte Medias { get; set; } public int Likes { get; set; } public int Comments { get; set; } public int Bookmarks { get; set; } public int Views { get; set; } public DateTime CreatedAt { get; set; } public List Tags { get; set; } = []; } private sealed class TagRow { public string Name { get; set; } = default!; public string Slug { get; set; } = default!; } }