using Microsoft.EntityFrameworkCore; using Nuuru.Server.Data; using Nuuru.Server.DTOs.Admin; using Nuuru.Server.Models.Booru; namespace Nuuru.Server.Services { public interface ITagCategoryService { Task> GetAllCategoriesAsync(); Task GetCategoryByIdAsync(Guid id); Task GetCategoryBySlugAsync(string slug); Task CreateCategoryAsync(CreateTagCategoryRequest request); Task UpdateCategoryAsync(Guid id, UpdateTagCategoryRequest request); Task DeleteCategoryAsync(Guid id); Task SlugExistsAsync(string slug, Guid? excludeId = null); } public class TagCategoryService : ITagCategoryService { private readonly ApplicationDbContext _context; private readonly ILogger _logger; public TagCategoryService(ApplicationDbContext context, ILogger logger) { _context = context; _logger = logger; } public async Task> GetAllCategoriesAsync() { var categories = await _context.BooruTagCategories .Include(c => c.Tags) .OrderBy(c => c.SortOrder) .ThenBy(c => c.Name) .ToListAsync(); return categories.Select(MapToDto); } public async Task GetCategoryByIdAsync(Guid id) { var category = await _context.BooruTagCategories .Include(c => c.Tags) .FirstOrDefaultAsync(c => c.Id == id); return category != null ? MapToDto(category) : null; } public async Task GetCategoryBySlugAsync(string slug) { var category = await _context.BooruTagCategories .Include(c => c.Tags) .FirstOrDefaultAsync(c => c.Slug == slug.ToLowerInvariant()); return category != null ? MapToDto(category) : null; } public async Task CreateCategoryAsync(CreateTagCategoryRequest request) { var slug = request.Slug.ToLowerInvariant().Trim(); if (await SlugExistsAsync(slug)) { throw new InvalidOperationException($"A category with slug '{slug}' already exists"); } var category = new TagCategory { Name = request.Name.Trim(), Slug = slug, ColorHex = request.ColorHex, SortOrder = request.SortOrder, MaxPerPost = request.MaxPerPost, ParentCategoryId = request.ParentCategoryId, IsActive = true }; _context.BooruTagCategories.Add(category); await _context.SaveChangesAsync(); _logger.LogInformation("Created tag category {Name} with slug {Slug}", category.Name, category.Slug); return MapToDto(category); } public async Task UpdateCategoryAsync(Guid id, UpdateTagCategoryRequest request) { var category = await _context.BooruTagCategories .Include(c => c.Tags) .FirstOrDefaultAsync(c => c.Id == id); if (category == null) return null; category.Name = request.Name.Trim(); category.ColorHex = request.ColorHex; category.SortOrder = request.SortOrder; category.MaxPerPost = request.MaxPerPost; category.IsActive = request.IsActive; category.ParentCategoryId = request.ParentCategoryId; await _context.SaveChangesAsync(); _logger.LogInformation("Updated tag category {Id} ({Name})", id, category.Name); return MapToDto(category); } public async Task DeleteCategoryAsync(Guid id) { var category = await _context.BooruTagCategories .Include(c => c.Tags) .FirstOrDefaultAsync(c => c.Id == id); if (category == null) return false; if (category.Tags.Any()) { throw new InvalidOperationException($"Cannot delete category '{category.Name}' because it has {category.Tags.Count} tags"); } _context.BooruTagCategories.Remove(category); await _context.SaveChangesAsync(); _logger.LogInformation("Deleted tag category {Id} ({Name})", id, category.Name); return true; } public async Task SlugExistsAsync(string slug, Guid? excludeId = null) { var query = _context.BooruTagCategories.Where(c => c.Slug == slug.ToLowerInvariant()); if (excludeId.HasValue) { query = query.Where(c => c.Id != excludeId.Value); } return await query.AnyAsync(); } private static TagCategoryDto MapToDto(TagCategory category) { return new TagCategoryDto { Id = category.Id, Name = category.Name, Slug = category.Slug, ColorHex = category.ColorHex, SortOrder = category.SortOrder, MaxPerPost = category.MaxPerPost, IsActive = category.IsActive, TagCount = category.Tags?.Count ?? 0, ParentCategoryId = category.ParentCategoryId }; } } }