using Backend.Data; using Backend.Domain; using Microsoft.EntityFrameworkCore; namespace Backend.Services; public sealed class NominationTrackingReviewService(AwardsDbContext db) { public async Task ReevaluateAsync(Nomination nomination, bool resetManualResolution, CancellationToken cancellationToken) { var settings = await db.SiteSettings .AsNoTracking() .FirstOrDefaultAsync(item => item.Id == 1, cancellationToken); ApplyEvaluation(nomination, TrackingRulesSettings.Read(settings), resetManualResolution); } public async Task ReevaluateAllAsync(bool resetManualResolution, CancellationToken cancellationToken) { var settings = await db.SiteSettings .AsNoTracking() .FirstOrDefaultAsync(item => item.Id == 1, cancellationToken); var rules = TrackingRulesSettings.Read(settings); await ReevaluateAllAsync(rules, resetManualResolution, cancellationToken); } public async Task ReevaluateAllAsync( TrackingRulesConfiguration rules, bool resetManualResolution, CancellationToken cancellationToken) { var nominations = await db.Nominations.ToArrayAsync(cancellationToken); foreach (var nomination in nominations) { ApplyEvaluation(nomination, rules, resetManualResolution); } } public NominationTrackingEvaluation Evaluate(Nomination nomination, TrackingRulesConfiguration rules) { var requiredMetrics = rules.ImportantMetrics .Where(item => item.Enabled && item.RequiredForAutoClassification) .ToArray(); var missingRequiredMetrics = requiredMetrics .Where(metric => !IsMetricPresent(metric, nomination)) .ToArray(); var unsupportedAutomaticWindows = rules.ImportantMetrics .Concat(rules.OptionalMetrics) .Where(item => item.Enabled && item.SourceSupport == "auto" && !TrackingRulesSettings.SupportsAutomaticWindow(item)) .ToArray(); var triggeredFlags = new List(); AddFlagIf(triggeredFlags, rules.Flags, TrackingRulesSettings.FlagTrackerUnresolved, nomination.TrackerStatus == "unresolved"); AddFlagIf(triggeredFlags, rules.Flags, TrackingRulesSettings.FlagUnsupportedPlatform, nomination.TrackerStatus == "unsupported_platform"); AddFlagIf(triggeredFlags, rules.Flags, TrackingRulesSettings.FlagNoTrackerData, nomination.TrackerStatus == "no_data"); AddFlagIf(triggeredFlags, rules.Flags, TrackingRulesSettings.FlagMissingRequiredMetric, missingRequiredMetrics.Length > 0); AddFlagIf(triggeredFlags, rules.Flags, TrackingRulesSettings.FlagUnsupportedMetricWindow, unsupportedAutomaticWindows.Length > 0); var needsManualReview = triggeredFlags.Any(flag => flag.RequiresManualReview); AddFlagIf(triggeredFlags, rules.Flags, TrackingRulesSettings.FlagManualReviewRequired, needsManualReview); return new NominationTrackingEvaluation( missingRequiredMetrics.Select(item => item.Key).ToArray(), triggeredFlags.ToArray(), needsManualReview || triggeredFlags.Any(flag => flag.Key == TrackingRulesSettings.FlagManualReviewRequired), triggeredFlags.Any(flag => flag.BlocksApproval), triggeredFlags.Any(flag => flag.AdminNoteRequiredOnOverride)); } public TrackingMetricState[] BuildMetricStates(Nomination nomination, TrackingRulesConfiguration rules) => rules.ImportantMetrics .Concat(rules.OptionalMetrics) .Where(item => item.Enabled && item.ShowInReview) .Select(metric => new TrackingMetricState( metric.Key, metric.Label, metric.RequiredForAutoClassification, metric.SourceSupport, IsMetricPresent(metric, nomination), ResolveMetricValue(metric, nomination), metric.Description, metric.WindowKey, TrackingRulesSettings.WindowLabel(metric.WindowKey), TrackingRulesSettings.SupportsAutomaticWindow(metric))) .ToArray(); private void ApplyEvaluation(Nomination nomination, TrackingRulesConfiguration rules, bool resetManualResolution) { var evaluation = Evaluate(nomination, rules); nomination.TrackingFlagsJson = TrackingRulesSettings.SerializeFlagHits(evaluation.Flags); if (resetManualResolution || nomination.TrackingReviewStatus is not ("reviewed" or "overridden")) { nomination.TrackingReviewStatus = evaluation.RequiresManualReview ? "flagged" : "clear"; if (resetManualResolution) { nomination.TrackingReviewNote = null; nomination.TrackingReviewedByTwitchId = null; nomination.TrackingReviewedAt = null; } } } private static bool IsMetricPresent(TrackingMetricRuleSetting metric, Nomination nomination) { if (metric.SourceSupport == "auto" && !TrackingRulesSettings.SupportsAutomaticWindow(metric)) { return false; } return metric.Key switch { TrackingRulesSettings.AvgViewers => nomination.AvgViewers.HasValue, TrackingRulesSettings.TrackerStatus => !string.IsNullOrWhiteSpace(nomination.TrackerStatus), TrackingRulesSettings.TrackerCheckedAt => nomination.TrackerCheckedAt.HasValue, TrackingRulesSettings.HoursStreamed => nomination.HoursStreamed.HasValue, TrackingRulesSettings.HoursWatched => nomination.HoursWatched.HasValue, TrackingRulesSettings.PeakViewers => nomination.PeakViewers.HasValue, TrackingRulesSettings.FollowersGained => nomination.FollowersGained.HasValue, _ => false, }; } private static string ResolveMetricValue(TrackingMetricRuleSetting metric, Nomination nomination) { if (metric.SourceSupport == "auto" && !TrackingRulesSettings.SupportsAutomaticWindow(metric)) { return $"Auto nur fuer {string.Join(", ", metric.AutoSupportedWindowKeys.Select(TrackingRulesSettings.WindowLabel))}"; } return metric.Key switch { TrackingRulesSettings.AvgViewers => nomination.AvgViewers?.ToString() ?? "offen", TrackingRulesSettings.TrackerStatus => string.IsNullOrWhiteSpace(nomination.TrackerStatus) ? "offen" : nomination.TrackerStatus, TrackingRulesSettings.TrackerCheckedAt => nomination.TrackerCheckedAt?.ToString("g") ?? "offen", TrackingRulesSettings.HoursStreamed => nomination.HoursStreamed?.ToString() ?? "offen", TrackingRulesSettings.HoursWatched => nomination.HoursWatched?.ToString() ?? "offen", TrackingRulesSettings.PeakViewers => nomination.PeakViewers?.ToString() ?? "offen", TrackingRulesSettings.FollowersGained => nomination.FollowersGained?.ToString() ?? "offen", TrackingRulesSettings.CategoryFit => "Manueller Kategorie-Check", TrackingRulesSettings.TopCategoriesContext => BuildTopCategoriesContextSummary(metric), _ => "manuell", }; } private static string BuildTopCategoriesContextSummary(TrackingMetricRuleSetting metric) { var parts = new List(); if (metric.TopCount.HasValue) { parts.Add($"Top {metric.TopCount.Value}"); } if (metric.MinPrimaryCategorySharePercent.HasValue) { parts.Add($">= {metric.MinPrimaryCategorySharePercent.Value}% Hauptkategorie"); } if (metric.MinPrimaryCategoryHours.HasValue) { parts.Add($">= {metric.MinPrimaryCategoryHours.Value}h Hauptkategorie"); } if (metric.MaxDistinctCategoriesBeforeFlag.HasValue) { parts.Add($"Flag ab {metric.MaxDistinctCategoriesBeforeFlag.Value}+ Kategorien"); } if (metric.IgnoredCategories.Length > 0) { parts.Add($"Ignore: {string.Join(", ", metric.IgnoredCategories)}"); } return parts.Count > 0 ? string.Join(" ยท ", parts) : "Top-Kategorien manuell pruefen"; } private static void AddFlagIf( ICollection target, IEnumerable availableFlags, string key, bool shouldAdd) { if (!shouldAdd) { return; } var rule = availableFlags.FirstOrDefault(item => item.Key == key); if (rule is null || !rule.Enabled || !rule.AutoTriggerEnabled) { return; } target.Add(new TrackingFlagHit( rule.Key, rule.Label, rule.Severity, rule.Description, rule.RequiresManualReview, rule.BlocksApproval, rule.AdminNoteRequiredOnOverride)); } } public sealed record NominationTrackingEvaluation( string[] MissingRequiredMetricKeys, TrackingFlagHit[] Flags, bool RequiresManualReview, bool HasBlockingFlag, bool RequiresOverrideNote); public sealed record TrackingMetricState( string Key, string Label, bool Required, string SourceSupport, bool Present, string Value, string Description, string WindowKey, string WindowLabel, bool AutoWindowSupported);