using MealPlan.Api.Data; using Microsoft.EntityFrameworkCore; namespace MealPlan.Api.Services.Generators; public interface ICatalogMatchingService { Task ResolveCatalogItemIdAsync(string nameOrCatalogName, CancellationToken ct = default); } public class CatalogMatchingService : ICatalogMatchingService { private readonly AppDbContext _db; private List? _cache; public CatalogMatchingService(AppDbContext db) => _db = db; public async Task ResolveCatalogItemIdAsync(string nameOrCatalogName, CancellationToken ct = default) { if (string.IsNullOrWhiteSpace(nameOrCatalogName)) return null; var cache = await GetCacheAsync(ct); var key = Normalize(nameOrCatalogName); var exact = cache.FirstOrDefault(c => c.NameKey == key || c.NamePlKey == key || c.CatalogNameKey == key); if (exact is not null) return exact.Id; return cache .Where(c => key.Contains(c.NameKey, StringComparison.Ordinal) || c.NameKey.Contains(key, StringComparison.Ordinal) || (!string.IsNullOrEmpty(c.NamePlKey) && (key.Contains(c.NamePlKey, StringComparison.Ordinal) || c.NamePlKey.Contains(key, StringComparison.Ordinal)))) .OrderBy(c => Math.Abs(c.NameKey.Length - key.Length)) .FirstOrDefault() ?.Id; } private async Task> GetCacheAsync(CancellationToken ct) { if (_cache is not null) return _cache; _cache = await _db.IngredientNutritionCatalog.AsNoTracking() .Where(i => i.IsActive) .Select(i => new CatalogEntry(i.Id, i.Name, i.NamePl)) .ToListAsync(ct); return _cache; } private static string Normalize(string value) => value.Trim().ToLowerInvariant(); private sealed record CatalogEntry(int Id, string Name, string? NamePl) { public string NameKey { get; } = Name.Trim().ToLowerInvariant(); public string NamePlKey { get; } = NamePl?.Trim().ToLowerInvariant() ?? string.Empty; public string CatalogNameKey { get; } = Name.Trim().ToLowerInvariant(); } }