using System.Text.Json;
using MealPlan.Api.Data;
using MealPlan.Api.DTOs.Recipe;
using MealPlan.Api.Models;
using MealPlan.Api.Services.AI;
using MealPlan.Api.Services.Generators;
using Microsoft.EntityFrameworkCore;

namespace MealPlan.Api.Services;

public interface IRecipeCalorieRecalculationService
{
    Task<RecalculateRecipeCaloriesResultDto?> RecalculateAsync(
        int recipeId,
        RecalculateRecipeCaloriesRequestDto request,
        CancellationToken ct = default);
}

public class RecipeCalorieRecalculationService : IRecipeCalorieRecalculationService
{
    private readonly AppDbContext _db;
    private readonly IOpenAiChatService _openAi;
    private readonly IRecipeManagementService _recipeManagement;

    public RecipeCalorieRecalculationService(
        AppDbContext db,
        IOpenAiChatService openAi,
        IRecipeManagementService recipeManagement)
    {
        _db = db;
        _openAi = openAi;
        _recipeManagement = recipeManagement;
    }

    public async Task<RecalculateRecipeCaloriesResultDto?> RecalculateAsync(
        int recipeId,
        RecalculateRecipeCaloriesRequestDto request,
        CancellationToken ct = default)
    {
        if (request.TargetCalories is < 50 or > 10000)
        {
            throw new InvalidOperationException("Target calories must be between 50 and 10000.");
        }

        var recipe = await _db.Recipes.AsNoTracking()
            .Include(r => r.Ingredients)
            .Include(r => r.Steps)
            .FirstOrDefaultAsync(r => r.Id == recipeId && r.IsActive, ct);
        if (recipe is null) return null;

        var tolerance = request.CalorieToleranceKcal > 0 ? request.CalorieToleranceKcal : 10;
        var workingIngredients = recipe.Ingredients
            .OrderBy(i => i.SortOrder)
            .Select(CloneIngredient)
            .ToList();

        var methods = new List<string>();
        var currentTotals = await ComputeTotalsAsync(workingIngredients, ct);
        var scaleFactor = RecipeMacroCalculator.ResolveScaleFactor(
            currentTotals?.Calories ?? recipe.Calories ?? 0,
            request.TargetCalories,
            tolerance);

        if (scaleFactor != 1m)
        {
            RecipeMacroCalculator.ScaleIngredientAmounts(
                workingIngredients,
                scaleFactor,
                tolerance,
                request.TargetCalories,
                currentTotals);
            methods.Add("catalog");
        }

        currentTotals = await ComputeTotalsAsync(workingIngredients, ct);
        var needsAi = _openAi.IsConfigured &&
            (currentTotals is null ||
             Math.Abs(currentTotals.Value.Calories - request.TargetCalories) > tolerance ||
             RecipeMacroCalculator.CountUnlinkedMacroIngredients(workingIngredients) > 0);

        if (needsAi)
        {
            await ApplyAiAdjustedAmountsAsync(recipe, workingIngredients, request.TargetCalories, ct);
            methods.Add("ai");
            currentTotals = await ComputeTotalsAsync(workingIngredients, ct);
        }

        if (currentTotals is null && recipe.Calories is > 0)
        {
            var fallbackScale = RecipeMacroCalculator.ResolveScaleFactor(
                recipe.Calories.Value,
                request.TargetCalories,
                tolerance);
            if (fallbackScale != 1m)
            {
                foreach (var ing in workingIngredients)
                {
                    if (ing.AmountGrams is not > 0 || RecipeMacroCalculator.IsSpiceUnit(ing.Unit)) continue;
                    ing.AmountGrams = Math.Round(ing.AmountGrams.Value * fallbackScale, 1);
                }

                if (!methods.Contains("catalog")) methods.Add("catalog");
            }
        }

        currentTotals = await ComputeTotalsAsync(workingIngredients, ct);
        var ingredientDtos = MapIngredients(workingIngredients);
        var unlinked = RecipeMacroCalculator.CountUnlinkedMacroIngredients(workingIngredients);

        RecipeDetailDto? savedRecipe = null;
        if (request.Apply)
        {
            var updateDto = new CreateRecipeDto
            {
                Name = recipe.Name,
                MealCategory = recipe.MealCategory,
                Calories = currentTotals?.Calories ?? request.TargetCalories,
                Protein = currentTotals?.Protein,
                Fat = currentTotals?.Fat,
                Carbs = currentTotals?.Carbs,
                PrepTimeMinutes = recipe.PrepTimeMinutes,
                Ingredients = workingIngredients.Select((i, index) => new CreateIngredientDto
                {
                    Name = i.Name,
                    AmountGrams = i.AmountGrams,
                    Unit = i.Unit,
                    SortOrder = index,
                    CatalogItemId = i.CatalogItemId,
                }).ToList(),
                Steps = recipe.Steps
                    .OrderBy(s => s.StepNumber)
                    .Select(s => new CreateRecipeStepDto
                    {
                        StepNumber = s.StepNumber,
                        Description = s.Description,
                    }).ToList(),
            };

            savedRecipe = await _recipeManagement.UpdateRecipeAsync(recipeId, updateDto, ct);
            if (savedRecipe is null) return null;
        }

        return new RecalculateRecipeCaloriesResultDto
        {
            RecipeId = recipeId,
            PreviousCalories = recipe.Calories,
            TargetCalories = request.TargetCalories,
            NewCalories = currentTotals?.Calories ?? savedRecipe?.Calories,
            NewProtein = currentTotals?.Protein ?? savedRecipe?.Protein,
            NewFat = currentTotals?.Fat ?? savedRecipe?.Fat,
            NewCarbs = currentTotals?.Carbs ?? savedRecipe?.Carbs,
            Applied = request.Apply && savedRecipe is not null,
            Method = methods.Count == 0 ? "none" : string.Join('+', methods),
            UnlinkedIngredientCount = unlinked,
            Ingredients = savedRecipe?.Ingredients ?? ingredientDtos,
            Recipe = savedRecipe,
        };
    }

