using MealPlan.Api.Data;
using MealPlan.Api.DTOs.Diet;
using MealPlan.Api.Models;
using Microsoft.EntityFrameworkCore;

namespace MealPlan.Api.Services;

public class DietTrackingService : IDietTrackingService
{
    private readonly AppDbContext _db;
    private readonly IRecipeService _recipeService;

    public DietTrackingService(AppDbContext db, IRecipeService recipeService)
    {
        _db = db;
        _recipeService = recipeService;
    }

    public async Task<UserDailyGoalsDto?> GetGoalsAsync(Guid userId, CancellationToken ct = default)
    {
        var goals = await _db.UserDailyGoals.AsNoTracking()
            .FirstOrDefaultAsync(g => g.UserId == userId, ct);
        return goals is null ? null : MapGoals(goals);
    }

    public async Task<UserDailyGoalsDto> UpsertGoalsAsync(Guid userId, UserDailyGoalsDto dto, CancellationToken ct = default)
    {
        var existing = await _db.UserDailyGoals.FirstOrDefaultAsync(g => g.UserId == userId, ct);
        if (existing is null)
        {
            existing = new UserDailyGoal
            {
                UserId = userId,
                CreatedAt = DateTime.UtcNow,
            };
            _db.UserDailyGoals.Add(existing);
        }

        existing.CalorieGoal = dto.CalorieGoal;
        existing.ProteinGoalG = dto.ProteinGoalG;
        existing.FatGoalG = dto.FatGoalG;
        existing.CarbsGoalG = dto.CarbsGoalG;
        existing.WaterGoalMl = dto.WaterGoalMl;
        existing.UpdatedAt = DateTime.UtcNow;

        await _db.SaveChangesAsync(ct);
        return MapGoals(existing);
    }

    public async Task<DailySummaryDto> GetDailySummaryAsync(Guid userId, DateOnly date, CancellationToken ct = default)
    {
        var goals = await GetGoalsAsync(userId, ct);
        var meals = await GetMealsAsync(userId, date, ct);
        var drinks = await GetDrinksAsync(userId, date, ct);

        var totalCalories = meals.Sum(m => m.Calories ?? 0);
        var totalProtein = meals.Sum(m => m.ProteinG ?? 0);
        var totalFat = meals.Sum(m => m.FatG ?? 0);
        var totalCarbs = meals.Sum(m => m.CarbsG ?? 0);
        var totalWater = drinks.Sum(d => d.VolumeMl);

        return new DailySummaryDto
        {
            Date = date,
            Goals = goals,
            Calories = BuildIntMacro(goals?.CalorieGoal, totalCalories),
            ProteinG = BuildDecimalMacro(goals?.ProteinGoalG, totalProtein),
            FatG = BuildDecimalMacro(goals?.FatGoalG, totalFat),
            CarbsG = BuildDecimalMacro(goals?.CarbsGoalG, totalCarbs),
            WaterMl = BuildIntMacro(goals?.WaterGoalMl, totalWater),
            Meals = meals,
            Drinks = drinks,
        };
    }

    public async Task<IReadOnlyList<MealConsumptionDto>> GetMealsAsync(Guid userId, DateOnly date, CancellationToken ct = default)
    {
        var rows = await _db.MealConsumptions.AsNoTracking()
            .Where(m => m.UserId == userId && m.LogDate == date)
            .OrderByDescending(m => m.ConsumedAt)
            .ToListAsync(ct);
        return rows.Select(MapMeal).ToList();
    }

