using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Nuuru.Server.Data; using Nuuru.Server.Models; namespace Nuuru.Server.Services { public interface IBanAppealService { Task CreateAppealAsync(Guid userId, Guid banId, string reason); Task HasPendingAppealAsync(Guid banId); Task<(IEnumerable Items, int TotalCount)> GetAppealsAsync(BanAppealStatus? status, int page, int pageSize); Task AcceptAppealAsync(Guid appealId, ApplicationUser moderator, string? note); Task RejectAppealAsync(Guid appealId, ApplicationUser moderator, string? note, bool denyFurtherAppeals = false); Task> GetUserAppealsAsync(Guid userId); Task GetPendingAppealCountAsync(); } public class BanAppealService : IBanAppealService { private readonly ApplicationDbContext _context; private readonly IBanService _banService; private readonly ILogger _logger; public BanAppealService( ApplicationDbContext context, IBanService banService, ILogger logger) { _context = context; _banService = banService; _logger = logger; } public async Task CreateAppealAsync(Guid userId, Guid banId, string reason) { var ban = await _context.Bans .Include(b => b.User) .FirstOrDefaultAsync(b => b.Id == banId); if (ban == null) throw new ArgumentException("Ban not found."); if (ban.User.Id != userId) throw new UnauthorizedAccessException("You can only appeal your own bans."); if (!ban.IsBanActive()) throw new InvalidOperationException("This ban is no longer active."); if (ban.AppealsDenied) throw new InvalidOperationException("Appeals have been permanently denied for this ban."); var hasPending = await HasPendingAppealAsync(banId); if (hasPending) throw new InvalidOperationException("You already have a pending appeal for this ban."); var appeal = new BanAppeal { BanId = banId, UserId = userId, Reason = reason, Status = BanAppealStatus.Pending, CreatedAt = DateTime.UtcNow }; _context.BanAppeals.Add(appeal); await _context.SaveChangesAsync(); _logger.LogInformation("Ban appeal created by user {UserId} for ban {BanId}", userId, banId); return appeal; } public async Task HasPendingAppealAsync(Guid banId) { return await _context.BanAppeals .AnyAsync(a => a.BanId == banId && a.Status == BanAppealStatus.Pending); } public async Task<(IEnumerable Items, int TotalCount)> GetAppealsAsync(BanAppealStatus? status, int page, int pageSize) { page = Math.Max(1, page); pageSize = Math.Clamp(pageSize, 1, 100); var query = _context.BanAppeals .Include(a => a.User) .Include(a => a.Moderator) .Include(a => a.Ban) .ThenInclude(b => b.User) .AsQueryable(); if (status.HasValue) { query = query.Where(a => a.Status == status.Value); } var totalCount = await query.CountAsync(); var items = await query .OrderByDescending(a => a.CreatedAt) .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(); return (items, totalCount); } public async Task AcceptAppealAsync(Guid appealId, ApplicationUser moderator, string? note) { var appeal = await _context.BanAppeals .Include(a => a.Ban) .ThenInclude(b => b.User) .Include(a => a.User) .FirstOrDefaultAsync(a => a.Id == appealId); if (appeal == null) return null; if (appeal.Status != BanAppealStatus.Pending) throw new InvalidOperationException("This appeal has already been resolved."); if (appeal.UserId == moderator.Id) throw new UnauthorizedAccessException("You cannot resolve your own ban appeal."); appeal.Status = BanAppealStatus.Accepted; appeal.ResolvedAt = DateTime.UtcNow; appeal.ModeratorNote = note; appeal.ModeratorId = moderator.Id; // Lift the ban (also saves the appeal changes via shared DbContext) await _banService.UnbanUserAsync(appeal.Ban.User.Id, appeal.Ban.Zone); _logger.LogInformation("Ban appeal {AppealId} accepted by moderator {ModeratorId}, ban {BanId} lifted", appealId, moderator.Id, appeal.BanId); return appeal; } public async Task RejectAppealAsync(Guid appealId, ApplicationUser moderator, string? note, bool denyFurtherAppeals = false) { var appeal = await _context.BanAppeals .Include(a => a.Ban) .ThenInclude(b => b.User) .Include(a => a.User) .FirstOrDefaultAsync(a => a.Id == appealId); if (appeal == null) return null; if (appeal.Status != BanAppealStatus.Pending) throw new InvalidOperationException("This appeal has already been resolved."); if (appeal.UserId == moderator.Id) throw new UnauthorizedAccessException("You cannot resolve your own ban appeal."); appeal.Status = BanAppealStatus.Rejected; appeal.ResolvedAt = DateTime.UtcNow; appeal.ModeratorNote = note; appeal.ModeratorId = moderator.Id; if (denyFurtherAppeals) { appeal.Ban.AppealsDenied = true; } await _context.SaveChangesAsync(); _logger.LogInformation("Ban appeal {AppealId} rejected by moderator {ModeratorId}{DenyNote}", appealId, moderator.Id, denyFurtherAppeals ? " (further appeals denied)" : ""); return appeal; } public async Task> GetUserAppealsAsync(Guid userId) { return await _context.BanAppeals .Include(a => a.Ban) .ThenInclude(b => b.User) .Include(a => a.User) .Include(a => a.Moderator) .Where(a => a.UserId == userId) .OrderByDescending(a => a.CreatedAt) .ToListAsync(); } public async Task GetPendingAppealCountAsync() { return await _context.BanAppeals .CountAsync(a => a.Status == BanAppealStatus.Pending); } } }