using Microsoft.EntityFrameworkCore; using Nuuru.Server.Data; using Nuuru.Server.Models.Booru; using Nuuru.Server.Services.BBCode; using static Nuuru.Server.Data.AvatarLookupExtensions; namespace Nuuru.Server.Services { public interface ICommentService { Task CreateCommentAsync(int postId, Guid userId, string contentRaw, string? ipAddress = null); Task GetCommentByIdAsync(int commentId); Task> GetAllCommentsByPostIdAsync(int postId); Task> GetCommentsByPostIdAsync(int postId, int page = 1, int pageSize = 50); Task GetCommentCountByPostIdAsync(int postId); Task UpdateCommentAsync(int commentId, string contentRaw); Task DeleteCommentAsync(int commentId); } public class CommentService : ICommentService { private readonly ApplicationDbContext _context; private readonly IBBCodeService _bbCodeService; private readonly INotificationService _notificationService; private readonly IWatchService _watchService; private readonly IUserSettingsService _settingsService; private readonly ILogger _logger; private readonly IBointsService _bointsService; public CommentService( ApplicationDbContext context, IBBCodeService bbCodeService, INotificationService notificationService, IWatchService watchService, IUserSettingsService settingsService, IBointsService bointsService, ILogger logger) { _context = context; _bbCodeService = bbCodeService; _notificationService = notificationService; _watchService = watchService; _settingsService = settingsService; _bointsService = bointsService; _logger = logger; } public async Task CreateCommentAsync(int postId, Guid userId, string contentRaw, string? ipAddress = null) { // Validate post exists and include uploader for notifications var post = await _context.BooruPosts .Include(p => p.Uploader) .FirstOrDefaultAsync(p => p.Id == postId); if (post == null) { _logger.LogWarning("Post {PostId} not found when creating comment", postId); return null; } // Validate user exists var user = await _context.Users.FindAsync(userId); if (user == null) { _logger.LogWarning("User {UserId} not found when creating comment", userId); return null; } // Parse BBCode to HTML and extract mentions in one pass var parseResult = _bbCodeService.ParseWithMentions(contentRaw, _context.CreateAvatarLookup()); var comment = new Comment { ContentRaw = contentRaw, ContentHtml = parseResult.Html, PostId = postId, UserId = userId, User = user, Post = post, CreatedAt = DateTime.UtcNow, IpAddress = ipAddress }; _context.BooruComments.Add(comment); await _context.SaveChangesAsync(); // Store mentions in relation table if (parseResult.MentionedUserIds.Count > 0) { foreach (var mentionedUserId in parseResult.MentionedUserIds) { _context.CommentMentions.Add(new Models.Booru.CommentMention { CommentId = comment.Id, MentionedUserId = mentionedUserId }); } await _context.SaveChangesAsync(); } _logger.LogInformation("Comment {CommentId} created by user {UserId} on post {PostId}", comment.Id, userId, postId); // Boints: 1 per comment, max once per hour await _bointsService.CreditAsync(userId, Models.BointsReason.CommentPosted, 1, sourceCommentId: comment.Id); // Auto-watch the post if preference is enabled var prefs = await _settingsService.GetAutoWatchPreferencesAsync(userId); if (prefs.AutoWatchOnPostComment) { await _watchService.EnsureWatchingAsync(userId, Models.WatchTargetType.BooruPost, postId); } // Create notifications if (post.Uploader != null) { await _notificationService.CreateCommentNotificationAsync(postId, comment.Id, userId, post.Uploader.Id); } await _notificationService.CreateMentionNotificationsAsync(parseResult.MentionedUserIds, comment.Id, postId, userId); await _notificationService.CreateWatchedPostCommentNotificationsAsync(postId, comment.Id, userId); return comment; } public async Task GetCommentByIdAsync(int commentId) { return await _context.BooruComments .Include(c => c.User) .FirstOrDefaultAsync(c => c.Id == commentId); } public async Task> GetAllCommentsByPostIdAsync(int postId) { return await _context.BooruComments .Include(c => c.User) .Where(c => c.PostId == postId) .OrderBy(c => c.CreatedAt) .ToListAsync(); } public async Task> GetCommentsByPostIdAsync(int postId, int page = 1, int pageSize = 50) { return await _context.BooruComments .Include(c => c.User) .Where(c => c.PostId == postId) .OrderBy(c => c.CreatedAt) // Oldest first for comments .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(); } public async Task GetCommentCountByPostIdAsync(int postId) { return await _context.BooruComments .CountAsync(c => c.PostId == postId); } public async Task UpdateCommentAsync(int commentId, string contentRaw) { var comment = await _context.BooruComments .Include(c => c.User) .Include(c => c.Mentions) .FirstOrDefaultAsync(c => c.Id == commentId); if (comment == null) { _logger.LogWarning("Comment {CommentId} not found for update", commentId); return null; } // Re-parse BBCode to HTML and extract mentions var parseResult = _bbCodeService.ParseWithMentions(contentRaw, _context.CreateAvatarLookup()); comment.ContentRaw = contentRaw; comment.ContentHtml = parseResult.Html; comment.EditedAt = DateTime.UtcNow; // Clear old mentions and add new ones _context.CommentMentions.RemoveRange(comment.Mentions); foreach (var mentionedUserId in parseResult.MentionedUserIds) { _context.CommentMentions.Add(new Models.Booru.CommentMention { CommentId = comment.Id, MentionedUserId = mentionedUserId }); } await _context.SaveChangesAsync(); _logger.LogInformation("Comment {CommentId} updated", commentId); return comment; } public async Task DeleteCommentAsync(int commentId) { var comment = await _context.BooruComments.FindAsync(commentId); if (comment == null) { _logger.LogWarning("Comment {CommentId} not found for deletion", commentId); return false; } _context.BooruComments.Remove(comment); await _context.SaveChangesAsync(); _logger.LogInformation("Comment {CommentId} deleted", commentId); return true; } } }