    public async Task<MealConsumptionDto?> LogMealAsync(Guid userId, LogMealDto dto, CancellationToken ct = default)
    {
        var consumedAt = dto.ConsumedAt ?? DateTime.UtcNow;
        var logDate = dto.LogDate ?? DateOnly.FromDateTime(consumedAt);

        string name;
        int? calories;
        decimal? protein;
        decimal? fat;
        decimal? carbs;
        int? recipeId = dto.RecipeId;

        if (dto.RecipeId.HasValue)
        {
            var recipe = await _recipeService.GetRecipeByIdAsync(dto.RecipeId.Value, ct);
            if (recipe is null)
            {
                return null;
            }

            name = recipe.Name;
            calories = ScaleInt(recipe.Calories, dto.Portions);
            protein = ScaleDecimal(recipe.Protein, dto.Portions);
            fat = ScaleDecimal(recipe.Fat, dto.Portions);
            carbs = ScaleDecimal(recipe.Carbs, dto.Portions);

            if (dto.Calories.HasValue) calories = dto.Calories;
            if (dto.ProteinG.HasValue) protein = dto.ProteinG;
            if (dto.FatG.HasValue) fat = dto.FatG;
            if (dto.CarbsG.HasValue) carbs = dto.CarbsG;
        }
        else if (dto.CatalogItemId.HasValue && dto.Grams is > 0)
        {
            var catalog = await _db.IngredientNutritionCatalog.AsNoTracking()
                .FirstOrDefaultAsync(i => i.Id == dto.CatalogItemId && i.IsActive, ct);
            if (catalog is null) return null;

            name = string.IsNullOrWhiteSpace(dto.Name) ? catalog.Name : dto.Name.Trim();
            var scale = dto.Grams.Value / 100m;
            calories = (int)Math.Round(catalog.CaloriesPer100G * scale);
            protein = Math.Round(catalog.ProteinGPer100G * scale, 2);
            fat = Math.Round(catalog.FatGPer100G * scale, 2);
            carbs = Math.Round(catalog.CarbsGPer100G * scale, 2);
            recipeId = null;
        }
        else if (!string.IsNullOrWhiteSpace(dto.Name))
        {
            name = dto.Name.Trim();
            calories = dto.Calories;
            protein = dto.ProteinG;
            fat = dto.FatG;
            carbs = dto.CarbsG;
            recipeId = null;
        }
        else
        {
            return null;
        }

        var entity = new MealConsumption
        {
            UserId = userId,
            RecipeId = recipeId,
            MealCategory = dto.MealCategory,
            LogDate = logDate,
            ConsumedAt = consumedAt,
            Name = name,
            Portions = dto.Portions,
            Calories = calories,
            ProteinG = protein,
            FatG = fat,
            CarbsG = carbs,
            Notes = dto.Notes,
            CreatedAt = DateTime.UtcNow,
        };

        _db.MealConsumptions.Add(entity);
        await _db.SaveChangesAsync(ct);
        return MapMeal(entity);
    }

    public async Task<MealPreviewDto?> PreviewMealAsync(
        Guid userId,
        DateOnly date,
        LogMealDto dto,
        CancellationToken ct = default)
    {
        var summary = await GetDailySummaryAsync(userId, date, ct);
        var snapshot = await ResolveMealSnapshotAsync(dto, ct);
        if (snapshot is null) return null;

        return new MealPreviewDto
        {
            Meal = snapshot,
            RemainingAfter = BuildRemainingAfter(summary, snapshot),
        };
    }

    public async Task<IReadOnlyList<DietSuggestionDto>> GetMealSuggestionsAsync(
        Guid userId,
        DateOnly date,
        int limit = 5,
        CancellationToken ct = default)
    {
        limit = Math.Clamp(limit, 1, 10);
        var summary = await GetDailySummaryAsync(userId, date, ct);
        var items = await _db.IngredientNutritionCatalog.AsNoTracking()
            .Include(i => i.Category)
            .Where(i => i.IsActive && i.Category!.IsActive)
            .ToListAsync(ct);

        if (items.Count == 0) return Array.Empty<DietSuggestionDto>();

        var reasonKey = PickPrimaryDeficit(summary);
        const int defaultGrams = 150;

        var scored = items.Select(item =>
        {
            var scale = defaultGrams / 100m;
            var calories = (int)Math.Round(item.CaloriesPer100G * scale);
            var protein = Math.Round(item.ProteinGPer100G * scale, 2);
            var fat = Math.Round(item.FatGPer100G * scale, 2);
            var carbs = Math.Round(item.CarbsGPer100G * scale, 2);
            var score = ScoreSuggestion(summary, reasonKey, calories, protein, fat, carbs);

            return (
                score,
                Suggestion: new DietSuggestionDto
                {
                    CatalogItemId = item.Id,
                    Name = item.Name,
                    NamePl = item.NamePl,
                    CategoryCode = item.Category?.Code ?? string.Empty,
                    SuggestedGrams = defaultGrams,
                    Calories = calories,
                    ProteinG = protein,
                    FatG = fat,
                    CarbsG = carbs,
                    ReasonKey = reasonKey,
                });
        })
        .OrderByDescending(x => x.score)
        .ThenBy(x => x.Suggestion.Name)
        .Take(limit)
        .Select(x => x.Suggestion)
        .ToList();

        return scored;
    }

