using MealPlan.Api.Data;
using MealPlan.Api.DTOs.MealPlan;
using MealPlan.Api.Services.Generators;
using Microsoft.EntityFrameworkCore;

namespace MealPlan.Api.Services;

public class MealPlanService : IMealPlanService
{
    private readonly AppDbContext _db;
    private readonly IRecipeService _recipeService;

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

    public async Task<IReadOnlyList<MealPlanEntryDto>> GetEntriesAsync(
        Guid userId,
        DateOnly from,
        DateOnly to,
        CancellationToken ct = default)
    {
        return await _db.MealPlanEntries.AsNoTracking()
            .Include(e => e.Recipe)
            .Where(e => e.UserId == userId && e.PlanDate >= from && e.PlanDate <= to)
            .OrderBy(e => e.PlanDate)
            .ThenBy(e => e.MealCategory)
            .Select(e => new MealPlanEntryDto
            {
                Id = e.Id,
                PlanDate = e.PlanDate,
                MealCategory = e.MealCategory,
                RecipeId = e.RecipeId,
                RecipeName = e.Recipe!.Name,
                Portions = e.Portions,
                Calories = e.Recipe!.Calories,
            })
            .ToListAsync(ct);
    }

    public async Task<MealPlanEntryDto?> UpsertEntryAsync(
        Guid userId,
        UpsertMealPlanEntryDto dto,
        CancellationToken ct = default)
    {
        var recipeExists = await _db.Recipes.AnyAsync(r => r.Id == dto.RecipeId && r.IsActive, ct);
        if (!recipeExists) return null;

        var existing = await _db.MealPlanEntries
            .Include(e => e.Recipe)
            .FirstOrDefaultAsync(
                e => e.UserId == userId && e.PlanDate == dto.PlanDate && e.MealCategory == dto.MealCategory,
                ct);

        if (existing is null)
        {
            existing = new Models.MealPlanEntry
            {
                UserId = userId,
                PlanDate = dto.PlanDate,
                MealCategory = dto.MealCategory,
                RecipeId = dto.RecipeId,
                Portions = dto.Portions,
                CreatedAt = DateTime.UtcNow,
            };
            _db.MealPlanEntries.Add(existing);
        }
        else
        {
            existing.RecipeId = dto.RecipeId;
            existing.Portions = dto.Portions;
            existing.UpdatedAt = DateTime.UtcNow;
            if (existing.Recipe is null)
            {
                await _db.Entry(existing).Reference(e => e.Recipe).LoadAsync(ct);
            }
        }

        await _db.SaveChangesAsync(ct);
        await _db.Entry(existing).Reference(e => e.Recipe).LoadAsync(ct);

        return new MealPlanEntryDto
        {
            Id = existing.Id,
            PlanDate = existing.PlanDate,
            MealCategory = existing.MealCategory,
            RecipeId = existing.RecipeId,
            RecipeName = existing.Recipe?.Name ?? string.Empty,
            Portions = existing.Portions,
            Calories = existing.Recipe?.Calories,
        };
    }

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

    public async Task<IReadOnlyList<ShoppingListItemDto>> GetShoppingListAsync(
        Guid userId,
        DateOnly from,
        DateOnly to,
        CancellationToken ct = default)
    {
        var entries = await _db.MealPlanEntries.AsNoTracking()
            .Where(e => e.UserId == userId && e.PlanDate >= from && e.PlanDate <= to)
            .ToListAsync(ct);

        var recipeIds = entries.Select(e => e.RecipeId).Distinct().ToList();
        var recipes = new List<DTOs.Recipe.RecipeDetailDto>();
        foreach (var id in recipeIds)
        {
            var recipe = await _recipeService.GetRecipeByIdAsync(id, ct);
            if (recipe is not null) recipes.Add(recipe);
        }

        var buckets = new Dictionary<string, ShoppingBucket>(StringComparer.OrdinalIgnoreCase);

        foreach (var entry in entries)
        {
            var recipe = recipes.FirstOrDefault(r => r.Id == entry.RecipeId);
            if (recipe is null) continue;

            foreach (var ing in recipe.Ingredients)
            {
                if (IngredientScalingHelper.IsNonQuantifiedUnit(ing.Unit))
                    continue;

                var (scaledGrams, _) = IngredientScalingHelper.ScaleForDietPlan(
                    ing.AmountGrams, ing.Unit, entry.Portions);

                if (scaledGrams is not > 0)
                    continue;

                var key = $"g:{NormalizeName(ing.Name)}";

                if (!buckets.TryGetValue(key, out var bucket))
                {
                    bucket = new ShoppingBucket(ing.Name, null);
                    buckets[key] = bucket;
                }

                bucket.Add(recipe.Name, scaledGrams, null);
            }
        }

        return buckets.Values
            .Select(b => new ShoppingListItemDto
            {
                Name = b.Name,
                Unit = b.Unit,
                TotalGrams = b.TotalGrams,
                TotalAmount = b.TotalAmount,
                Breakdown = b.Breakdown,
            })
            .OrderBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
            .ToList();
    }

    private static string NormalizeName(string name) => name.Trim().ToLowerInvariant();

    private sealed class ShoppingBucket
    {
        public string Name { get; }
        public string? Unit { get; }
        public decimal? TotalGrams { get; private set; }
        public decimal? TotalAmount { get; private set; }
        public List<string> Breakdown { get; } = new();

        public ShoppingBucket(string name, string? unit)
        {
            Name = name;
            Unit = unit;
        }

        public void Add(string recipeName, decimal? grams, decimal? amount)
        {
            if (Unit is null && grams.HasValue)
            {
                TotalGrams = (TotalGrams ?? 0) + grams.Value;
                Breakdown.Add($"{recipeName}: {grams.Value:0.##}g");
            }
            else if (amount.HasValue)
            {
                TotalAmount = (TotalAmount ?? 0) + amount.Value;
                var suffix = Unit is null ? "g" : $" {Unit}";
                Breakdown.Add($"{recipeName}: {amount.Value:0.##}{suffix}");
            }
        }
    }
}
