using Microsoft.EntityFrameworkCore; using Nuuru.Server.Data; using Nuuru.Server.DTOs.Booru; using Nuuru.Server.Models.Booru; using Nuuru.Server.Services.Search; namespace Nuuru.Server.Services { public interface IFavoriteService { Task ToggleFavoriteAsync(int postId, Guid userId); Task IsFavoritedAsync(int postId, Guid userId); Task GetFavoriteStateAsync(int postId, Guid? userId = null); Task> GetUserFavoritesAsync(Guid userId, int page = 1, int pageSize = 20); Task GetUserFavoriteCountAsync(Guid userId); } public class FavoriteService : IFavoriteService { private readonly ApplicationDbContext _context; private readonly ILogger _logger; private readonly IDefaultQueryFilterService _defaultQueryFilterService; private readonly IBointsService _bointsService; public FavoriteService( ApplicationDbContext context, ILogger logger, IDefaultQueryFilterService defaultQueryFilterService, IBointsService bointsService) { _context = context; _logger = logger; _defaultQueryFilterService = defaultQueryFilterService; _bointsService = bointsService; } public async Task ToggleFavoriteAsync(int postId, Guid userId) { var postExists = await _context.BooruPosts.AnyAsync(p => p.Id == postId); if (!postExists) { _logger.LogWarning("Post {PostId} not found when toggling favorite", postId); return null; } var existingFavorite = await _context.BooruPostFavorites .FirstOrDefaultAsync(f => f.PostId == postId && f.UserId == userId); bool isFavorited; if (existingFavorite != null) { _context.BooruPostFavorites.Remove(existingFavorite); isFavorited = false; _logger.LogInformation("User {UserId} unfavorited post {PostId}", userId, postId); } else { var favorite = new PostFavorite { PostId = postId, UserId = userId, CreatedAt = DateTime.UtcNow }; _context.BooruPostFavorites.Add(favorite); isFavorited = true; _logger.LogInformation("User {UserId} favorited post {PostId}", userId, postId); } await _context.SaveChangesAsync(); // Credit uploader when their post is favorited if (isFavorited) { var uploaderId = await _context.BooruPosts .Where(p => p.Id == postId) .Select(p => p.UploaderId) .FirstOrDefaultAsync(); if (uploaderId != Guid.Empty && uploaderId != userId) { await _bointsService.CreditAsync(uploaderId, Models.BointsReason.FavoriteReceived, 3, sourcePostId: postId, sourceUserId: userId); } } var count = await GetFavoriteCountAsync(postId); return new FavoriteResponse { IsFavorited = isFavorited, FavoriteCount = count }; } public async Task IsFavoritedAsync(int postId, Guid userId) { return await _context.BooruPostFavorites .AnyAsync(f => f.PostId == postId && f.UserId == userId); } public async Task GetFavoriteStateAsync(int postId, Guid? userId = null) { var count = await GetFavoriteCountAsync(postId); var isFavorited = false; if (userId.HasValue) { isFavorited = await IsFavoritedAsync(postId, userId.Value); } return new FavoriteResponse { IsFavorited = isFavorited, FavoriteCount = count }; } public async Task> GetUserFavoritesAsync(Guid userId, int page = 1, int pageSize = 20) { var allowedPostIds = (await _defaultQueryFilterService .ApplyDefaultFiltersAsync(_context.BooruPosts.AsQueryable())) .Select(p => p.Id); return await _context.BooruPostFavorites .Where(f => f.UserId == userId && allowedPostIds.Contains(f.PostId)) .OrderByDescending(f => f.CreatedAt) .Skip((page - 1) * pageSize) .Take(pageSize) .Include(f => f.Post) .ThenInclude(p => p.Uploader) .Include(f => f.Post) .ThenInclude(p => p.PostTags) .ThenInclude(pt => pt.Tag) .ThenInclude(t => t.Category) .Select(f => f.Post) .ToListAsync(); } public async Task GetUserFavoriteCountAsync(Guid userId) { var allowedPostIds = (await _defaultQueryFilterService .ApplyDefaultFiltersAsync(_context.BooruPosts.AsQueryable())) .Select(p => p.Id); return await _context.BooruPostFavorites .CountAsync(f => f.UserId == userId && allowedPostIds.Contains(f.PostId)); } private async Task GetFavoriteCountAsync(int postId) { return await _context.BooruPostFavorites .CountAsync(f => f.PostId == postId); } } }