    private async Task<MealMacroSnapshotDto?> ResolveMealSnapshotAsync(LogMealDto dto, CancellationToken ct)
    {
        if (dto.RecipeId.HasValue)
        {
            var recipe = await _recipeService.GetRecipeByIdAsync(dto.RecipeId.Value, ct);
            if (recipe is null) return null;

            return new MealMacroSnapshotDto
            {
                Name = recipe.Name,
                Portions = dto.Portions,
                Calories = ScaleInt(recipe.Calories, dto.Portions),
                ProteinG = ScaleDecimal(recipe.Protein, dto.Portions),
                FatG = ScaleDecimal(recipe.Fat, dto.Portions),
                CarbsG = ScaleDecimal(recipe.Carbs, dto.Portions),
            };
        }

        if (dto.CatalogItemId.HasValue && dto.Grams is > 0)
        {
            var catalog = await _db.IngredientNutritionCatalog.AsNoTracking()
                .FirstOrDefaultAsync(i => i.Id == dto.CatalogItemId && i.IsActive, ct);
            if (catalog is null) return null;

            var scale = dto.Grams.Value / 100m;
            return new MealMacroSnapshotDto
            {
                Name = string.IsNullOrWhiteSpace(dto.Name) ? catalog.Name : dto.Name.Trim(),
                Portions = 1,
                Calories = (int)Math.Round(catalog.CaloriesPer100G * scale),
                ProteinG = Math.Round(catalog.ProteinGPer100G * scale, 2),
                FatG = Math.Round(catalog.FatGPer100G * scale, 2),
                CarbsG = Math.Round(catalog.CarbsGPer100G * scale, 2),
            };
        }

        if (!string.IsNullOrWhiteSpace(dto.Name))
        {
            return new MealMacroSnapshotDto
            {
                Name = dto.Name.Trim(),
                Portions = dto.Portions,
                Calories = dto.Calories,
                ProteinG = dto.ProteinG,
                FatG = dto.FatG,
                CarbsG = dto.CarbsG,
            };
        }

        return null;
    }

    private static RemainingAfterMealDto BuildRemainingAfter(DailySummaryDto summary, MealMacroSnapshotDto meal)
    {
        int? RemainingInt(int? goal, int consumed, int? mealValue) =>
            goal.HasValue ? Math.Max(goal.Value - consumed - (mealValue ?? 0), 0) : null;

        decimal? RemainingDec(decimal? goal, decimal consumed, decimal? mealValue) =>
            goal.HasValue ? Math.Max(goal.Value - consumed - (mealValue ?? 0), 0) : null;

        return new RemainingAfterMealDto
        {
            Calories = RemainingInt(summary.Calories.Goal, summary.Calories.Consumed, meal.Calories),
            ProteinG = RemainingDec(summary.ProteinG.Goal, summary.ProteinG.Consumed, meal.ProteinG),
            FatG = RemainingDec(summary.FatG.Goal, summary.FatG.Consumed, meal.FatG),
            CarbsG = RemainingDec(summary.CarbsG.Goal, summary.CarbsG.Consumed, meal.CarbsG),
        };
    }

