using Cleanuparr.Domain.Enums; using Cleanuparr.Infrastructure.Models; using Cleanuparr.Infrastructure.Services.Interfaces; using Cleanuparr.Persistence.Models.Configuration.QueueCleaner; using Microsoft.Extensions.Logging; namespace Cleanuparr.Infrastructure.Services; public class RuleIntervalValidator : IRuleIntervalValidator { private readonly ILogger _logger; public RuleIntervalValidator(ILogger logger) { _logger = logger; } public ValidationResult ValidateStallRuleIntervals(StallRule newRule, List existingRules) { _logger.LogDebug("Validating stall rule intervals for rule {rule}", newRule.Name); var allRules = existingRules.Cast().ToList(); allRules.Add(newRule); return ValidateRuleIntervals(allRules, newRule.Name); } public ValidationResult ValidateSlowRuleIntervals(SlowRule newRule, List existingRules) { _logger.LogDebug("Validating slow rule intervals for rule {rule}", newRule.Name); var allRules = existingRules.Cast().ToList(); allRules.Add(newRule); return ValidateRuleIntervals(allRules, newRule.Name); } /// /// Validates that the provided rules do not create overlapping intervals. /// /// The collection of all rules, including the newly created one. /// The name of the new rule. /// private ValidationResult ValidateRuleIntervals(List allRules, string newRuleName) { // Remove duplicate rules with the same ID (keep the last one, which is typically the updated version) var deduplicatedRules = allRules .GroupBy(r => r.Id) .Select(g => g.Last()) .ToList(); // Only consider enabled rules for validation List enabledRules = deduplicatedRules .Where(r => r.Enabled) .ToList(); // Expand privacy types (Both -> Public + Private) List intervals = ExpandPrivacyTypes(enabledRules); // Group by privacy type and check for overlaps List publicIntervals = intervals .Where(i => i.PrivacyType == TorrentPrivacyType.Public) .ToList(); List privateIntervals = intervals .Where(i => i.PrivacyType == TorrentPrivacyType.Private) .ToList(); List publicOverlaps = FindAllOverlappingIntervals(publicIntervals, newRuleName); List privateOverlaps = FindAllOverlappingIntervals(privateIntervals, newRuleName); HashSet overlappingRules = []; foreach (var overlap in publicOverlaps) { overlappingRules.Add(overlap.ConflictingRuleName); _logger.LogWarning("Rule {newRuleName} overlaps for Public torrents with rule {ruleName} (both cover {start}%-{end}%)", newRuleName, overlap.ConflictingRuleName, overlap.OverlapStart, overlap.OverlapEnd ); } foreach (var overlap in privateOverlaps) { overlappingRules.Add(overlap.ConflictingRuleName); _logger.LogWarning("Rule {newRuleName} overlaps for Private torrents with rule {ruleName} (both cover {start}%-{end}%)", newRuleName, overlap.ConflictingRuleName, overlap.OverlapStart, overlap.OverlapEnd ); } if (overlappingRules.Count > 0) { var details = overlappingRules.ToList(); return ValidationResult.Failure("Rule creates overlapping intervals with existing rules: " + string.Join(", ", overlappingRules), details); } return ValidationResult.Success(); } private static List ExpandPrivacyTypes(List rules) { return rules .SelectMany(rule => GetPrivacyTypes(rule.PrivacyType) .Select(privacyType => new RuleInterval { PrivacyType = privacyType, Start = rule.MinCompletionPercentage, End = rule.MaxCompletionPercentage, RuleName = rule.Name, RuleId = rule.Id })) .ToList(); } private static IEnumerable GetPrivacyTypes(TorrentPrivacyType type) { if (type == TorrentPrivacyType.Both) { yield return TorrentPrivacyType.Public; yield return TorrentPrivacyType.Private; } else { yield return type; } } /// /// Finds all overlapping intervals for the new rule being validated. /// /// List of intervals to check for overlaps /// Name of the rule being validated /// List of all overlaps found private static List FindAllOverlappingIntervals(List intervals, string newRuleName) { var overlaps = new List(); if (intervals.Count < 2) { return overlaps; } var sortedIntervals = intervals .OrderBy(i => i.Start) .ThenBy(i => i.End) .ToList(); // Find the new rule interval(s) var newRuleIntervals = sortedIntervals.Where(i => i.RuleName == newRuleName).ToList(); var existingIntervals = sortedIntervals.Where(i => i.RuleName != newRuleName).ToList(); // Check each new rule interval against all existing intervals foreach (var newInterval in newRuleIntervals) { foreach (var existingInterval in existingIntervals) { // Skip if same rule ID (handles the case where a rule is being updated) if (newInterval.RuleId == existingInterval.RuleId) { continue; } // Check for overlap if (newInterval.Start < existingInterval.End && existingInterval.Start < newInterval.End) { var overlapStart = Math.Max(newInterval.Start, existingInterval.Start); var overlapEnd = Math.Min(newInterval.End, existingInterval.End); if (overlapEnd > overlapStart) { overlaps.Add(new OverlapResult { ConflictingRuleName = existingInterval.RuleName, OverlapStart = overlapStart, OverlapEnd = overlapEnd }); } } } } return overlaps; } private class OverlapResult { public string ConflictingRuleName { get; set; } = string.Empty; public double OverlapStart { get; set; } public double OverlapEnd { get; set; } } }