using System.Text.Json; using Koogle.Domain.Entities; using Koogle.Domain.Enums; using Koogle.Domain.Interfaces; using Koogle.Infrastructure.Identity; using KoogleApp.Data; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace Koogle.Infrastructure.Data; /// /// Seeds demo data for the Demo club including user, persons, expenses, and sample days. /// public static class DemoSeeder { private static readonly string[] MemberNames = { "Hans Maier", "Klaus Schmidt", "Werner Braun", "Dieter Fischer", "Juergen Weber", "Heinz Mueller", "Rolf Schneider", "Karl Hoffmann" }; private static readonly string[] GuestNames = { "Stefan Gast", "Thomas Besucher" }; /// /// DTO for deserializing GIF template entries from giftemplates.json. /// private record GifTemplateEntry(string Name, string Filename, string ThrowEventType, string? Description); /// /// Seeds demo user, club membership, persons, expenses, triggers and sample days. /// public static async Task SeedAsync(IServiceProvider services, IConfiguration config, IHostEnvironment env) { var demoEnabled = config.GetValue("Bootstrap:Demo:Enabled"); if (!demoEnabled) return; var allowSeed = env.IsDevelopment() || config.GetValue("Bootstrap:EnableSeeding"); if (!allowSeed) return; using var scope = services.CreateScope(); var userManager = scope.ServiceProvider.GetRequiredService>(); var appDb = scope.ServiceProvider.GetRequiredService(); var triggerRepo = scope.ServiceProvider.GetRequiredService(); var demoEmail = config["Bootstrap:Demo:Email"] ?? "demo@koogle.de"; var demoPassword = config["Bootstrap:Demo:Password"] ?? "demo123"; var demoClubName = config["Bootstrap:Demo:ClubName"] ?? "Demo"; // 1) Find or create Demo Club (should exist from BootstrapSeeder if DefaultClub.Name == "Demo") var club = await appDb.Clubs.FirstOrDefaultAsync(c => c.Name == demoClubName && !c.IsDeleted); if (club == null) { club = new Club { Name = demoClubName, CreatedAt = DateTime.UtcNow }; appDb.Clubs.Add(club); await appDb.SaveChangesAsync(); } // 2) Create Demo User (NOT SuperAdmin) var demoUser = await userManager.FindByEmailAsync(demoEmail); if (demoUser == null) { demoUser = new ApplicationUser { UserName = demoEmail, Email = demoEmail, EmailConfirmed = true }; var result = await userManager.CreateAsync(demoUser, demoPassword); if (!result.Succeeded) { throw new InvalidOperationException("Failed to create Demo user: " + string.Join("; ", result.Errors.Select(e => $"{e.Code}:{e.Description}"))); } } // 3) Create UserProfile for Demo user var profile = await appDb.UserProfiles.FirstOrDefaultAsync(p => p.IdentityUserId == demoUser.Id); if (profile == null) { profile = new UserProfile { IdentityUserId = demoUser.Id, DisplayName = "Demo Benutzer", Locale = "de-DE", TimeZone = "Europe/Berlin", CreatedAt = DateTime.UtcNow }; appDb.UserProfiles.Add(profile); await appDb.SaveChangesAsync(); } // 4) Add Demo user to Demo club var membership = await appDb.UserProfileClubs.FindAsync(profile.Id, club.Id); if (membership == null) { membership = new UserProfileClub { UserProfileId = profile.Id, ClubId = club.Id, IsDefault = true, AssignedAt = DateTime.UtcNow, AssignedById = demoUser.Id }; appDb.UserProfileClubs.Add(membership); await appDb.SaveChangesAsync(); } // 5) Assign ClubAdmin role to Demo user (NOT SuperAdmin!) var roleExists = await appDb.UserProfileClubRoleAssignments.AnyAsync(a => a.UserProfileId == profile.Id && a.ClubId == club.Id && a.RoleName == "Admin" && !a.IsDeleted); if (!roleExists) { appDb.UserProfileClubRoleAssignments.Add(new UserProfileClubRoleAssignment { UserProfileId = profile.Id, ClubId = club.Id, RoleId = Guid.Empty, RoleName = "Admin", AssignedAt = DateTime.UtcNow, AssignedById = demoUser.Id, CreatedAt = DateTime.UtcNow }); await appDb.SaveChangesAsync(); } // 6) Seed demo data if not already present var hasPersons = await appDb.Persons.AnyAsync(p => p.ClubId == club.Id && !p.IsDeleted); if (!hasPersons) { await SeedDemoDataAsync(appDb, triggerRepo, club.Id, demoUser.Id); } // 7) Seed template GIFs if not already present var webEnv = scope.ServiceProvider.GetRequiredService(); var hasGifs = await appDb.ClubGifs.AnyAsync(g => g.ClubId == club.Id && !g.IsDeleted); if (!hasGifs) { await SeedTemplateGifsInternalAsync(appDb, webEnv, club.Id, club.LoginName); } } /// /// Resets the Demo club to initial state: hard delete all data, then re-seed. /// public static async Task ResetDemoClubAsync(IServiceProvider services, Guid clubId) { using var scope = services.CreateScope(); var appDb = scope.ServiceProvider.GetRequiredService(); var triggerRepo = scope.ServiceProvider.GetRequiredService(); var env = scope.ServiceProvider.GetRequiredService(); // Find creator ID and club for seeding var club = await appDb.Clubs.FindAsync(clubId); if (club == null) return; var membership = await appDb.UserProfileClubs.FirstOrDefaultAsync(m => m.ClubId == clubId); var creatorId = membership?.AssignedById ?? Guid.Empty; // Hard delete all club data including GIFs await DeleteClubGifsAsync(appDb, env, clubId, club.LoginName); await HardDeleteClubDataAsync(appDb, clubId); // Re-seed demo data await SeedDemoDataAsync(appDb, triggerRepo, clubId, creatorId); // Re-seed template GIFs await SeedTemplateGifsInternalAsync(appDb, env, clubId, club.LoginName); } private static async Task HardDeleteClubDataAsync(AppDbContext db, Guid clubId) { // Order matters due to FK constraints - delete children first await db.Database.ExecuteSqlAsync($"DELETE FROM app.PersonExpenses WHERE ClubId = {clubId}"); await db.Database.ExecuteSqlAsync($"DELETE FROM app.PlayerGameStatistics WHERE ClubId = {clubId}"); await db.Database.ExecuteSqlAsync($"DELETE FROM app.GamePersons WHERE ClubId = {clubId}"); await db.Database.ExecuteSqlAsync($"DELETE FROM app.Games WHERE ClubId = {clubId}"); await db.Database.ExecuteSqlAsync($"DELETE FROM app.DayPersons WHERE ClubId = {clubId}"); await db.Database.ExecuteSqlAsync($"DELETE FROM app.Days WHERE ClubId = {clubId}"); await db.Database.ExecuteSqlAsync($"DELETE FROM app.ExpenseTriggers WHERE ClubId = {clubId}"); await db.Database.ExecuteSqlAsync($"DELETE FROM app.Expenses WHERE ClubId = {clubId}"); await db.Database.ExecuteSqlAsync($"DELETE FROM app.Persons WHERE ClubId = {clubId}"); } private static async Task SeedDemoDataAsync(AppDbContext db, ITriggerRepository triggerRepo, Guid clubId, Guid creatorId) { var now = DateTime.UtcNow; // 1) Seed Persons (8 Members + 2 Guests) var persons = new List(); foreach (var name in MemberNames) { var person = new Person { Name = name, PersonStatus = PersonStatus.Member, ClubId = clubId, CreatedAt = now, CreatedById = creatorId }; db.Persons.Add(person); persons.Add(person); } foreach (var name in GuestNames) { var person = new Person { Name = name, PersonStatus = PersonStatus.Guest, ClubId = clubId, CreatedAt = now, CreatedById = creatorId }; db.Persons.Add(person); persons.Add(person); } await db.SaveChangesAsync(); // 2) Seed Expenses with Trigger mappings var expenseData = new (string Name, decimal Price, ExpenseTriggerType TriggerType, bool IsInverse)[] { ("Gosse", 0.50m, ExpenseTriggerType.Gutter, false), ("Pudel", 0.30m, ExpenseTriggerType.NoWood, false), ("Klingel", 0.20m, ExpenseTriggerType.Bell, false), ("Anwurffehler", 0.40m, ExpenseTriggerType.FirstThrowFail, false), ("Kranz", 0.50m, ExpenseTriggerType.Circle, true), ("Alle Neune", 1.00m, ExpenseTriggerType.Strike, true), ("Gosse Anwurf", 1.00m, ExpenseTriggerType.FullGutter, false), ("Ausgeschieden", 2.00m, ExpenseTriggerType.Eliminated, false), ("Abwesenheit", 5.00m, ExpenseTriggerType.Absent, false), ("Strafpunkt", 0.10m, ExpenseTriggerType.ExpensePoint, false) }; var expenses = new List(); foreach (var (name, price, triggerType, isInverse) in expenseData) { var expense = new Expense { ClubId = clubId, Name = name, Description = $"Strafe fuer {name}", Price = price, IsOneClick = true, IsInverse = isInverse, IsVariable = false, ExpenseType = ExpenseType.Monetary, CreatedAt = now, CreatedById = creatorId }; db.Expenses.Add(expense); expenses.Add(expense); } await db.SaveChangesAsync(); // 3) Create ExpenseTrigger mappings for (int i = 0; i < expenses.Count; i++) { var expense = expenses[i]; var triggerType = expenseData[i].TriggerType; var trigger = await triggerRepo.GetByTriggerTypeAsync(triggerType); if (trigger != null) { var expenseTrigger = new ExpenseTrigger { ClubId = clubId, ExpenseId = expense.Id, TriggerId = trigger.Id, AssignedAt = now, AssignedById = creatorId }; db.ExpenseTriggers.Add(expenseTrigger); } } await db.SaveChangesAsync(); // 4) Seed Sample Days with Games await SeedSampleDaysAsync(db, clubId, creatorId, persons, expenses, now); } private static async Task SeedSampleDaysAsync( AppDbContext db, Guid clubId, Guid creatorId, List persons, List expenses, DateTime now) { var members = persons.Where(p => p.PersonStatus == PersonStatus.Member).ToList(); var gosseExpense = expenses.First(e => e.Name == "Gosse"); var pudelExpense = expenses.First(e => e.Name == "Pudel"); var klingelExpense = expenses.First(e => e.Name == "Klingel"); // Day 1: 3 participants, 1 game, some expenses var day1 = new Day { PostDate = now.AddDays(-14), Status = DayStatus.Closed, ClubId = clubId, CreatedAt = now, CreatedById = creatorId }; db.Days.Add(day1); await db.SaveChangesAsync(); var day1Participants = members.Take(3).ToList(); foreach (var p in day1Participants) { db.Set().Add(new DayPerson { DayId = day1.Id, ClubId = clubId, PersonId = p.Id }); } var game1 = new Game { DayId = day1.Id, ClubId = clubId, GameType = "Training", Status = GameStatus.Completed, StartedAt = day1.PostDate, CompletedAt = day1.PostDate.AddHours(2), CreatedAt = now, CreatedById = creatorId }; db.Games.Add(game1); await db.SaveChangesAsync(); foreach (var p in day1Participants) { db.Set().Add(new GamePerson { GameId = game1.Id, ClubId = clubId, PersonId = p.Id }); } await db.SaveChangesAsync(); // Day 2: 5 participants, 2 games, diverse expenses var day2 = new Day { PostDate = now.AddDays(-7), Status = DayStatus.Closed, ClubId = clubId, CreatedAt = now, CreatedById = creatorId }; db.Days.Add(day2); await db.SaveChangesAsync(); var day2Participants = members.Take(5).ToList(); foreach (var p in day2Participants) { db.Set().Add(new DayPerson { DayId = day2.Id, ClubId = clubId, PersonId = p.Id }); } var game2a = new Game { DayId = day2.Id, ClubId = clubId, GameType = "Training", Status = GameStatus.Completed, StartedAt = day2.PostDate, CompletedAt = day2.PostDate.AddHours(1), CreatedAt = now, CreatedById = creatorId }; var game2b = new Game { DayId = day2.Id, ClubId = clubId, GameType = "Totenkiste", Status = GameStatus.Completed, StartedAt = day2.PostDate.AddHours(1), CompletedAt = day2.PostDate.AddHours(2), CreatedAt = now, CreatedById = creatorId }; db.Games.Add(game2a); db.Games.Add(game2b); await db.SaveChangesAsync(); foreach (var p in day2Participants) { db.Set().Add(new GamePerson { GameId = game2a.Id, ClubId = clubId, PersonId = p.Id }); db.Set().Add(new GamePerson { GameId = game2b.Id, ClubId = clubId, PersonId = p.Id }); } await db.SaveChangesAsync(); // Day 3: 4 participants, 1 game (recent, still open) var day3 = new Day { PostDate = now.AddDays(-1), Status = DayStatus.Started, ClubId = clubId, CreatedAt = now, CreatedById = creatorId }; db.Days.Add(day3); await db.SaveChangesAsync(); var day3Participants = members.Take(4).ToList(); foreach (var p in day3Participants) { db.Set().Add(new DayPerson { DayId = day3.Id, ClubId = clubId, PersonId = p.Id }); } var game3 = new Game { DayId = day3.Id, ClubId = clubId, GameType = "Training", Status = GameStatus.Active, StartedAt = day3.PostDate, CreatedAt = now, CreatedById = creatorId }; db.Games.Add(game3); await db.SaveChangesAsync(); foreach (var p in day3Participants) { db.Set().Add(new GamePerson { GameId = game3.Id, ClubId = clubId, PersonId = p.Id }); } await db.SaveChangesAsync(); } /// /// Seeds template GIFs for a club by copying files and creating database entries. /// Called during demo reset and when creating new clubs. /// /// Service provider. /// Club ID to seed GIFs for. /// Club's login name for folder path. public static async Task SeedTemplateGifsAsync(IServiceProvider services, Guid clubId, string clubLoginName) { using var scope = services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var env = scope.ServiceProvider.GetRequiredService(); await SeedTemplateGifsInternalAsync(db, env, clubId, clubLoginName); } private static async Task SeedTemplateGifsInternalAsync( AppDbContext db, IWebHostEnvironment env, Guid clubId, string clubLoginName) { var templatePath = Path.Combine(env.WebRootPath, "club-template", "gifs"); var templateJsonPath = Path.Combine(templatePath, "giftemplates.json"); if (!File.Exists(templateJsonPath)) { return; // No template file, skip seeding } // Read template definitions var jsonContent = await File.ReadAllTextAsync(templateJsonPath); var templates = JsonSerializer.Deserialize>(jsonContent, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); if (templates == null || templates.Count == 0) { return; } // Create target directory var targetPath = Path.Combine(env.WebRootPath, "club-media", clubLoginName, "gifs"); Directory.CreateDirectory(targetPath); var now = DateTime.UtcNow; foreach (var template in templates) { var sourceFile = Path.Combine(templatePath, template.Filename); if (!File.Exists(sourceFile)) { continue; // Skip if source file doesn't exist } // Generate new filename for club var extension = Path.GetExtension(template.Filename); var newFileName = $"{Guid.NewGuid()}{extension}"; var targetFile = Path.Combine(targetPath, newFileName); // Copy file File.Copy(sourceFile, targetFile, overwrite: true); // Parse event type if (!Enum.TryParse(template.ThrowEventType, out var eventType)) { eventType = ThrowEventType.None; } // Get file info var fileInfo = new FileInfo(targetFile); // Determine content type var contentType = extension.ToLowerInvariant() switch { ".gif" => "image/gif", ".mp4" => "video/mp4", ".webm" => "video/webm", _ => "application/octet-stream" }; // Create database entry var gif = new ClubGif { ClubId = clubId, Name = template.Name, FileName = newFileName, OriginalFileName = template.Filename, ContentType = contentType, FileSizeBytes = fileInfo.Length, AssignedEvents = eventType, Description = template.Description, IsEnabled = true, IsPendingApproval = false, RatingScore = 0, RatingCount = 0, CreatedAt = now }; db.ClubGifs.Add(gif); } await db.SaveChangesAsync(); } /// /// Deletes all GIF files and database entries for a club. /// private static async Task DeleteClubGifsAsync(AppDbContext db, IWebHostEnvironment env, Guid clubId, string clubLoginName) { // Delete database entries await db.Database.ExecuteSqlAsync($"DELETE FROM app.ClubGifRatings WHERE ClubGifId IN (SELECT Id FROM app.ClubGifs WHERE ClubId = {clubId})"); await db.Database.ExecuteSqlAsync($"DELETE FROM app.ClubGifs WHERE ClubId = {clubId}"); await db.Database.ExecuteSqlAsync($"DELETE FROM app.GifSubmissionTokens WHERE ClubId = {clubId}"); // Delete files var gifPath = Path.Combine(env.WebRootPath, "club-media", clubLoginName, "gifs"); if (Directory.Exists(gifPath)) { Directory.Delete(gifPath, recursive: true); } } }