using MealPlan.Api.Data; using MealPlan.Api.Models; using Microsoft.EntityFrameworkCore; namespace MealPlan.Api.Services; public readonly record struct MacroTotals(int Calories, decimal Protein, decimal Fat, decimal Carbs); public static class RecipeMacroCalculator { public static bool IsSpiceUnit(string? unit) { if (string.IsNullOrWhiteSpace(unit)) return false; var u = unit.Trim().ToLowerInvariant(); return u is "do smaku" or "optional" or "opcjonalnie" or "szczypta" or "to taste"; } public static bool IsQuantifiedMacroIngredient(decimal? amountGrams, string? unit, int? catalogItemId) => catalogItemId.HasValue && amountGrams is > 0 && !IsSpiceUnit(unit); public static async Task ComputeFromCatalogAsync( AppDbContext db, IEnumerable<(decimal? AmountGrams, string? Unit, int? CatalogItemId)> ingredients, CancellationToken ct = default) { var rows = ingredients.ToList(); var ids = rows .Where(i => IsQuantifiedMacroIngredient(i.AmountGrams, i.Unit, i.CatalogItemId)) .Select(i => i.CatalogItemId!.Value) .Distinct() .ToList(); if (ids.Count == 0) return null; var catalog = await db.IngredientNutritionCatalog.AsNoTracking() .Where(i => ids.Contains(i.Id) && i.IsActive) .ToDictionaryAsync(i => i.Id, ct); int calories = 0; decimal protein = 0, fat = 0, carbs = 0; foreach (var ing in rows) { if (!IsQuantifiedMacroIngredient(ing.AmountGrams, ing.Unit, ing.CatalogItemId)) continue; if (!catalog.TryGetValue(ing.CatalogItemId!.Value, out var cat)) continue; var scale = ing.AmountGrams!.Value / 100m; calories += (int)Math.Round(cat.CaloriesPer100G * scale); protein += cat.ProteinGPer100G * scale; fat += cat.FatGPer100G * scale; carbs += cat.CarbsGPer100G * scale; } if (calories <= 0) return null; return new MacroTotals(calories, Math.Round(protein, 2), Math.Round(fat, 2), Math.Round(carbs, 2)); } public static decimal ResolveScaleFactor(int currentCalories, int targetCalories, int toleranceKcal) { if (currentCalories <= 0 || targetCalories <= 0) return 1m; if (Math.Abs(currentCalories - targetCalories) <= toleranceKcal) return 1m; return Math.Clamp((decimal)targetCalories / currentCalories, 0.25m, 4m); } public static void ScaleIngredientAmounts( IList ingredients, decimal scaleFactor, int toleranceKcal, int targetCalories, MacroTotals? currentTotals) { if (scaleFactor == 1m) return; if (currentTotals is null || currentTotals.Value.Calories <= 0) return; if (Math.Abs(currentTotals.Value.Calories - targetCalories) <= toleranceKcal) return; foreach (var ing in ingredients) { if (!IsQuantifiedMacroIngredient(ing.AmountGrams, ing.Unit, ing.CatalogItemId)) continue; ing.AmountGrams = Math.Round(ing.AmountGrams!.Value * scaleFactor, 1); } } public static int CountUnlinkedMacroIngredients(IEnumerable ingredients) => ingredients.Count(i => i.AmountGrams is > 0 && !IsSpiceUnit(i.Unit) && !i.CatalogItemId.HasValue); }