using Microsoft.EntityFrameworkCore; using Nuuru.Server.Data; using Nuuru.Server.DTOs.Admin; using Nuuru.Server.Models.Forum; namespace Nuuru.Server.Services { public interface IForumCategoryService { Task> GetAllCategoriesAsync(); Task> GetVisibleCategoriesAsync(Guid? userId); Task GetCategoryBySlugAsync(string slug); Task CanAccessCategoryAsync(Guid categoryId, Guid? userId); Task GetClanCategoryForUserAsync(Guid userId); Task GetCategoryByIdAsync(Guid id); Task<(int threadCount, int postCount)> GetCategoryStatsAsync(Guid categoryId); Task GetLatestThreadAsync(Guid categoryId); Task CreateCategoryAsync(CreateForumCategoryRequest request); Task UpdateCategoryAsync(Guid id, UpdateForumCategoryRequest request); Task DeleteCategoryAsync(Guid id); Task SlugExistsAsync(string slug, Guid? excludeId = null); } public class ForumCategoryService : IForumCategoryService { private readonly ApplicationDbContext _context; private readonly ISiteSettingsService _siteSettings; private readonly ILogger _logger; public ForumCategoryService( ApplicationDbContext context, ISiteSettingsService siteSettings, ILogger logger) { _context = context; _siteSettings = siteSettings; _logger = logger; } public async Task> GetAllCategoriesAsync() { return await _context.ForumCategories .OrderBy(c => c.DisplayOrder) .ToListAsync(); } public async Task> GetVisibleCategoriesAsync(Guid? userId) { var all = await _context.ForumCategories .OrderBy(c => c.DisplayOrder) .ToListAsync(); // Get all clan-private category IDs var clanCategoryIds = await _context.ClanForumCategories .Select(c => c.ForumCategoryId) .ToHashSetAsync(); if (clanCategoryIds.Count == 0) return all; // Hide all clan categories when clans are disabled var clansEnabled = await _siteSettings.GetBoolAsync("boints.enabled", false) && await _siteSettings.GetBoolAsync("clans.enabled", false); if (!clansEnabled) return all.Where(c => !clanCategoryIds.Contains(c.Id)).ToList(); // Get the user's clan category IDs (if any) var userClanCategoryIds = new HashSet(); if (userId.HasValue) { var userClanId = await _context.ClanMembers .Where(m => m.UserId == userId.Value) .Select(m => m.ClanId) .FirstOrDefaultAsync(); if (userClanId != 0) { userClanCategoryIds = await _context.ClanForumCategories .Where(c => c.ClanId == userClanId) .Select(c => c.ForumCategoryId) .ToHashSetAsync(); } } // Filter: show non-clan categories + clan categories the user has access to // Override clan category slugs to "clan" so frontend always uses /forum/clan return all .Where(c => !clanCategoryIds.Contains(c.Id) || userClanCategoryIds.Contains(c.Id)) .Select(c => { if (userClanCategoryIds.Contains(c.Id)) c.Slug = "clan"; return c; }) .ToList(); } public async Task CanAccessCategoryAsync(Guid categoryId, Guid? userId) { var clanCategory = await _context.ClanForumCategories .FirstOrDefaultAsync(c => c.ForumCategoryId == categoryId); // Not a clan category — everyone can access if (clanCategory == null) return true; // Clan forums inaccessible when clans disabled var clansEnabled = await _siteSettings.GetBoolAsync("boints.enabled", false) && await _siteSettings.GetBoolAsync("clans.enabled", false); if (!clansEnabled) return false; // Clan category — only members if (!userId.HasValue) return false; return await _context.ClanMembers .AnyAsync(m => m.ClanId == clanCategory.ClanId && m.UserId == userId.Value); } public async Task GetCategoryBySlugAsync(string slug) { return await _context.ForumCategories .FirstOrDefaultAsync(c => c.Slug == slug.ToLower()); } public async Task GetClanCategoryForUserAsync(Guid userId) { var clansEnabled = await _siteSettings.GetBoolAsync("boints.enabled", false) && await _siteSettings.GetBoolAsync("clans.enabled", false); if (!clansEnabled) return null; var clanId = await _context.ClanMembers .Where(m => m.UserId == userId) .Select(m => m.ClanId) .FirstOrDefaultAsync(); if (clanId == 0) return null; var clanCategory = await _context.ClanForumCategories .Where(c => c.ClanId == clanId) .Select(c => c.ForumCategoryId) .FirstOrDefaultAsync(); if (clanCategory == default) return null; return await _context.ForumCategories.FindAsync(clanCategory); } public async Task GetCategoryByIdAsync(Guid id) { return await _context.ForumCategories.FindAsync(id); } public async Task<(int threadCount, int postCount)> GetCategoryStatsAsync(Guid categoryId) { var threadCount = await _context.ForumThreads .CountAsync(t => t.CategoryId == categoryId); var postCount = await _context.ForumPosts .CountAsync(p => p.Thread.CategoryId == categoryId); return (threadCount, postCount); } public async Task GetLatestThreadAsync(Guid categoryId) { return await _context.ForumThreads .Include(t => t.Author) .Include(t => t.LastPost) .ThenInclude(p => p.Author) .Where(t => t.CategoryId == categoryId) .OrderByDescending(t => t.LastPostAt) .AsSplitQuery() .FirstOrDefaultAsync(); } public async Task CreateCategoryAsync(CreateForumCategoryRequest request) { var slug = request.Slug.ToLowerInvariant().Trim(); if (await SlugExistsAsync(slug)) { throw new InvalidOperationException($"A forum category with slug '{slug}' already exists"); } var category = new ForumCategory { Name = request.Name.Trim(), Slug = slug, Description = request.Description, DisplayOrder = request.DisplayOrder, Color = request.Color }; _context.ForumCategories.Add(category); await _context.SaveChangesAsync(); _logger.LogInformation("Created forum category {Name} with slug {Slug}", category.Name, category.Slug); return category; } public async Task UpdateCategoryAsync(Guid id, UpdateForumCategoryRequest request) { var category = await _context.ForumCategories.FindAsync(id); if (category == null) return null; category.Name = request.Name.Trim(); category.Description = request.Description; category.DisplayOrder = request.DisplayOrder; category.Color = request.Color; await _context.SaveChangesAsync(); _logger.LogInformation("Updated forum category {Id} ({Name})", id, category.Name); return category; } public async Task DeleteCategoryAsync(Guid id) { var category = await _context.ForumCategories .Include(c => c.Threads) .FirstOrDefaultAsync(c => c.Id == id); if (category == null) return false; if (category.Threads.Any()) { throw new InvalidOperationException($"Cannot delete category '{category.Name}' because it has {category.Threads.Count} threads"); } _context.ForumCategories.Remove(category); await _context.SaveChangesAsync(); _logger.LogInformation("Deleted forum category {Id} ({Name})", id, category.Name); return true; } public async Task SlugExistsAsync(string slug, Guid? excludeId = null) { var query = _context.ForumCategories.Where(c => c.Slug == slug.ToLowerInvariant()); if (excludeId.HasValue) { query = query.Where(c => c.Id != excludeId.Value); } return await query.AnyAsync(); } } }