    private static string PickPrimaryDeficit(DailySummaryDto summary)
    {
        var deficits = new List<(string Key, decimal Weight)>();

        if (summary.ProteinG.Goal is > 0 && summary.ProteinG.Remaining is > 0)
        {
            deficits.Add(("highProtein", summary.ProteinG.Remaining.Value / summary.ProteinG.Goal.Value));
        }

        if (summary.CarbsG.Goal is > 0 && summary.CarbsG.Remaining is > 0)
        {
            deficits.Add(("needCarbs", summary.CarbsG.Remaining.Value / summary.CarbsG.Goal.Value));
        }

        if (summary.FatG.Goal is > 0 && summary.FatG.Remaining is > 0)
        {
            deficits.Add(("needFat", summary.FatG.Remaining.Value / summary.FatG.Goal.Value));
        }

        if (summary.Calories.Goal is > 0 && summary.Calories.Remaining is > 0)
        {
            deficits.Add(("needCalories", (decimal)summary.Calories.Remaining.Value / summary.Calories.Goal.Value));
        }

        return deficits.OrderByDescending(d => d.Weight).FirstOrDefault().Key ?? "balanced";
    }

    private static decimal ScoreSuggestion(
        DailySummaryDto summary,
        string reasonKey,
        int calories,
        decimal protein,
        decimal fat,
        decimal carbs)
    {
        if (calories <= 0) return 0;

        return reasonKey switch
        {
            "highProtein" => protein * 2m + protein / calories,
            "needCarbs" => carbs * 2m + carbs / calories,
            "needFat" => fat * 2m + fat / calories,
            "needCalories" => calories,
            _ => protein + carbs + (1000m / Math.Max(calories, 1)),
        };
    }

    public async Task<bool> DeleteMealAsync(Guid userId, int id, CancellationToken ct = default)
    {
        var entity = await _db.MealConsumptions.FirstOrDefaultAsync(m => m.Id == id && m.UserId == userId, ct);
        if (entity is null) return false;
        _db.MealConsumptions.Remove(entity);
        await _db.SaveChangesAsync(ct);
        return true;
    }

    public async Task<IReadOnlyList<DrinkCatalogItemDto>> GetDrinkCatalogAsync(CancellationToken ct = default)
    {
        return await _db.DrinkCatalog.AsNoTracking()
            .Where(d => d.IsActive)
            .OrderBy(d => d.SortOrder)
            .Select(d => new DrinkCatalogItemDto
            {
                Id = d.Id,
                Code = d.Code,
                Name = d.Name,
                DefaultVolumeMl = d.DefaultVolumeMl,
                CaloriesPer100Ml = d.CaloriesPer100Ml,
                IconEmoji = d.IconEmoji,
            })
            .ToListAsync(ct);
    }

    public async Task<IReadOnlyList<DrinkConsumptionDto>> GetDrinksAsync(Guid userId, DateOnly date, CancellationToken ct = default)
    {
        var rows = await _db.DrinkConsumptions.AsNoTracking()
            .Where(d => d.UserId == userId && d.LogDate == date)
            .OrderByDescending(d => d.ConsumedAt)
            .ToListAsync(ct);
        return rows.Select(MapDrink).ToList();
    }

    public async Task<DrinkConsumptionDto?> LogDrinkAsync(Guid userId, LogDrinkDto dto, CancellationToken ct = default)
    {
        var consumedAt = dto.ConsumedAt ?? DateTime.UtcNow;
        var logDate = dto.LogDate ?? DateOnly.FromDateTime(consumedAt);

        string name;
        int? catalogId = dto.DrinkCatalogId;
        int? calories = dto.Calories;

        if (dto.DrinkCatalogId.HasValue)
        {
            var catalog = await _db.DrinkCatalog.AsNoTracking()
                .FirstOrDefaultAsync(d => d.Id == dto.DrinkCatalogId && d.IsActive, ct);
            if (catalog is null) return null;

            name = dto.Name?.Trim() ?? catalog.Name;
            if (!calories.HasValue && catalog.CaloriesPer100Ml.HasValue)
            {
                calories = (int)Math.Round(catalog.CaloriesPer100Ml.Value * dto.VolumeMl / 100m);
            }
        }
        else
        {
            name = dto.Name!.Trim();
            catalogId = null;
        }

        var entity = new DrinkConsumption
        {
            UserId = userId,
            DrinkCatalogId = catalogId,
            LogDate = logDate,
            ConsumedAt = consumedAt,
            Name = name,
            VolumeMl = dto.VolumeMl,
            Calories = calories,
            CreatedAt = DateTime.UtcNow,
        };

        _db.DrinkConsumptions.Add(entity);
        await _db.SaveChangesAsync(ct);
        return MapDrink(entity);
    }