    private async Task ApplyAiAdjustedAmountsAsync(
        Recipe source,
        IList<Ingredient> ingredients,
        int targetCalories,
        CancellationToken ct)
    {
        var system = """
            You adjust recipe ingredient amounts to match a calorie target for a meal planning app.
            Respond ONLY JSON:
            {"ingredients":[{"name":"exact ingredient name","amountGrams":100,"unit":null}]}
            Rules:
            - Keep every ingredient from the input list (same names).
            - Change amountGrams for macro ingredients so the recipe totals about targetCalories kcal.
            - Use realistic gram amounts. Round to one decimal.
            - Spices with unit "do smaku" / "to taste" keep null amountGrams.
            - Do not add or remove ingredients.
            """;

        var user = JsonSerializer.Serialize(new
        {
            recipeName = source.Name,
            targetCalories,
            currentCalories = source.Calories,
            ingredients = ingredients.Select(i => new
            {
                i.Name,
                i.AmountGrams,
                i.Unit,
                i.CatalogItemId,
            }),
        });

        var json = await _openAi.CompleteJsonAsync(system, user, OpenAiUseCase.Recipe, ct);
        using var doc = JsonDocument.Parse(json);
        if (!doc.RootElement.TryGetProperty("ingredients", out var ingredientsEl) ||
            ingredientsEl.ValueKind != JsonValueKind.Array)
        {
            throw new InvalidOperationException("AI did not return adjusted ingredients. Try again.");
        }

        var byName = ingredients.ToDictionary(i => i.Name.Trim(), StringComparer.OrdinalIgnoreCase);
        foreach (var item in ingredientsEl.EnumerateArray())
        {
            var name = item.TryGetProperty("name", out var nameEl)
                ? AiRecipeJsonParser.GetString(nameEl)
                : null;
            if (string.IsNullOrWhiteSpace(name) || !byName.TryGetValue(name.Trim(), out var target)) continue;

            if (item.TryGetProperty("amountGrams", out var gramsEl))
            {
                target.AmountGrams = AiRecipeJsonParser.GetNullableDecimal(gramsEl);
            }

            if (item.TryGetProperty("unit", out var unitEl) && unitEl.ValueKind != JsonValueKind.Null)
            {
                target.Unit = AiRecipeJsonParser.GetString(unitEl);
            }
        }
    }

    private async Task<MacroTotals?> ComputeTotalsAsync(IEnumerable<Ingredient> ingredients, CancellationToken ct)
    {
        return await RecipeMacroCalculator.ComputeFromCatalogAsync(
            _db,
            ingredients.Select(i => (i.AmountGrams, i.Unit, i.CatalogItemId)),
            ct);
    }

    private static Ingredient CloneIngredient(Ingredient source) => new()
    {
        Id = source.Id,
        Name = source.Name,
        AmountGrams = source.AmountGrams,
        Unit = source.Unit,
        SortOrder = source.SortOrder,
        CatalogItemId = source.CatalogItemId,
    };

    private static List<IngredientDto> MapIngredients(IEnumerable<Ingredient> ingredients) =>
        ingredients
            .OrderBy(i => i.SortOrder)
            .Select(i => new IngredientDto
            {
                Id = i.Id,
                Name = i.Name,
                AmountGrams = i.AmountGrams,
                Unit = i.Unit,
                SortOrder = i.SortOrder,
                CatalogItemId = i.CatalogItemId,
            }).ToList();
}
