using System.Text.Json; using Backend.Domain; namespace Backend.Services; public sealed record WorkflowRuleSetting( string Key, string Label, bool Enabled, int Limit, string Mode, string Description); public static class WorkflowRuleSettings { private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); public const string MaxFinalistsPerCategory = "max_finalists_per_category"; public const string MaxCandidateAppearances = "max_candidate_appearances"; public const string MaxWinnerPlacements = "max_winner_placements"; public const string WinnerRequiresClip = "winner_requires_clip"; public const string RecommendedNominatorsPerSubcategory = "recommended_nominators_per_subcategory"; public static WorkflowRuleSetting[] Defaults { get; } = [ new(MaxFinalistsPerCategory, "Finale Kandidat:innen pro Kategorie", true, 4, "block", "Begrenzt, wie viele finale Kandidat:innen in einer Kategorie vorbereitet werden."), new(MaxCandidateAppearances, "Kandidaturen pro Person", true, 2, "warn", "Warnt oder blockiert, wenn dieselbe Person in zu vielen Kategorien final auftaucht."), new(MaxWinnerPlacements, "Gewinnerplätze pro Person", true, 1, "block", "Verhindert oder warnt, wenn dieselbe Person mehrfach als Gewinner:in gesetzt wird."), new(WinnerRequiresClip, "Gewinner braucht Clip-Link", true, 1, "block", "Verhindert oder warnt, wenn ein Gewinner ohne gepflegten YouTube-/Twitch-Clip gesetzt wird."), new(RecommendedNominatorsPerSubcategory, "Empfohlene Nominierer pro Unterkategorie", true, 4, "warn", "Zeigt in der Kategorie-Übersicht an, ab wann eine Unterkategorie nominierungsseitig gut getragen ist. Diese Regel blockiert nichts."), ]; public static WorkflowRuleSetting[] Read(SiteSettings? settings) { var storedRules = Parse(settings?.WorkflowRulesJson); return Read(storedRules); } public static WorkflowRuleSetting[] Read(Season? season, SiteSettings? settings) { var seasonRules = Parse(season?.WorkflowRulesJson); if (seasonRules.Length > 0) { return Read(seasonRules); } return Read(settings); } private static WorkflowRuleSetting[] Read(WorkflowRuleSetting[] storedRules) { return Defaults .Select(defaultRule => { var storedRule = storedRules.FirstOrDefault(item => string.Equals(item.Key, defaultRule.Key, StringComparison.OrdinalIgnoreCase)); return storedRule is null ? defaultRule : Normalize(storedRule, defaultRule); }) .ToArray(); } public static string Serialize(IEnumerable rules) => JsonSerializer.Serialize(rules.Select(rule => Normalize(rule, Defaults.FirstOrDefault(item => item.Key == rule.Key) ?? rule)), JsonOptions); public static WorkflowRuleSetting Find(IEnumerable rules, string key) => rules.FirstOrDefault(item => string.Equals(item.Key, key, StringComparison.OrdinalIgnoreCase)) ?? Defaults.First(item => item.Key == key); public static bool ShouldBlock(WorkflowRuleSetting rule) => rule.Enabled && string.Equals(rule.Mode, "block", StringComparison.OrdinalIgnoreCase); public static string CandidateIdentityKey(Candidate candidate) { var channel = candidate.ChannelSlug.Trim().TrimStart('@').ToLowerInvariant(); if (!string.IsNullOrWhiteSpace(channel)) { return $"slug:{channel}"; } return $"name:{candidate.DisplayName.Trim().ToLowerInvariant()}"; } public static string CandidateIdentityKey(string displayName, string channelSlug) { var channel = channelSlug.Trim().TrimStart('@').ToLowerInvariant(); if (!string.IsNullOrWhiteSpace(channel)) { return $"slug:{channel}"; } return $"name:{displayName.Trim().ToLowerInvariant()}"; } private static WorkflowRuleSetting[] Parse(string? json) { if (string.IsNullOrWhiteSpace(json)) { return []; } try { return JsonSerializer.Deserialize(json, JsonOptions) ?? []; } catch (JsonException) { return []; } } private static WorkflowRuleSetting Normalize(WorkflowRuleSetting rule, WorkflowRuleSetting fallback) { var mode = rule.Mode.Trim().ToLowerInvariant() switch { "block" => "block", "warn" => "warn", _ => fallback.Mode, }; if (string.Equals(fallback.Key, RecommendedNominatorsPerSubcategory, StringComparison.OrdinalIgnoreCase)) { mode = "warn"; } return rule with { Key = fallback.Key, Label = string.IsNullOrWhiteSpace(rule.Label) ? fallback.Label : rule.Label.Trim(), Enabled = rule.Enabled, Limit = Math.Clamp(rule.Limit, 1, 50), Mode = mode, Description = string.IsNullOrWhiteSpace(rule.Description) ? fallback.Description : rule.Description.Trim(), }; } }