    public async Task<bool> DeleteDrinkAsync(Guid userId, int id, CancellationToken ct = default)
    {
        var entity = await _db.DrinkConsumptions.FirstOrDefaultAsync(d => d.Id == id && d.UserId == userId, ct);
        if (entity is null) return false;
        _db.DrinkConsumptions.Remove(entity);
        await _db.SaveChangesAsync(ct);
        return true;
    }

    public async Task<IReadOnlyList<DietReminderDto>> GetRemindersAsync(Guid userId, CancellationToken ct = default)
    {
        var rows = await _db.DietReminders.AsNoTracking()
            .Where(r => r.UserId == userId)
            .OrderBy(r => r.TimeOfDay)
            .ToListAsync(ct);
        return rows.Select(MapReminder).ToList();
    }

    public async Task<DietReminderDto?> CreateReminderAsync(Guid userId, CreateDietReminderDto dto, CancellationToken ct = default)
    {
        var entity = new DietReminder
        {
            UserId = userId,
            ReminderType = dto.ReminderType,
            Title = dto.Title.Trim(),
            Message = dto.Message?.Trim(),
            TimeOfDay = dto.TimeOfDay,
            DaysOfWeekMask = dto.DaysOfWeekMask,
            MealCategory = dto.MealCategory,
            IsEnabled = dto.IsEnabled,
            CreatedAt = DateTime.UtcNow,
        };

        _db.DietReminders.Add(entity);
        await _db.SaveChangesAsync(ct);
        return MapReminder(entity);
    }

    public async Task<DietReminderDto?> UpdateReminderAsync(Guid userId, int id, UpdateDietReminderDto dto, CancellationToken ct = default)
    {
        var entity = await _db.DietReminders.FirstOrDefaultAsync(r => r.Id == id && r.UserId == userId, ct);
        if (entity is null) return null;

        entity.ReminderType = dto.ReminderType;
        entity.Title = dto.Title.Trim();
        entity.Message = dto.Message?.Trim();
        entity.TimeOfDay = dto.TimeOfDay;
        entity.DaysOfWeekMask = dto.DaysOfWeekMask;
        entity.MealCategory = dto.MealCategory;
        entity.IsEnabled = dto.IsEnabled;
        entity.UpdatedAt = DateTime.UtcNow;

        await _db.SaveChangesAsync(ct);
        return MapReminder(entity);
    }

    public async Task<bool> DeleteReminderAsync(Guid userId, int id, CancellationToken ct = default)
    {
        var entity = await _db.DietReminders.FirstOrDefaultAsync(r => r.Id == id && r.UserId == userId, ct);
        if (entity is null) return false;
        _db.DietReminders.Remove(entity);
        await _db.SaveChangesAsync(ct);
        return true;
    }

    public async Task<IReadOnlyList<DueReminderDto>> GetDueRemindersAsync(
        Guid userId,
        DateOnly? date,
        TimeOnly? time,
        CancellationToken ct = default)
    {
        var targetDate = date ?? DateOnly.FromDateTime(DateTime.UtcNow);
        var targetTime = time ?? TimeOnly.FromDateTime(DateTime.UtcNow);
        var dayOfWeek = targetDate.ToDateTime(TimeOnly.MinValue).DayOfWeek;

        var reminders = await _db.DietReminders.AsNoTracking()
            .Where(r => r.UserId == userId && r.IsEnabled)
            .ToListAsync(ct);

        return reminders
            .Where(r => ReminderDaysMask.IncludesDay(r.DaysOfWeekMask, dayOfWeek))
            .Where(r => r.TimeOfDay.Hour == targetTime.Hour && r.TimeOfDay.Minute == targetTime.Minute)
            .Select(r => new DueReminderDto
            {
                Id = r.Id,
                ReminderType = r.ReminderType,
                Title = r.Title,
                Message = r.Message,
                TimeOfDay = r.TimeOfDay,
                MealCategory = r.MealCategory,
            })
            .ToList();
    }

