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);
}
}
}