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

namespace MealPlan.Api.Services;

/// <summary>Creates and updates recipes in the existing PostgreSQL schema.</summary>
public class RecipeManagementService : IRecipeManagementService
{
    private readonly AppDbContext _db;
    private readonly IRecipeService _recipeService;

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

    public async Task<RecipeDetailDto> CreateRecipeAsync(CreateRecipeDto dto, CancellationToken ct = default)
    {
        var recipe = MapToEntity(dto);
        recipe.CreatedAt = DateTime.UtcNow;
        recipe.IsActive = true;

        await ApplyMacrosFromCatalogAsync(recipe, ct);

        _db.Recipes.Add(recipe);
        await _db.SaveChangesAsync(ct);

        return (await _recipeService.GetRecipeByIdAsync(recipe.Id, ct))!;
    }

    public async Task<RecipeDetailDto?> UpdateRecipeAsync(int id, CreateRecipeDto dto, CancellationToken ct = default)
    {
        var recipe = await _db.Recipes
            .Include(r => r.Ingredients)
            .Include(r => r.Steps)
            .FirstOrDefaultAsync(r => r.Id == id && r.IsActive, ct);

        if (recipe is null)
        {
            return null;
        }

        recipe.Name = dto.Name.Trim();
        recipe.MealCategory = dto.MealCategory;
        recipe.Calories = dto.Calories;
        recipe.Protein = dto.Protein;
        recipe.Fat = dto.Fat;
        recipe.Carbs = dto.Carbs;
        recipe.PrepTimeMinutes = dto.PrepTimeMinutes;
        recipe.UpdatedAt = DateTime.UtcNow;

        _db.Ingredients.RemoveRange(recipe.Ingredients);
        _db.RecipeSteps.RemoveRange(recipe.Steps);

        recipe.Ingredients = MapIngredients(dto.Ingredients);
        recipe.Steps = MapSteps(dto.Steps);

        await ApplyMacrosFromCatalogAsync(recipe, ct);

        await _db.SaveChangesAsync(ct);
        return await _recipeService.GetRecipeByIdAsync(id, ct);
    }

    public async Task<RecipeMacroRecalcResultDto> RecalculateAllMacrosFromCatalogAsync(CancellationToken ct = default)
    {
        var catalog = await _db.IngredientNutritionCatalog.AsNoTracking()
            .Where(i => i.IsActive)
            .ToDictionaryAsync(i => i.Id, ct);

        var recipes = await _db.Recipes
            .Include(r => r.Ingredients)
            .Where(r => r.IsActive)
            .ToListAsync(ct);

        var updated = 0;
        foreach (var recipe in recipes)
        {
            if (TrySetMacrosFromCatalog(recipe, catalog, overwrite: true))
            {
                recipe.UpdatedAt = DateTime.UtcNow;
                updated++;
            }
        }

        await _db.SaveChangesAsync(ct);

        var unlinked = await _db.Ingredients.CountAsync(i =>
            i.AmountGrams > 0 &&
            i.CatalogItemId == null &&
            (i.Unit == null || !HasNonMacroUnit(i.Unit)), ct);

        return new RecipeMacroRecalcResultDto
        {
            RecipesProcessed = recipes.Count,
            RecipesUpdated = updated,
            UnlinkedIngredientRows = unlinked,
        };
    }

    internal static Recipe MapToEntity(CreateRecipeDto dto) =>
        new()
        {
            Name = dto.Name.Trim(),
            MealCategory = dto.MealCategory,
            Calories = dto.Calories,
            Protein = dto.Protein,
            Fat = dto.Fat,
            Carbs = dto.Carbs,
            PrepTimeMinutes = dto.PrepTimeMinutes,
            Ingredients = MapIngredients(dto.Ingredients),
            Steps = MapSteps(dto.Steps),
        };

    private static List<Ingredient> MapIngredients(IEnumerable<CreateIngredientDto> items) =>
        items.Select((item, index) => new Ingredient
        {
            Name = item.Name.Trim(),
            AmountGrams = item.AmountGrams,
            Unit = string.IsNullOrWhiteSpace(item.Unit) ? null : item.Unit.Trim(),
            SortOrder = item.SortOrder > 0 ? item.SortOrder : index,
            CatalogItemId = item.CatalogItemId,
        }).ToList();

    private async Task ApplyMacrosFromCatalogAsync(Recipe recipe, CancellationToken ct)
    {
        var catalogIds = recipe.Ingredients
            .Where(i => i.CatalogItemId.HasValue && i.AmountGrams is > 0 && !HasNonMacroUnit(i.Unit))
            .Select(i => i.CatalogItemId!.Value)
            .Distinct()
            .ToList();

        if (catalogIds.Count == 0) return;

        var catalog = await _db.IngredientNutritionCatalog.AsNoTracking()
            .Where(i => catalogIds.Contains(i.Id) && i.IsActive)
            .ToDictionaryAsync(i => i.Id, ct);

        TrySetMacrosFromCatalog(recipe, catalog, overwrite: false);
    }

    private static bool TrySetMacrosFromCatalog(
        Recipe recipe,
        IReadOnlyDictionary<int, IngredientNutritionCatalogItem> catalog,
        bool overwrite)
    {
        int totalCalories = 0;
        decimal totalProtein = 0;
        decimal totalFat = 0;
        decimal totalCarbs = 0;
        var hasAny = false;

        foreach (var ing in recipe.Ingredients)
        {
            if (ing.CatalogItemId is null || ing.AmountGrams is not > 0 || HasNonMacroUnit(ing.Unit)) continue;
            if (!catalog.TryGetValue(ing.CatalogItemId.Value, out var cat)) continue;

            var scale = ing.AmountGrams.Value / 100m;
            totalCalories += (int)Math.Round(cat.CaloriesPer100G * scale);
            totalProtein += cat.ProteinGPer100G * scale;
            totalFat += cat.FatGPer100G * scale;
            totalCarbs += cat.CarbsGPer100G * scale;
            hasAny = true;
        }

        if (!hasAny) return false;

        if (overwrite || !recipe.Calories.HasValue) recipe.Calories = totalCalories;
        if (overwrite || !recipe.Protein.HasValue) recipe.Protein = Math.Round(totalProtein, 2);
        if (overwrite || !recipe.Fat.HasValue) recipe.Fat = Math.Round(totalFat, 2);
        if (overwrite || !recipe.Carbs.HasValue) recipe.Carbs = Math.Round(totalCarbs, 2);
        return true;
    }

    private static bool HasNonMacroUnit(string? unit)
    {
        if (string.IsNullOrWhiteSpace(unit)) return false;
        var u = unit.Trim().ToLowerInvariant();
        return u is "do smaku" or "optional" or "opcjonalnie" or "szczypta";
    }

    private static List<RecipeStep> MapSteps(IEnumerable<CreateRecipeStepDto> items) =>
        items.Select(item => new RecipeStep
        {
            StepNumber = item.StepNumber,
            Description = item.Description.Trim(),
        }).ToList();
}