    public async Task<IReadOnlyList<HistoryDayDto>> GetHistoryAsync(
        Guid userId,
        DateOnly from,
        DateOnly to,
        CancellationToken ct = default)
    {
        var meals = await _db.MealConsumptions.AsNoTracking()
            .Where(m => m.UserId == userId && m.LogDate >= from && m.LogDate <= to)
            .GroupBy(m => m.LogDate)
            .Select(g => new
            {
                Date = g.Key,
                Calories = g.Sum(m => m.Calories ?? 0),
                ProteinG = g.Sum(m => m.ProteinG ?? 0),
                FatG = g.Sum(m => m.FatG ?? 0),
                CarbsG = g.Sum(m => m.CarbsG ?? 0),
                MealCount = g.Count(),
            })
            .ToListAsync(ct);

        var drinks = await _db.DrinkConsumptions.AsNoTracking()
            .Where(d => d.UserId == userId && d.LogDate >= from && d.LogDate <= to)
            .GroupBy(d => d.LogDate)
            .Select(g => new { Date = g.Key, WaterMl = g.Sum(d => d.VolumeMl) })
            .ToListAsync(ct);

        var drinkMap = drinks.ToDictionary(d => d.Date, d => d.WaterMl);
        var days = new List<HistoryDayDto>();
        for (var d = from; d <= to; d = d.AddDays(1))
        {
            var meal = meals.FirstOrDefault(m => m.Date == d);
            days.Add(new HistoryDayDto
            {
                Date = d,
                Calories = meal?.Calories ?? 0,
                ProteinG = meal?.ProteinG ?? 0,
                FatG = meal?.FatG ?? 0,
                CarbsG = meal?.CarbsG ?? 0,
                WaterMl = drinkMap.GetValueOrDefault(d, 0),
                MealCount = meal?.MealCount ?? 0,
            });
        }

        return days;
    }

    public async Task<IReadOnlyList<RecentMealDto>> GetRecentMealsAsync(
        Guid userId,
        int limit = 10,
        CancellationToken ct = default)
    {
        var rows = await _db.MealConsumptions.AsNoTracking()
            .Where(m => m.UserId == userId)
            .OrderByDescending(m => m.ConsumedAt)
            .Take(Math.Clamp(limit * 3, 10, 100))
            .ToListAsync(ct);

        var seen = new HashSet<string>();
        var result = new List<RecentMealDto>();
        foreach (var m in rows)
        {
            var key = m.RecipeId.HasValue
                ? $"r:{m.RecipeId}:{m.MealCategory}"
                : $"n:{m.Name.ToLowerInvariant()}:{m.MealCategory}";
            if (!seen.Add(key)) continue;

            result.Add(new RecentMealDto
            {
                RecipeId = m.RecipeId,
                Name = m.Name,
                MealCategory = m.MealCategory,
                Portions = m.Portions,
                Calories = m.Calories,
                ProteinG = m.ProteinG,
                FatG = m.FatG,
                CarbsG = m.CarbsG,
                LastLoggedAt = m.ConsumedAt,
            });
            if (result.Count >= limit) break;
        }

        return result;
    }

