using Microsoft.EntityFrameworkCore; using Nuuru.Server.Data; using Nuuru.Server.Models.Booru; namespace Nuuru.Server.Services { public interface IPostHistoryService { // Recording Task RecordTagChangeAsync(int postId, Guid userId, string? userIp, IEnumerable tags); Task RecordSourceChangeAsync(int postId, Guid userId, string? userIp, string? source); // Retrieval Task> GetTagHistoryAsync(int postId, bool includeSuppressed); Task> GetSourceHistoryAsync(int postId, bool includeSuppressed); Task GetTagHistoryByIdAsync(int historyId); Task GetSourceHistoryByIdAsync(int historyId); // Suppression Task SuppressTagHistoryAsync(int historyId, Guid moderatorId, string? reason); Task UnsuppressTagHistoryAsync(int historyId); Task SuppressSourceHistoryAsync(int historyId, Guid moderatorId, string? reason); Task UnsuppressSourceHistoryAsync(int historyId); // Revert helpers (returns data for controller to coordinate) Task GetTagsFromHistoryAsync(int historyId); Task<(int postId, string? source)?> GetSourceFromHistoryAsync(int historyId); Task RevertSourceDirectAsync(int historyId, Guid userId, string? userIp); } public class PostHistoryService : IPostHistoryService { private readonly ApplicationDbContext _context; private readonly ILogger _logger; public PostHistoryService( ApplicationDbContext context, ILogger logger) { _context = context; _logger = logger; } public async Task RecordTagChangeAsync(int postId, Guid userId, string? userIp, IEnumerable tags) { var tagList = tags.ToList(); var tagString = string.Join(" ", tagList.OrderBy(t => t)); var history = new TagHistory { PostId = postId, UserId = userId, UserIp = userIp, Tags = tagString, DateSet = DateTime.UtcNow }; _context.TagHistories.Add(history); await _context.SaveChangesAsync(); _logger.LogInformation("Recorded tag history for post {PostId} by user {UserId}: {TagCount} tags", postId, userId, tagList.Count); } public async Task RecordSourceChangeAsync(int postId, Guid userId, string? userIp, string? source) { var history = new SourceHistory { PostId = postId, UserId = userId, UserIp = userIp, Source = source, DateSet = DateTime.UtcNow }; _context.SourceHistories.Add(history); await _context.SaveChangesAsync(); _logger.LogInformation("Recorded source history for post {PostId} by user {UserId}", postId, userId); } public async Task> GetTagHistoryAsync(int postId, bool includeSuppressed) { var query = _context.TagHistories .Include(h => h.User) .Include(h => h.SuppressedBy) .Where(h => h.PostId == postId); if (!includeSuppressed) { query = query.Where(h => h.SuppressedAt == null); } return await query .OrderByDescending(h => h.DateSet) .ToListAsync(); } public async Task> GetSourceHistoryAsync(int postId, bool includeSuppressed) { var query = _context.SourceHistories .Include(h => h.User) .Include(h => h.SuppressedBy) .Where(h => h.PostId == postId); if (!includeSuppressed) { query = query.Where(h => h.SuppressedAt == null); } return await query .OrderByDescending(h => h.DateSet) .ToListAsync(); } public async Task GetTagHistoryByIdAsync(int historyId) { return await _context.TagHistories .Include(h => h.User) .Include(h => h.Post) .FirstOrDefaultAsync(h => h.Id == historyId); } public async Task GetSourceHistoryByIdAsync(int historyId) { return await _context.SourceHistories .Include(h => h.User) .Include(h => h.Post) .FirstOrDefaultAsync(h => h.Id == historyId); } public async Task SuppressTagHistoryAsync(int historyId, Guid moderatorId, string? reason) { var history = await _context.TagHistories.FindAsync(historyId); if (history == null) { return false; } history.SuppressedAt = DateTime.UtcNow; history.SuppressedById = moderatorId; history.SuppressionReason = reason; await _context.SaveChangesAsync(); _logger.LogInformation("Tag history {HistoryId} suppressed by moderator {ModeratorId}", historyId, moderatorId); return true; } public async Task UnsuppressTagHistoryAsync(int historyId) { var history = await _context.TagHistories.FindAsync(historyId); if (history == null) { return false; } history.SuppressedAt = null; history.SuppressedById = null; history.SuppressionReason = null; await _context.SaveChangesAsync(); _logger.LogInformation("Tag history {HistoryId} unsuppressed", historyId); return true; } public async Task SuppressSourceHistoryAsync(int historyId, Guid moderatorId, string? reason) { var history = await _context.SourceHistories.FindAsync(historyId); if (history == null) { return false; } history.SuppressedAt = DateTime.UtcNow; history.SuppressedById = moderatorId; history.SuppressionReason = reason; await _context.SaveChangesAsync(); _logger.LogInformation("Source history {HistoryId} suppressed by moderator {ModeratorId}", historyId, moderatorId); return true; } public async Task UnsuppressSourceHistoryAsync(int historyId) { var history = await _context.SourceHistories.FindAsync(historyId); if (history == null) { return false; } history.SuppressedAt = null; history.SuppressedById = null; history.SuppressionReason = null; await _context.SaveChangesAsync(); _logger.LogInformation("Source history {HistoryId} unsuppressed", historyId); return true; } public async Task GetTagsFromHistoryAsync(int historyId) { var history = await _context.TagHistories.FindAsync(historyId); if (history == null) { return null; } return history.Tags.Split(' ', StringSplitOptions.RemoveEmptyEntries); } public async Task<(int postId, string? source)?> GetSourceFromHistoryAsync(int historyId) { var history = await _context.SourceHistories.FindAsync(historyId); if (history == null) { return null; } return (history.PostId, history.Source); } public async Task RevertSourceDirectAsync(int historyId, Guid userId, string? userIp) { var history = await _context.SourceHistories .Include(h => h.Post) .FirstOrDefaultAsync(h => h.Id == historyId); if (history == null) { return false; } // Skip if source hasn't changed if (history.Post.Source == history.Source) { return true; } // Update the post's source directly history.Post.Source = history.Source; await _context.SaveChangesAsync(); // Record this revert as a new history entry await RecordSourceChangeAsync(history.PostId, userId, userIp, history.Source); _logger.LogInformation("Source for post {PostId} reverted to history entry {HistoryId} by user {UserId}", history.PostId, historyId, userId); return true; } } }