using Microsoft.EntityFrameworkCore; using MealPlanner.Data; using MealPlanner.Models; namespace MealPlanner.Services; public class MealPlanService(AppDbContext db, TheMealDbClient mealDbClient, RecipeService recipeService) { public async Task GenerateWeekPlanAsync(string userId, DateOnly weekStart) { // Remove existing plan for this week if present var existing = await db.MealPlans .Include(p => p.Entries) .FirstOrDefaultAsync(p => p.UserId == userId && p.WeekStart == weekStart); if (existing is not null) { db.MealPlans.Remove(existing); await db.SaveChangesAsync(); } var plan = new MealPlan { Id = Guid.NewGuid(), UserId = userId, WeekStart = weekStart, CreatedAt = DateTime.UtcNow, }; // Get own recipes to optionally mix in (up to 3) var ownRecipes = await db.Recipes .Include(r => r.Ingredients) .Where(r => r.UserId == userId) .ToListAsync(); var usedRecipeIds = new HashSet(); var entries = new List(); // Shuffle own recipes and pick up to 3 var ownToUse = ownRecipes .OrderBy(_ => Random.Shared.Next()) .Take(Math.Min(3, ownRecipes.Count)) .ToList(); int ownIndex = 0; for (int dayOffset = 0; dayOffset < 7; dayOffset++) { var date = weekStart.AddDays(dayOffset); Recipe? recipe = null; // Use an own recipe for this slot if available if (ownIndex < ownToUse.Count) { recipe = ownToUse[ownIndex++]; } else { // Fetch random from TheMealDB, retry if duplicate for (int attempt = 0; attempt < 5; attempt++) { var candidate = await mealDbClient.GetRandomAsync(); if (candidate is null) continue; if (candidate.ExternalId is not null && usedRecipeIds.Any(id => db.Recipes.Any(r => r.Id == id && r.ExternalId == candidate.ExternalId))) continue; recipe = await recipeService.EnsurePersistedAsync(candidate); break; } } if (recipe is null) continue; usedRecipeIds.Add(recipe.Id); entries.Add(new MealPlanEntry { Id = Guid.NewGuid(), MealPlanId = plan.Id, Date = date, RecipeId = recipe.Id, }); } plan.Entries = entries; db.MealPlans.Add(plan); await db.SaveChangesAsync(); return await LoadPlanWithDetailsAsync(plan.Id) ?? plan; } public async Task GetCurrentPlanAsync(string userId) { var today = DateOnly.FromDateTime(DateTime.UtcNow); var weekStart = GetWeekStart(today); return await GetPlanAsync(userId, weekStart); } public async Task GetPlanAsync(string userId, DateOnly weekStart) { var plan = await db.MealPlans .Where(p => p.UserId == userId && p.WeekStart == weekStart) .Select(p => p.Id) .FirstOrDefaultAsync(); if (plan == default) return null; return await LoadPlanWithDetailsAsync(plan); } public async Task SwapEntryAsync(Guid entryId, Guid newRecipeId, string userId) { var entry = await db.MealPlanEntries .Include(e => e.MealPlan) .FirstOrDefaultAsync(e => e.Id == entryId); if (entry is null) return null; if (entry.MealPlan.UserId != userId) throw new UnauthorizedAccessException(); var recipe = await db.Recipes.FindAsync(newRecipeId); if (recipe is null) return null; entry.RecipeId = newRecipeId; await db.SaveChangesAsync(); return await db.MealPlanEntries .Include(e => e.Recipe).ThenInclude(r => r.Ingredients) .FirstOrDefaultAsync(e => e.Id == entryId); } public async Task RerollEntryAsync(Guid entryId, string userId) { var entry = await db.MealPlanEntries .Include(e => e.MealPlan).ThenInclude(p => p.Entries) .FirstOrDefaultAsync(e => e.Id == entryId); if (entry is null) return null; if (entry.MealPlan.UserId != userId) throw new UnauthorizedAccessException(); var usedExternalIds = await db.MealPlanEntries .Where(e => e.MealPlanId == entry.MealPlanId) .Include(e => e.Recipe) .Select(e => e.Recipe.ExternalId) .ToListAsync(); Recipe? newRecipe = null; for (int attempt = 0; attempt < 10; attempt++) { var candidate = await mealDbClient.GetRandomAsync(); if (candidate is null) continue; if (usedExternalIds.Contains(candidate.ExternalId)) continue; newRecipe = await recipeService.EnsurePersistedAsync(candidate); break; } if (newRecipe is null) return null; entry.RecipeId = newRecipe.Id; await db.SaveChangesAsync(); return await db.MealPlanEntries .Include(e => e.Recipe).ThenInclude(r => r.Ingredients) .FirstOrDefaultAsync(e => e.Id == entryId); } private async Task LoadPlanWithDetailsAsync(Guid planId) { return await db.MealPlans .Include(p => p.Entries) .ThenInclude(e => e.Recipe) .ThenInclude(r => r.Ingredients) .FirstOrDefaultAsync(p => p.Id == planId); } public static DateOnly GetWeekStart(DateOnly date) { // Monday as week start int diff = ((int)date.DayOfWeek - (int)DayOfWeek.Monday + 7) % 7; return date.AddDays(-diff); } }