    public async Task<CopyMealsResultDto?> CopyYesterdayMealsAsync(
        Guid userId,
        DateOnly targetDate,
        CancellationToken ct = default)
    {
        var sourceDate = targetDate.AddDays(-1);
        var sourceMeals = await _db.MealConsumptions
            .Where(m => m.UserId == userId && m.LogDate == sourceDate)
            .ToListAsync(ct);

        if (sourceMeals.Count == 0)
        {
            return new CopyMealsResultDto
            {
                SourceDate = sourceDate,
                TargetDate = targetDate,
                CopiedCount = 0,
            };
        }

        var now = DateTime.UtcNow;
        foreach (var src in sourceMeals)
        {
            _db.MealConsumptions.Add(new MealConsumption
            {
                UserId = userId,
                RecipeId = src.RecipeId,
                MealCategory = src.MealCategory,
                LogDate = targetDate,
                ConsumedAt = now,
                Name = src.Name,
                Portions = src.Portions,
                Calories = src.Calories,
                ProteinG = src.ProteinG,
                FatG = src.FatG,
                CarbsG = src.CarbsG,
                Notes = src.Notes,
                CreatedAt = now,
            });
        }

        await _db.SaveChangesAsync(ct);
        return new CopyMealsResultDto
        {
            SourceDate = sourceDate,
            TargetDate = targetDate,
            CopiedCount = sourceMeals.Count,
        };
    }

    public async Task<IReadOnlyList<FavoriteRecipeDto>> GetFavoriteRecipesAsync(
        Guid userId,
        CancellationToken ct = default)
    {
        return await _db.UserFavoriteRecipes.AsNoTracking()
            .Include(f => f.Recipe)
            .Where(f => f.UserId == userId && f.Recipe!.IsActive)
            .OrderBy(f => f.Recipe!.Name)
            .Select(f => new FavoriteRecipeDto
            {
                RecipeId = f.RecipeId,
                Name = f.Recipe!.Name,
                MealCategory = f.Recipe!.MealCategory,
                Calories = f.Recipe!.Calories,
            })
            .ToListAsync(ct);
    }

    public async Task<FavoriteRecipeDto?> AddFavoriteRecipeAsync(
        Guid userId,
        int recipeId,
        CancellationToken ct = default)
    {
        var recipe = await _db.Recipes.AsNoTracking()
            .FirstOrDefaultAsync(r => r.Id == recipeId && r.IsActive, ct);
        if (recipe is null) return null;

        var exists = await _db.UserFavoriteRecipes
            .AnyAsync(f => f.UserId == userId && f.RecipeId == recipeId, ct);
        if (!exists)
        {
            _db.UserFavoriteRecipes.Add(new UserFavoriteRecipe
            {
                UserId = userId,
                RecipeId = recipeId,
                CreatedAt = DateTime.UtcNow,
            });
            await _db.SaveChangesAsync(ct);
        }

        return new FavoriteRecipeDto
        {
            RecipeId = recipe.Id,
            Name = recipe.Name,
            MealCategory = recipe.MealCategory,
            Calories = recipe.Calories,
        };
    }

    public async Task<bool> RemoveFavoriteRecipeAsync(Guid userId, int recipeId, CancellationToken ct = default)
    {
        var fav = await _db.UserFavoriteRecipes
            .FirstOrDefaultAsync(f => f.UserId == userId && f.RecipeId == recipeId, ct);
        if (fav is null) return false;
        _db.UserFavoriteRecipes.Remove(fav);
        await _db.SaveChangesAsync(ct);
        return true;
    }

    public async Task<IReadOnlyList<FavoriteCatalogItemDto>> GetFavoriteCatalogItemsAsync(
        Guid userId,
        CancellationToken ct = default)
    {
        return await _db.UserFavoriteCatalogItems.AsNoTracking()
            .Include(f => f.CatalogItem)
            .ThenInclude(c => c!.Category)
            .Where(f => f.UserId == userId && f.CatalogItem!.IsActive)
            .OrderBy(f => f.CatalogItem!.Name)
            .Select(f => new FavoriteCatalogItemDto
            {
                CatalogItemId = f.CatalogItemId,
                Name = f.CatalogItem!.Name,
                NamePl = f.CatalogItem!.NamePl,
                CategoryCode = f.CatalogItem!.Category!.Code,
            })
            .ToListAsync(ct);
    }

