using System.Text.Json; using Backend.Domain; using Microsoft.EntityFrameworkCore; namespace Backend.Data; public static partial class SeedDataBootstrapper { private static async Task EnsureSeedOperationalDataAsync(AwardsDbContext db, Season season) { var categories = await db.Categories .Where(item => item.SeasonId == season.Id) .ToDictionaryAsync(item => item.Slug, StringComparer.OrdinalIgnoreCase); var candidates = await db.Candidates .Where(item => item.SeasonId == season.Id) .ToArrayAsync(); var normalizedLegacyState = await NormalizeLegacyDemoLabelsAsync(db); if (!await db.ClipSubmissions.AnyAsync(item => item.SeasonId == season.Id)) { db.ClipSubmissions.AddRange( new ClipSubmission { SeasonId = season.Id, CategoryId = ResolveCategoryId(categories, "vtuber-des-jahres"), CandidateId = ResolveCandidateId(categories, candidates, "vtuber-des-jahres", "Hoshimi Miyu"), SubmittedByTwitchId = "local_user_3", ClipUrl = "https://clips.twitch.tv/StarlitDebutMoment", Title = "Starlight Debut Moment", Creator = "Hoshimi Miyu", Platform = "Twitch", Status = "approved", ReviewNote = "Geprüfter Clip fuer Voting-Vorschau.", ReviewedByTwitchId = "jayuhime_admin", CreatedFromIp = "127.0.0.1", CreatedAt = new DateTimeOffset(2026, 6, 18, 8, 15, 0, TimeSpan.Zero), ReviewedAt = new DateTimeOffset(2026, 6, 18, 12, 5, 0, TimeSpan.Zero), }, new ClipSubmission { SeasonId = season.Id, CategoryId = ResolveCategoryId(categories, "vtuber-des-jahres"), CandidateId = ResolveCandidateId(categories, candidates, "vtuber-des-jahres", "Kurainu"), SubmittedByTwitchId = "local_user_4", ClipUrl = "https://clips.twitch.tv/KurainuFinaleHype", Title = "Finale-Hype mit Chat-Chaos", Creator = "Kurainu", Platform = "Twitch", Status = "approved", ReviewNote = "Geprüfter Clip fuer Voting-Vorschau.", ReviewedByTwitchId = "jayuhime_admin", CreatedFromIp = "127.0.0.1", CreatedAt = new DateTimeOffset(2026, 6, 18, 8, 35, 0, TimeSpan.Zero), ReviewedAt = new DateTimeOffset(2026, 6, 18, 12, 10, 0, TimeSpan.Zero), }, new ClipSubmission { SeasonId = season.Id, CategoryId = ResolveCategoryId(categories, "best-gaming"), CandidateId = ResolveCandidateId(categories, candidates, "best-gaming", "Kurainu"), SubmittedByTwitchId = "local_user", ClipUrl = "https://clips.twitch.tv/EpicGamingMoment", Title = "Epischer Clutch im Finale", Creator = "Kurainu", Platform = "Twitch", Status = "pending", CreatedFromIp = "127.0.0.1", CreatedAt = new DateTimeOffset(2026, 6, 17, 9, 10, 0, TimeSpan.Zero), }, new ClipSubmission { SeasonId = season.Id, CategoryId = ResolveCategoryId(categories, "gesang-musik"), CandidateId = ResolveCandidateId(categories, candidates, "gesang-musik", "Melo Diva"), SubmittedByTwitchId = "local_user_2", ClipUrl = "https://www.youtube.com/watch?v=liveCoverMoment", Title = "Live-Cover mit Gänsehaut", Creator = "Melo Diva", Platform = "YouTube", Status = "approved", ReviewNote = "Geprüfter Clip fuer Review-Workflow.", ReviewedByTwitchId = "jayuhime_admin", CreatedFromIp = "127.0.0.1", CreatedAt = new DateTimeOffset(2026, 6, 18, 10, 30, 0, TimeSpan.Zero), ReviewedAt = new DateTimeOffset(2026, 6, 18, 12, 0, 0, TimeSpan.Zero), }); } if (!normalizedLegacyState.HasRiskSeed && !await db.RiskFlags.AnyAsync(item => item.Source == "seed")) { db.RiskFlags.Add(new RiskFlag { SeasonId = season.Id, TwitchUserId = "sample_user", Source = "seed", Type = "rapid_vote_updates", Severity = "medium", Status = "open", Summary = "Mehrere Voting-Aenderungen in kurzer Zeit erkannt.", CreatedFromIp = "127.0.0.1", UserAgent = "seed-bootstrap", MetadataJson = JsonSerializer.Serialize(new { recentVoteSubmissions = 3 }), CreatedAt = new DateTimeOffset(2026, 6, 17, 8, 40, 0, TimeSpan.Zero), }); } if (!normalizedLegacyState.HasAuditSeed && !await db.AdminAuditEntries.AnyAsync(item => item.ActionType == "seed.initialize")) { db.AdminAuditEntries.Add(new AdminAuditEntry { AdminTwitchUserId = "system", ActionType = "seed.initialize", EntityType = "database", EntityId = season.Year.ToString(), Summary = "Startinhalte wurden in der Datenbank bereitgestellt.", MetadataJson = JsonSerializer.Serialize(new { categories = SeedCatalog.CategorySeeds.Length }), CreatedFromIp = "seed", UserAgent = "seed-bootstrap", CreatedAt = new DateTimeOffset(2026, 6, 17, 8, 32, 0, TimeSpan.Zero), }); } } private static int? ResolveCategoryId(IReadOnlyDictionary categories, string slug) => categories.TryGetValue(slug, out var category) ? category.Id : null; private static int? ResolveCandidateId( IReadOnlyDictionary categories, IEnumerable candidates, string categorySlug, string displayName) { var categoryId = ResolveCategoryId(categories, categorySlug); return categoryId is int resolvedCategoryId ? candidates.FirstOrDefault(item => item.CategoryId == resolvedCategoryId && string.Equals(item.DisplayName, displayName, StringComparison.OrdinalIgnoreCase))?.Id : null; } private static async Task NormalizeLegacyDemoLabelsAsync(AwardsDbContext db) { var legacySessions = await db.UserSessions .Where(item => item.TwitchUserId == "admin_demo" || item.TwitchUserId == "jayuhime_demo" || item.TwitchUserId == "demo_user") .ToArrayAsync(); foreach (var session in legacySessions) { session.TwitchUserId = session.TwitchUserId switch { "admin_demo" => "jayuhime_admin", "jayuhime_demo" => "jayuhime_viewer", "demo_user" => "local_user", _ => session.TwitchUserId, }; session.DisplayName = session.DisplayName switch { "Admin Demo" => "Jayuhime Admin", "Demo User" => "Local User", _ => session.DisplayName, }; } var legacyClipSubmissions = await db.ClipSubmissions .Where(item => item.SubmittedByTwitchId == "demo_user" || item.SubmittedByTwitchId == "demo_user_2" || item.ClipUrl.Contains("Demo") || item.ClipUrl.Contains("demo") || (item.ReviewNote != null && item.ReviewNote.Contains("Demo-Clip"))) .ToArrayAsync(); foreach (var clip in legacyClipSubmissions) { clip.SubmittedByTwitchId = clip.SubmittedByTwitchId switch { "demo_user" => "local_user", "demo_user_2" => "local_user_2", _ => clip.SubmittedByTwitchId, }; clip.ClipUrl = clip.ClipUrl .Replace("DemoGamingMoment", "EpicGamingMoment") .Replace("demoSong", "liveCoverMoment"); clip.ReviewNote = clip.ReviewNote?.Replace("Demo-Clip", "Geprüfter Clip"); } await LinkExistingClipsToCandidatesAsync(db); var legacyRiskFlags = await db.RiskFlags .Where(item => item.Source == "demo" || item.Summary.StartsWith("Demo:") || item.TwitchUserId == "jayuhime_demo" || item.TwitchUserId == "demo_user") .ToArrayAsync(); foreach (var flag in legacyRiskFlags) { flag.Source = "seed"; flag.TwitchUserId = flag.TwitchUserId switch { "demo_user" => "local_user", "jayuhime_demo" => "jayuhime_viewer", _ => flag.TwitchUserId, }; flag.Summary = flag.Summary.Replace("Demo: ", string.Empty); flag.UserAgent = flag.UserAgent == "demo-seed" ? "seed-bootstrap" : flag.UserAgent; } var legacyAuditEntries = await db.AdminAuditEntries .Where(item => item.ActionType == "demo.seed" || item.Summary.Contains("Demo-Inhalte") || item.AdminTwitchUserId == "admin_demo" || item.AdminTwitchUserId == "jayuhime_demo") .ToArrayAsync(); foreach (var entry in legacyAuditEntries) { entry.AdminTwitchUserId = entry.AdminTwitchUserId switch { "admin_demo" => "jayuhime_admin", "jayuhime_demo" => "jayuhime_viewer", _ => entry.AdminTwitchUserId, }; if (entry.ActionType == "demo.seed") { entry.ActionType = "seed.initialize"; } if (entry.Summary.Contains("Demo-Inhalte")) { entry.Summary = "Startinhalte wurden in der Datenbank bereitgestellt."; } } return new LegacySeedState( legacyRiskFlags.Length > 0 || await db.RiskFlags.AnyAsync(item => item.Source == "seed"), legacyAuditEntries.Length > 0 || await db.AdminAuditEntries.AnyAsync(item => item.ActionType == "seed.initialize")); } private static async Task LinkExistingClipsToCandidatesAsync(AwardsDbContext db) { var clips = await db.ClipSubmissions .Where(item => item.CandidateId == null && item.CategoryId != null && item.Creator != string.Empty) .ToArrayAsync(); if (clips.Length == 0) { return; } var seasonIds = clips.Select(item => item.SeasonId).Distinct().ToArray(); var categoryIds = clips.Select(item => item.CategoryId!.Value).Distinct().ToArray(); var candidates = await db.Candidates .Where(item => seasonIds.Contains(item.SeasonId) && categoryIds.Contains(item.CategoryId)) .ToArrayAsync(); foreach (var clip in clips) { var creatorKey = NormalizeSeedCandidateKey(clip.Creator); var candidate = candidates.FirstOrDefault(item => item.SeasonId == clip.SeasonId && item.CategoryId == clip.CategoryId && (NormalizeSeedCandidateKey(item.DisplayName) == creatorKey || NormalizeSeedCandidateKey(item.ChannelSlug) == creatorKey)); if (candidate is not null) { clip.CandidateId = candidate.Id; } } } private static string NormalizeSeedCandidateKey(string value) => new( value .Trim() .TrimStart('@') .ToLowerInvariant() .Where(char.IsLetterOrDigit) .ToArray()); private sealed record LegacySeedState(bool HasRiskSeed, bool HasAuditSeed); }