using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Nuuru.Server.Auth; using Nuuru.Server.DTOs.Admin; using Nuuru.Server.Extensions; using Nuuru.Server.Services; namespace Nuuru.Server.Controllers { [ApiController] [Route("api/booru/tag-relations")] public class TagRelationController : ControllerBase { private readonly ITagRelationService _tagRelationService; private readonly IBulkTagService _bulkTagService; private readonly IBulkPostService _bulkPostService; private readonly ILogger _logger; public TagRelationController( ITagRelationService tagRelationService, IBulkTagService bulkTagService, IBulkPostService bulkPostService, ILogger logger) { _tagRelationService = tagRelationService; _bulkTagService = bulkTagService; _bulkPostService = bulkPostService; _logger = logger; } #region Aliases [HttpGet("aliases")] public async Task GetTagAliases([FromQuery] int page = 1, [FromQuery] int pageSize = 50) { var result = await _tagRelationService.GetAllAliasesAsync(page, pageSize); return Ok(result); } [HttpGet("aliases/search")] public async Task SearchTagAliases([FromQuery] string query, [FromQuery] int limit = 20) { if (string.IsNullOrWhiteSpace(query)) return BadRequest(new { error = "Search query cannot be empty" }); var result = await _tagRelationService.SearchAliasesAsync(query, limit); return Ok(result); } [HttpGet("aliases/{id}")] public async Task GetTagAlias(Guid id) { var result = await _tagRelationService.GetAliasAsync(id); return result != null ? Ok(result) : NotFound(); } [HttpPost("aliases")] [Authorize(Policy = Permissions.Admin.SystemSettings)] public async Task CreateTagAlias([FromBody] CreateTagAliasRequest request) { try { if (request.AliasTagName.Equals(request.TargetTagName, StringComparison.OrdinalIgnoreCase)) return BadRequest(new { error = "A tag cannot alias to itself" }); var alias = await _tagRelationService.CreateAliasAsync(request, User.GetUserId()); return CreatedAtAction(nameof(GetTagAlias), new { id = alias.Id }, alias); } catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); } catch (Exception ex) { return BadRequest(new { error = ex.Message }); } } [HttpDelete("aliases/{id}")] [Authorize(Policy = Permissions.Admin.SystemSettings)] public async Task DeleteTagAlias(Guid id) { var success = await _tagRelationService.DeleteAliasAsync(id); return success ? Ok(new { message = "Alias deleted" }) : NotFound(); } #endregion #region Implications [HttpGet("implications")] public async Task GetTagImplications([FromQuery] int page = 1, [FromQuery] int pageSize = 50) { var result = await _tagRelationService.GetAllImplicationsAsync(page, pageSize); return Ok(result); } [HttpGet("implications/search")] public async Task SearchTagImplications([FromQuery] string query, [FromQuery] int limit = 20) { if (string.IsNullOrWhiteSpace(query)) return BadRequest(new { error = "Search query cannot be empty" }); var result = await _tagRelationService.SearchImplicationsAsync(query, limit); return Ok(result); } [HttpGet("implications/{id}")] public async Task GetTagImplication(Guid id) { var result = await _tagRelationService.GetImplicationAsync(id); return result != null ? Ok(result) : NotFound(); } [HttpPost("implications")] [Authorize(Policy = Permissions.Admin.SystemSettings)] public async Task CreateTagImplication([FromBody] CreateTagImplicationRequest request) { try { if (request.AntecedentTagName.Equals(request.ConsequentTagName, StringComparison.OrdinalIgnoreCase)) return BadRequest(new { error = "A tag cannot imply itself" }); var implication = await _tagRelationService.CreateImplicationAsync(request, User.GetUserId()); return CreatedAtAction(nameof(GetTagImplication), new { id = implication.Id }, implication); } catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); } catch (Exception ex) { return BadRequest(new { error = ex.Message }); } } [HttpDelete("implications/{id}")] [Authorize(Policy = Permissions.Admin.SystemSettings)] public async Task DeleteTagImplication(Guid id) { var success = await _tagRelationService.DeleteImplicationAsync(id); return success ? Ok(new { message = "Implication deleted" }) : NotFound(); } #endregion #region Bulk Operations [HttpPost("bulk/apply-aliases")] [Authorize(Policy = Permissions.Admin.BulkOperations)] public async Task ApplyAllAliases() { try { _logger.LogInformation("Bulk apply all aliases initiated by user {UserId}", User.GetUserId()); var result = await _bulkTagService.ApplyAllAliasesAsync(); return Ok(result); } catch (Exception ex) { _logger.LogError(ex, "Error in bulk apply aliases"); return StatusCode(500, new { error = "Failed to apply aliases" }); } } [HttpPost("bulk/apply-implications")] [Authorize(Policy = Permissions.Admin.BulkOperations)] public async Task ApplyAllImplications() { try { _logger.LogInformation("Bulk apply all implications initiated by user {UserId}", User.GetUserId()); var result = await _bulkTagService.ApplyAllImplicationsAsync(); return Ok(result); } catch (Exception ex) { _logger.LogError(ex, "Error in bulk apply implications"); return StatusCode(500, new { error = "Failed to apply implications" }); } } [HttpPost("bulk/rename-tag")] [Authorize(Policy = Permissions.Admin.BulkOperations)] public async Task RenameTag([FromBody] MassTagRenameRequest request) { try { _logger.LogInformation("Bulk rename tag '{CurrentName}' to '{NewName}' (Selective: {IsSelective}, Query: {Query}, Excluded: {ExcludedCount}) by user {UserId}", request.CurrentName, request.NewName, request.PostIds?.Count > 0, request.SearchQuery, request.ExcludedPostIds?.Count ?? 0, User.GetUserId()); var result = await _bulkTagService.RenameTagAsync(request.CurrentName, request.NewName, request.PostIds, request.SearchQuery, request.ExcludedPostIds); return result.Success ? Ok(result) : BadRequest(result); } catch (Exception ex) { _logger.LogError(ex, "Error in bulk rename tag"); return StatusCode(500, new { error = "Failed to rename tag" }); } } [HttpPost("bulk/delete-tag")] [Authorize(Policy = Permissions.Admin.BulkOperations)] public async Task DeleteTag([FromBody] MassTagDeleteRequest request) { try { _logger.LogInformation("Bulk delete tag '{TagName}' (Selective: {IsSelective}, Query: {Query}, Excluded: {ExcludedCount}) by user {UserId}", request.TagName, request.PostIds?.Count > 0, request.SearchQuery, request.ExcludedPostIds?.Count ?? 0, User.GetUserId()); var result = await _bulkTagService.DeleteTagAsync(request.TagName, request.PostIds, request.SearchQuery, request.ExcludedPostIds); return result.Success ? Ok(result) : BadRequest(result); } catch (Exception ex) { _logger.LogError(ex, "Error in bulk delete tag"); return StatusCode(500, new { error = "Failed to delete tag" }); } } [HttpPost("bulk/merge-tags")] [Authorize(Policy = Permissions.Admin.BulkOperations)] public async Task MergeTags([FromBody] MassTagMergeRequest request) { try { _logger.LogInformation("Bulk merge tag '{SourceTagName}' into '{TargetTagName}' (Selective: {IsSelective}, Query: {Query}, Excluded: {ExcludedCount}) by user {UserId}", request.SourceTagName, request.TargetTagName, request.PostIds?.Count > 0, request.SearchQuery, request.ExcludedPostIds?.Count ?? 0, User.GetUserId()); var result = await _bulkTagService.MergeTagsAsync(request.SourceTagName, request.TargetTagName, request.PostIds, request.SearchQuery, request.ExcludedPostIds); return result.Success ? Ok(result) : BadRequest(result); } catch (Exception ex) { _logger.LogError(ex, "Error in bulk merge tags"); return StatusCode(500, new { error = "Failed to merge tags" }); } } [HttpPost("bulk/add-tag")] [Authorize(Policy = Permissions.Admin.BulkOperations)] public async Task AddTag([FromBody] MassTagAddRequest request) { try { _logger.LogInformation("Bulk add tag '{TagName}' (Selective: {IsSelective}, Query: {Query}, Excluded: {ExcludedCount}) by user {UserId}", request.TagName, request.PostIds?.Count > 0, request.SearchQuery, request.ExcludedPostIds?.Count ?? 0, User.GetUserId()); var result = await _bulkTagService.AddTagAsync(request.TagName, request.PostIds, request.SearchQuery, request.ExcludedPostIds); return result.Success ? Ok(result) : BadRequest(result); } catch (Exception ex) { _logger.LogError(ex, "Error in bulk add tag"); return StatusCode(500, new { error = "Failed to add tag" }); } } [HttpPost("bulk/set-rating")] [Authorize(Policy = Permissions.Admin.BulkOperations)] public async Task SetRating([FromBody] MassTagRatingRequest request) { try { _logger.LogInformation("Bulk set rating '{Rating}' (Selective: {IsSelective}, Query: {Query}, Excluded: {ExcludedCount}) by user {UserId}", request.Rating, request.PostIds?.Count > 0, request.SearchQuery, request.ExcludedPostIds?.Count ?? 0, User.GetUserId()); var result = await _bulkTagService.SetRatingAsync(request.Rating, request.PostIds, request.SearchQuery, request.ExcludedPostIds); return result.Success ? Ok(result) : BadRequest(result); } catch (Exception ex) { _logger.LogError(ex, "Error in bulk set rating"); return StatusCode(500, new { error = "Failed to set rating" }); } } [HttpPost("bulk/update-selection")] [Authorize(Policy = Permissions.Admin.BulkOperations)] public async Task BulkUpdateSelection([FromBody] MassTagUpdateRequest request) { try { _logger.LogInformation("Bulk update initiated for {Count} posts by user {UserId}", request.PostIds?.Count ?? 0, User.GetUserId()); var result = await _bulkTagService.MassUpdateTagsAsync(request.PostIds ?? new List(), request.TagsToAdd, request.TagsToRemove); return result.Success ? Ok(result) : BadRequest(result); } catch (Exception ex) { _logger.LogError(ex, "Error in bulk update selection"); return StatusCode(500, new { error = "Failed to update selection" }); } } #endregion } }