using Microsoft.EntityFrameworkCore; using Nuuru.Server.Data; using Nuuru.Server.DTOs.Booru; using Nuuru.Server.Models.Booru; namespace Nuuru.Server.Services { public interface IVoteService { Task VoteAsync(int postId, Guid userId, int value); Task GetUserVoteAsync(int postId, Guid userId); Task GetVoteStateAsync(int postId, Guid? userId = null); } public class VoteService : IVoteService { private readonly ApplicationDbContext _context; private readonly ILogger _logger; public VoteService(ApplicationDbContext context, ILogger logger) { _context = context; _logger = logger; } public async Task VoteAsync(int postId, Guid userId, int value) { if (value < -1 || value > 1) { _logger.LogWarning("Invalid vote value {Value} from user {UserId}", value, userId); return null; } var post = await _context.BooruPosts.FirstOrDefaultAsync(p => p.Id == postId); if (post == null) { _logger.LogWarning("Post {PostId} not found when voting", postId); return null; } var existingVote = await _context.BooruPostVotes .FirstOrDefaultAsync(v => v.PostId == postId && v.UserId == userId); // Track the net change to apply to the uploader's reaction score int scoreDelta = 0; if (value == 0) { if (existingVote != null) { post.Score -= existingVote.Value; scoreDelta = -existingVote.Value; _context.BooruPostVotes.Remove(existingVote); _logger.LogInformation("User {UserId} removed vote on post {PostId}", userId, postId); } } else if (existingVote != null) { post.Score = post.Score - existingVote.Value + value; scoreDelta = value - existingVote.Value; existingVote.Value = value; existingVote.CreatedAt = DateTime.UtcNow; _logger.LogInformation("User {UserId} updated vote to {Value} on post {PostId}", userId, value, postId); } else { var vote = new PostVote { PostId = postId, UserId = userId, Value = value, CreatedAt = DateTime.UtcNow }; post.Score += value; scoreDelta = value; _context.BooruPostVotes.Add(vote); _logger.LogInformation("User {UserId} voted {Value} on post {PostId}", userId, value, postId); } // Update the uploader's reaction score (skip self-votes) if (scoreDelta != 0 && post.UploaderId != userId) { var uploader = await _context.Users.FindAsync(post.UploaderId); if (uploader != null) { uploader.ReactionScore += scoreDelta; } } await _context.SaveChangesAsync(); return await GetVoteStateAsync(postId, userId); } public async Task GetUserVoteAsync(int postId, Guid userId) { var vote = await _context.BooruPostVotes .FirstOrDefaultAsync(v => v.PostId == postId && v.UserId == userId); return vote?.Value; } public async Task GetVoteStateAsync(int postId, Guid? userId = null) { var votes = await _context.BooruPostVotes .Where(v => v.PostId == postId) .GroupBy(_ => 1) .Select(g => new { Upvotes = g.Count(v => v.Value == 1), Downvotes = g.Count(v => v.Value == -1), Score = g.Sum(v => v.Value) }) .FirstOrDefaultAsync(); int? userVote = null; if (userId.HasValue) { userVote = await GetUserVoteAsync(postId, userId.Value); } return new VoteResponse { Score = votes?.Score ?? 0, Upvotes = votes?.Upvotes ?? 0, Downvotes = votes?.Downvotes ?? 0, UserVote = userVote }; } } }