using LiteCharms.Features.MidrandBooks.Categories; using LiteCharms.Features.MidrandBooks.Products; namespace LiteCharms.Features.MidrandBooks.Seed; public sealed class CategorySeederService(CategoryService categoryService, ProductService productService, IFeatureManager features, ILogger logger) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { if (!await features.IsEnabledAsync("CategorySeederService")) return; logger.LogInformation("Category and Product-Tag Mapping Seeding started (15-char limit applied)"); // Initialize Bogus to ensure repeatable distribution matrix pathing var faker = new Faker(); Randomizer.Seed = new Random(101); // 1. Curate Broad Book Categories (IsMain = true, Max 20, Max 15 chars) var broadMainCategories = new[] { "Fiction", "Non-Fiction", "Youth & Kids", "Academic", "Biographies", "Business", "Sci-Fi & Fantasy", "Thrillers", "Self-Help", "History", "Spirituality", "Arts & Photo", "Technology", "Cookbooks", "Travel & Maps", "Poetry & Drama", "Graphic Novels" }; // 2. Curate Niche Subcategories/Tags (IsMain = false, Max 15 chars) var specializedSubCategories = new[] { "Cyberpunk", "Space Opera", "Historical Fix", "Cozy Mystery", "True Crime", "Agile Project", "Software Eng", "AI & ML", "Cloud Comput", "SA History", "African Lit", "Apartheid Era", "Mandela Legacy", "Finance", "Investments", "Startup", "Leadership", "CBT Therapy", "Mindfulness", "Yoga & Health", "Baking Basics", "African Food", "Vegan Recipes", "Ancient World", "WWII History", "Geopolitics", "Writing Guides", "Criticism", "Classic Poetry", "Early Learning", "Teen Romance", "Survival", "Urban Fantasy", "Dark Fantasy", "Psych Thriller", "Hard Sci-Fi", "Data Science", "DevOps", "Cybersecurity", "Economics", "Real Estate", "Governance", "Essays", "Memoirs", "Art History", "Architecture", "Photography", "Travel Writing", "Gaming Culture", "Philosophy", "Ethics", "DIY Home", "SA Gardening", "Parenting" }; // 3. Seed Main Categories into the System logger.LogInformation("Seeding broad main categories..."); foreach (var mainCat in broadMainCategories) { if (stoppingToken.IsCancellationRequested) return; // Defensive truncation fallback just in case strings get modified later string safeName = mainCat.Length > 15 ? mainCat.Substring(0, 15) : mainCat; var result = await categoryService.CreateCategoryAsync(safeName, isMain: true, stoppingToken); if (result.IsFailed && !result.Errors[0].Message.Contains("already exists", StringComparison.OrdinalIgnoreCase)) { logger.LogWarning("Notice while adding main category '{Name}': {Msg}", safeName, result.Errors[0].Message); } } // 4. Seed Subcategories into the System logger.LogInformation("Seeding boundless specialized niche tags..."); foreach (var subCat in specializedSubCategories) { if (stoppingToken.IsCancellationRequested) return; string safeName = subCat.Length > 15 ? subCat.Substring(0, 15) : subCat; var result = await categoryService.CreateCategoryAsync(safeName, isMain: false, stoppingToken); if (result.IsFailed && !result.Errors[0].Message.Contains("already exists", StringComparison.OrdinalIgnoreCase)) { logger.LogWarning("Notice while adding subcategory '{Name}': {Msg}", safeName, result.Errors[0].Message); } } // 5. Query back all enabled categories to extract active IDs for junction mapping var fetchMainResult = await categoryService.GetCategoriesAsync(isMain: true, stoppingToken); var fetchSubResult = await categoryService.GetCategoriesAsync(isMain: false, stoppingToken); if (fetchMainResult.IsFailed || fetchSubResult.IsFailed) { logger.LogError("Aborting junction seeding: Could not retrieve categories from data store."); return; } var mainCategoryIds = fetchMainResult.Value.Select(c => c.Id).ToArray(); var subCategoryIds = fetchSubResult.Value.Select(c => c.Id).ToArray(); // 6. Map Categories to your Product Collection (Product IDs 0 - 21) logger.LogInformation("Beginning Product-Category mapping assignments for Product IDs 0 through 21..."); for (long productId = 0; productId <= 21; productId++) { if (stoppingToken.IsCancellationRequested) break; // Every book belongs to 1 or 2 main categories int mainCategoriesToAssign = faker.Random.Number(1, 2); var chosenMainIds = faker.PickRandom(mainCategoryIds, mainCategoriesToAssign).Distinct(); foreach (var mainId in chosenMainIds) { var linkResult = await productService.AddProductCategoryAsync(productId, mainId, stoppingToken); if (linkResult.IsFailed) { if (!linkResult.Errors[0].Message.Contains("exist", StringComparison.OrdinalIgnoreCase)) { logger.LogDebug("Junction note for Product {PId} and Main Category {CId}: {Msg}", productId, mainId, linkResult.Errors[0].Message); } } } // Every book gets 1 to 4 granular subgenre tags int subCategoriesToAssign = faker.Random.Number(1, 4); var chosenSubIds = faker.PickRandom(subCategoryIds, subCategoriesToAssign).Distinct(); foreach (var subId in chosenSubIds) { var linkResult = await productService.AddProductCategoryAsync(productId, subId, stoppingToken); if (linkResult.IsFailed && !linkResult.Errors[0].Message.Contains("exist", StringComparison.OrdinalIgnoreCase)) { logger.LogDebug("Junction note for Product {PId} and Sub Category {CId}: {Msg}", productId, subId, linkResult.Errors[0].Message); } } } logger.LogInformation("Category and Product-Tag Mapping Seeding completed successfully."); } }