using Microsoft.EntityFrameworkCore; using Nuuru.Server.Data; using Nuuru.Server.Models.Messaging; using Nuuru.Server.Services.BBCode; using static Nuuru.Server.Data.AvatarLookupExtensions; namespace Nuuru.Server.Services { public interface IMessageService { Task CreateMessageAsync(Guid conversationId, Guid authorId, string content); Task<(IEnumerable Items, int TotalCount)> GetMessagesByConversationAsync(Guid conversationId, int page, int pageSize); Task UpdateMessageAsync(int messageId, Guid requestingUserId, string content); Task DeleteMessageAsync(int messageId, Guid requestingUserId); Task GetMessageByIdAsync(int messageId); Task GetMessagePageAsync(Guid conversationId, int messageId, int pageSize = 50); } public class MessageService : IMessageService { private readonly ApplicationDbContext _context; private readonly IBBCodeService _bbCodeService; private readonly INotificationService _notificationService; private readonly ILogger _logger; public MessageService( ApplicationDbContext context, IBBCodeService bbCodeService, INotificationService notificationService, ILogger logger) { _context = context; _bbCodeService = bbCodeService; _notificationService = notificationService; _logger = logger; } public async Task CreateMessageAsync(Guid conversationId, Guid authorId, string content) { // Validate conversation exists var conversation = await _context.Conversations .Include(c => c.Participants) .FirstOrDefaultAsync(c => c.Id == conversationId); if (conversation == null) { _logger.LogWarning("Conversation {ConversationId} not found when creating message", conversationId); return null; } // Check if conversation is locked if (conversation.IsLocked) { _logger.LogWarning("Attempted to create message in locked conversation {ConversationId}", conversationId); return null; } // Validate author is an active participant var authorParticipant = conversation.Participants.FirstOrDefault(p => p.UserId == authorId && !p.HasLeft); if (authorParticipant == null) { _logger.LogWarning("User {UserId} is not an active participant of conversation {ConversationId}", authorId, conversationId); return null; } // Validate author exists var author = await _context.Users.FindAsync(authorId); if (author == null) { _logger.LogWarning("Author {AuthorId} not found when creating message", authorId); return null; } // Parse BBCode to HTML and extract mentions var parseResult = _bbCodeService.ParseWithMentions(content, _context.CreateAvatarLookup()); var message = new Message { ContentRaw = content, ContentHtml = parseResult.Html, ConversationId = conversationId, Conversation = conversation, AuthorId = authorId, Author = author, CreatedAt = DateTime.UtcNow }; _context.Messages.Add(message); // Update conversation stats conversation.MessageCount++; conversation.LastMessageAt = DateTime.UtcNow; // Update author's read timestamp authorParticipant.LastReadAt = DateTime.UtcNow; await _context.SaveChangesAsync(); // Store mentions in relation table (uses saved message.Id) if (parseResult.MentionedUserIds.Count > 0) { foreach (var mentionedUserId in parseResult.MentionedUserIds) { _context.MessageMentions.Add(new MessageMention { MessageId = message.Id, MentionedUserId = mentionedUserId }); } await _context.SaveChangesAsync(); } _logger.LogInformation("Message {MessageId} created by user {AuthorId} in conversation {ConversationId}", message.Id, authorId, conversationId); // Create notifications for other participants var recipientIds = conversation.Participants .Where(p => p.UserId != authorId && !p.HasLeft) .Select(p => p.UserId) .ToList(); try { await _notificationService.CreatePrivateMessageNotificationAsync(conversationId, message.Id, authorId, recipientIds); } catch (Exception ex) { _logger.LogError(ex, "Failed to create notifications for message {MessageId}", message.Id); } return message; } public async Task<(IEnumerable Items, int TotalCount)> GetMessagesByConversationAsync(Guid conversationId, int page, int pageSize) { var query = _context.Messages .Include(m => m.Author) .Where(m => m.ConversationId == conversationId); var totalCount = await query.CountAsync(); var messages = await query .OrderBy(m => m.CreatedAt) .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(); return (messages, totalCount); } public async Task UpdateMessageAsync(int messageId, Guid requestingUserId, string content) { var message = await _context.Messages .Include(m => m.Author) .Include(m => m.Mentions) .FirstOrDefaultAsync(m => m.Id == messageId); if (message == null) { _logger.LogWarning("Message {MessageId} not found for update", messageId); return null; } // Only author can edit if (message.AuthorId != requestingUserId) { _logger.LogWarning("User {UserId} attempted to edit message {MessageId} they don't own", requestingUserId, messageId); return null; } // Re-parse BBCode to HTML and extract mentions var parseResult = _bbCodeService.ParseWithMentions(content, _context.CreateAvatarLookup()); message.ContentRaw = content; message.ContentHtml = parseResult.Html; message.EditedAt = DateTime.UtcNow; // Clear old mentions and add new ones _context.MessageMentions.RemoveRange(message.Mentions); foreach (var mentionedUserId in parseResult.MentionedUserIds) { _context.MessageMentions.Add(new MessageMention { MessageId = message.Id, MentionedUserId = mentionedUserId }); } await _context.SaveChangesAsync(); _logger.LogInformation("Message {MessageId} updated by user {UserId}", messageId, requestingUserId); return message; } public async Task DeleteMessageAsync(int messageId, Guid requestingUserId) { var message = await _context.Messages .Include(m => m.Conversation) .FirstOrDefaultAsync(m => m.Id == messageId); if (message == null) { _logger.LogWarning("Message {MessageId} not found for deletion", messageId); return false; } // Only author can delete if (message.AuthorId != requestingUserId) { _logger.LogWarning("User {UserId} attempted to delete message {MessageId} they don't own", requestingUserId, messageId); return false; } // Update conversation message count message.Conversation.MessageCount = Math.Max(0, message.Conversation.MessageCount - 1); _context.Messages.Remove(message); await _context.SaveChangesAsync(); _logger.LogInformation("Message {MessageId} deleted by user {UserId}", messageId, requestingUserId); return true; } public async Task GetMessageByIdAsync(int messageId) { return await _context.Messages .Include(m => m.Author) .Include(m => m.Conversation) .FirstOrDefaultAsync(m => m.Id == messageId); } public async Task GetMessagePageAsync(Guid conversationId, int messageId, int pageSize = 50) { var message = await _context.Messages .Where(m => m.Id == messageId && m.ConversationId == conversationId) .Select(m => m.CreatedAt) .FirstOrDefaultAsync(); if (message == default) return null; var messagesBefore = await _context.Messages .CountAsync(m => m.ConversationId == conversationId && m.CreatedAt < message); // Messages are ordered ascending, so page = (index / pageSize) + 1 return (messagesBefore / pageSize) + 1; } } }