using Cleanuparr.Api.Extensions; using Cleanuparr.Api.Features.QueueCleaner.Contracts.Requests; using Cleanuparr.Infrastructure.Services.Interfaces; using Cleanuparr.Persistence; using Cleanuparr.Persistence.Models.Configuration.QueueCleaner; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace Cleanuparr.Api.Features.QueueCleaner.Controllers; [ApiController] [Route("api/queue-rules")] [Authorize] public class QueueRulesController : ControllerBase { private readonly ILogger _logger; private readonly DataContext _dataContext; private readonly IRuleIntervalValidator _ruleIntervalValidator; public QueueRulesController( ILogger logger, DataContext dataContext, IRuleIntervalValidator ruleIntervalValidator) { _logger = logger; _dataContext = dataContext; _ruleIntervalValidator = ruleIntervalValidator; } [HttpGet("stall")] public async Task GetStallRules() { await DataContext.Lock.WaitAsync(); try { var rules = await _dataContext.StallRules .OrderBy(r => r.MinCompletionPercentage) .ThenBy(r => r.Name) .AsNoTracking() .ToListAsync(); return Ok(rules); } finally { DataContext.Lock.Release(); } } [HttpPost("stall")] public async Task CreateStallRule([FromBody] StallRuleDto ruleDto) { await DataContext.Lock.WaitAsync(); try { var queueCleanerConfig = await _dataContext.QueueCleanerConfigs .FirstAsync(); var existingRule = await _dataContext.StallRules .FirstOrDefaultAsync(r => r.Name.ToLower() == ruleDto.Name.ToLower()); if (existingRule != null) { return this.ProblemResult(StatusCodes.Status400BadRequest, "A stall rule with this name already exists"); } var rule = new StallRule { Id = Guid.NewGuid(), QueueCleanerConfigId = queueCleanerConfig.Id, Name = ruleDto.Name.Trim(), Enabled = ruleDto.Enabled, MaxStrikes = ruleDto.MaxStrikes, PrivacyType = ruleDto.PrivacyType, MinCompletionPercentage = ruleDto.MinCompletionPercentage, MaxCompletionPercentage = ruleDto.MaxCompletionPercentage, ResetStrikesOnProgress = ruleDto.ResetStrikesOnProgress, DeletePrivateTorrentsFromClient = ruleDto.DeletePrivateTorrentsFromClient, ChangeCategory = ruleDto.ChangeCategory, MinimumProgress = ruleDto.MinimumProgress?.Trim(), }; var existingRules = await _dataContext.StallRules.ToListAsync(); var intervalValidationResult = _ruleIntervalValidator.ValidateStallRuleIntervals(rule, existingRules); if (!intervalValidationResult.IsValid) { return this.ProblemResult(StatusCodes.Status400BadRequest, intervalValidationResult.ErrorMessage); } rule.Validate(); _dataContext.StallRules.Add(rule); await _dataContext.SaveChangesAsync(); _logger.LogInformation("Created stall rule: {RuleName} with ID: {RuleId}", rule.Name, rule.Id); return CreatedAtAction(nameof(GetStallRules), new { id = rule.Id }, rule); } finally { DataContext.Lock.Release(); } } [HttpPut("stall/{id}")] public async Task UpdateStallRule(Guid id, [FromBody] StallRuleDto ruleDto) { await DataContext.Lock.WaitAsync(); try { var existingRule = await _dataContext.StallRules .FirstOrDefaultAsync(r => r.Id == id); if (existingRule == null) { return this.ProblemResult(StatusCodes.Status404NotFound, $"Stall rule with ID {id} not found"); } var duplicateRule = await _dataContext.StallRules .FirstOrDefaultAsync(r => r.Id != id && r.Name.ToLower() == ruleDto.Name.ToLower()); if (duplicateRule != null) { return this.ProblemResult(StatusCodes.Status400BadRequest, "A stall rule with this name already exists"); } var updatedRule = existingRule with { Name = ruleDto.Name.Trim(), Enabled = ruleDto.Enabled, MaxStrikes = ruleDto.MaxStrikes, PrivacyType = ruleDto.PrivacyType, MinCompletionPercentage = ruleDto.MinCompletionPercentage, MaxCompletionPercentage = ruleDto.MaxCompletionPercentage, ResetStrikesOnProgress = ruleDto.ResetStrikesOnProgress, DeletePrivateTorrentsFromClient = ruleDto.DeletePrivateTorrentsFromClient, ChangeCategory = ruleDto.ChangeCategory, MinimumProgress = ruleDto.MinimumProgress?.Trim(), }; var existingRules = await _dataContext.StallRules .Where(r => r.Id != id) .ToListAsync(); var intervalValidationResult = _ruleIntervalValidator.ValidateStallRuleIntervals(updatedRule, existingRules); if (!intervalValidationResult.IsValid) { return this.ProblemResult(StatusCodes.Status400BadRequest, intervalValidationResult.ErrorMessage); } updatedRule.Validate(); _dataContext.Entry(existingRule).CurrentValues.SetValues(updatedRule); await _dataContext.SaveChangesAsync(); _logger.LogInformation("Updated stall rule: {RuleName} with ID: {RuleId}", updatedRule.Name, id); return Ok(updatedRule); } finally { DataContext.Lock.Release(); } } [HttpDelete("stall/{id}")] public async Task DeleteStallRule(Guid id) { await DataContext.Lock.WaitAsync(); try { var existingRule = await _dataContext.StallRules .FirstOrDefaultAsync(r => r.Id == id); if (existingRule == null) { return this.ProblemResult(StatusCodes.Status404NotFound, $"Stall rule with ID {id} not found"); } _dataContext.StallRules.Remove(existingRule); await _dataContext.SaveChangesAsync(); _logger.LogInformation("Deleted stall rule: {RuleName} with ID: {RuleId}", existingRule.Name, id); return NoContent(); } finally { DataContext.Lock.Release(); } } [HttpGet("slow")] public async Task GetSlowRules() { await DataContext.Lock.WaitAsync(); try { var rules = await _dataContext.SlowRules .OrderBy(r => r.MinCompletionPercentage) .ThenBy(r => r.Name) .AsNoTracking() .ToListAsync(); return Ok(rules); } finally { DataContext.Lock.Release(); } } [HttpPost("slow")] public async Task CreateSlowRule([FromBody] SlowRuleDto ruleDto) { await DataContext.Lock.WaitAsync(); try { var queueCleanerConfig = await _dataContext.QueueCleanerConfigs .FirstAsync(); var existingRule = await _dataContext.SlowRules .FirstOrDefaultAsync(r => r.Name.ToLower() == ruleDto.Name.ToLower()); if (existingRule != null) { return this.ProblemResult(StatusCodes.Status400BadRequest, "A slow rule with this name already exists"); } var rule = new SlowRule { Id = Guid.NewGuid(), QueueCleanerConfigId = queueCleanerConfig.Id, Name = ruleDto.Name.Trim(), Enabled = ruleDto.Enabled, MaxStrikes = ruleDto.MaxStrikes, PrivacyType = ruleDto.PrivacyType, MinCompletionPercentage = ruleDto.MinCompletionPercentage, MaxCompletionPercentage = ruleDto.MaxCompletionPercentage, ResetStrikesOnProgress = ruleDto.ResetStrikesOnProgress, MinSpeed = ruleDto.MinSpeed?.Trim() ?? string.Empty, MaxTimeHours = ruleDto.MaxTimeHours, IgnoreAboveSize = ruleDto.IgnoreAboveSize, DeletePrivateTorrentsFromClient = ruleDto.DeletePrivateTorrentsFromClient, ChangeCategory = ruleDto.ChangeCategory, }; var existingRules = await _dataContext.SlowRules.ToListAsync(); var intervalValidationResult = _ruleIntervalValidator.ValidateSlowRuleIntervals(rule, existingRules); if (!intervalValidationResult.IsValid) { return this.ProblemResult(StatusCodes.Status400BadRequest, intervalValidationResult.ErrorMessage); } rule.Validate(); _dataContext.SlowRules.Add(rule); await _dataContext.SaveChangesAsync(); _logger.LogInformation("Created slow rule: {RuleName} with ID: {RuleId}", rule.Name, rule.Id); return CreatedAtAction(nameof(GetSlowRules), new { id = rule.Id }, rule); } finally { DataContext.Lock.Release(); } } [HttpPut("slow/{id}")] public async Task UpdateSlowRule(Guid id, [FromBody] SlowRuleDto ruleDto) { await DataContext.Lock.WaitAsync(); try { var existingRule = await _dataContext.SlowRules .FirstOrDefaultAsync(r => r.Id == id); if (existingRule == null) { return this.ProblemResult(StatusCodes.Status404NotFound, $"Slow rule with ID {id} not found"); } var duplicateRule = await _dataContext.SlowRules .FirstOrDefaultAsync(r => r.Id != id && r.Name.ToLower() == ruleDto.Name.ToLower()); if (duplicateRule != null) { return this.ProblemResult(StatusCodes.Status400BadRequest, "A slow rule with this name already exists"); } var updatedRule = existingRule with { Name = ruleDto.Name.Trim(), Enabled = ruleDto.Enabled, MaxStrikes = ruleDto.MaxStrikes, PrivacyType = ruleDto.PrivacyType, MinCompletionPercentage = ruleDto.MinCompletionPercentage, MaxCompletionPercentage = ruleDto.MaxCompletionPercentage, ResetStrikesOnProgress = ruleDto.ResetStrikesOnProgress, MinSpeed = ruleDto.MinSpeed?.Trim() ?? string.Empty, MaxTimeHours = ruleDto.MaxTimeHours, IgnoreAboveSize = ruleDto.IgnoreAboveSize, DeletePrivateTorrentsFromClient = ruleDto.DeletePrivateTorrentsFromClient, ChangeCategory = ruleDto.ChangeCategory, }; var existingRules = await _dataContext.SlowRules .Where(r => r.Id != id) .ToListAsync(); var intervalValidationResult = _ruleIntervalValidator.ValidateSlowRuleIntervals(updatedRule, existingRules); if (!intervalValidationResult.IsValid) { return this.ProblemResult(StatusCodes.Status400BadRequest, intervalValidationResult.ErrorMessage); } updatedRule.Validate(); _dataContext.Entry(existingRule).CurrentValues.SetValues(updatedRule); await _dataContext.SaveChangesAsync(); _logger.LogInformation("Updated slow rule: {RuleName} with ID: {RuleId}", updatedRule.Name, id); return Ok(updatedRule); } finally { DataContext.Lock.Release(); } } [HttpDelete("slow/{id}")] public async Task DeleteSlowRule(Guid id) { await DataContext.Lock.WaitAsync(); try { var existingRule = await _dataContext.SlowRules .FirstOrDefaultAsync(r => r.Id == id); if (existingRule == null) { return this.ProblemResult(StatusCodes.Status404NotFound, $"Slow rule with ID {id} not found"); } _dataContext.SlowRules.Remove(existingRule); await _dataContext.SaveChangesAsync(); _logger.LogInformation("Deleted slow rule: {RuleName} with ID: {RuleId}", existingRule.Name, id); return NoContent(); } finally { DataContext.Lock.Release(); } } }