    public async Task<FavoriteCatalogItemDto?> AddFavoriteCatalogItemAsync(
        Guid userId,
        int catalogItemId,
        CancellationToken ct = default)
    {
        var item = await _db.IngredientNutritionCatalog.AsNoTracking()
            .Include(i => i.Category)
            .FirstOrDefaultAsync(i => i.Id == catalogItemId && i.IsActive, ct);
        if (item is null) return null;

        var exists = await _db.UserFavoriteCatalogItems
            .AnyAsync(f => f.UserId == userId && f.CatalogItemId == catalogItemId, ct);
        if (!exists)
        {
            _db.UserFavoriteCatalogItems.Add(new UserFavoriteCatalogItem
            {
                UserId = userId,
                CatalogItemId = catalogItemId,
                CreatedAt = DateTime.UtcNow,
            });
            await _db.SaveChangesAsync(ct);
        }

        return new FavoriteCatalogItemDto
        {
            CatalogItemId = item.Id,
            Name = item.Name,
            NamePl = item.NamePl,
            CategoryCode = item.Category?.Code ?? string.Empty,
        };
    }

    public async Task<bool> RemoveFavoriteCatalogItemAsync(
        Guid userId,
        int catalogItemId,
        CancellationToken ct = default)
    {
        var fav = await _db.UserFavoriteCatalogItems
            .FirstOrDefaultAsync(f => f.UserId == userId && f.CatalogItemId == catalogItemId, ct);
        if (fav is null) return false;
        _db.UserFavoriteCatalogItems.Remove(fav);
        await _db.SaveChangesAsync(ct);
        return true;
    }

    private static UserDailyGoalsDto MapGoals(UserDailyGoal g) => new()
    {
        CalorieGoal = g.CalorieGoal,
        ProteinGoalG = g.ProteinGoalG,
        FatGoalG = g.FatGoalG,
        CarbsGoalG = g.CarbsGoalG,
        WaterGoalMl = g.WaterGoalMl,
    };

    private static MealConsumptionDto MapMeal(MealConsumption m) => new()
    {
        Id = m.Id,
        RecipeId = m.RecipeId,
        MealCategory = m.MealCategory,
        LogDate = m.LogDate,
        ConsumedAt = m.ConsumedAt,
        Name = m.Name,
        Portions = m.Portions,
        Calories = m.Calories,
        ProteinG = m.ProteinG,
        FatG = m.FatG,
        CarbsG = m.CarbsG,
        Notes = m.Notes,
    };

    private static DrinkConsumptionDto MapDrink(DrinkConsumption d) => new()
    {
        Id = d.Id,
        DrinkCatalogId = d.DrinkCatalogId,
        LogDate = d.LogDate,
        ConsumedAt = d.ConsumedAt,
        Name = d.Name,
        VolumeMl = d.VolumeMl,
        Calories = d.Calories,
    };

    private static DietReminderDto MapReminder(DietReminder r) => new()
    {
        Id = r.Id,
        ReminderType = r.ReminderType,
        Title = r.Title,
        Message = r.Message,
        TimeOfDay = r.TimeOfDay,
        DaysOfWeekMask = r.DaysOfWeekMask,
        MealCategory = r.MealCategory,
        IsEnabled = r.IsEnabled,
    };

    private static MacroRemainingDto BuildIntMacro(int? goal, int consumed) => new()
    {
        Goal = goal,
        Consumed = consumed,
        Remaining = goal.HasValue ? Math.Max(goal.Value - consumed, 0) : null,
        ProgressPercent = goal is > 0 ? Math.Round(consumed * 100m / goal.Value, 1) : null,
    };

    private static DecimalMacroRemainingDto BuildDecimalMacro(decimal? goal, decimal consumed) => new()
    {
        Goal = goal,
        Consumed = consumed,
        Remaining = goal.HasValue ? Math.Max(goal.Value - consumed, 0) : null,
        ProgressPercent = goal is > 0 ? Math.Round(consumed * 100m / goal.Value, 1) : null,
    };

    private static int? ScaleInt(int? value, decimal portions) =>
        value.HasValue ? (int)Math.Round(value.Value * portions) : null;

    private static decimal? ScaleDecimal(decimal? value, decimal portions) =>
        value.HasValue ? Math.Round(value.Value * portions, 2) : null;
}
