// Controllers/HelloFreshController.cs
using Microsoft.AspNetCore.Mvc;
using administration.Models;
using administration.Models.HelloFresh;
using Microsoft.AspNetCore.Authorization;
using System.Text.RegularExpressions;
using System.Text.Json;
using administration.Models.HelloFresh.DTO;
using administration.Models.DTO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using static Azure.Core.HttpHeader;
using Microsoft.EntityFrameworkCore;
using System.Security.Claims;

namespace administration.Controllers;

[Authorize]
[Route("HelloFresh")]
public class HelloFreshController : AppController
{
    private readonly HelloFreshContext _context;
    private readonly LayoutDataContext _layout;
    private readonly IHttpClientFactory _httpFactory;

    [ActivatorUtilitiesConstructor]
    public HelloFreshController(HelloFreshContext context, LayoutDataContext layout, IHttpClientFactory httpClientFactory)
    {
        _context = context;
        _layout = layout;
        _httpFactory = httpClientFactory;
    }

    [HttpGet("")]
    public IActionResult Index() => View();

    [HttpGet("ingredients")]
    public IActionResult Ingredients() => View();

    [HttpGet("historique")]
    public IActionResult Historique() => View(); // Views/HelloFresh/History.cshtml

    [HttpGet("cuisine")]
    public IActionResult Cuisine() => View(); // Views/HelloFresh/History.cshtml

    #region Recettes
    [HttpGet("GetRecipesByPage")]
    public IActionResult GetRecipesByPage([FromQuery] int page = 1, [FromQuery] int count = 12, [FromQuery] string? search = null)
    {
        try
        {
            if (page < 1 || count < 1)
                return BadRequest("Les paramètres 'page' et 'count' doivent être supérieurs à 0.");

            var q = _context.Recettes.AsQueryable();
            if (!string.IsNullOrWhiteSpace(search))
            {
                var s = search.Trim().ToLowerInvariant();
                q = q.Where(r => r.Name.ToLower().Contains(s));
            }

            var totalItems = q.Count();
            var lastPage = Math.Max(1, (int)Math.Ceiling((double)totalItems / count));
            if (page > lastPage && totalItems > 0) page = lastPage;

            var recettes = q.OrderByDescending(r => r.Id)
                            .Skip((page - 1) * count)
                            .Take(count)
                            .ToList();

            return Ok(new
            {
                recipes = recettes,
                currentPage = page,
                lastPage,
                totalItems,
                pageSize = count
            });
        }
        catch (Exception ex)
        {
            return StatusCode(500, new { message = "Erreur serveur", error = ex.Message });
        }
    }

    // ❌ Supprime la limite de 3 portions
    [HttpPost("SaveRecipe")]
    public IActionResult SaveRecipe([FromBody] string idSavingRecette)
    {
        try
        {
            var userId = HttpContext.Session.GetInt32("UserId");
            if (userId == null) return Unauthorized("Utilisateur non connecté.");
            if (string.IsNullOrWhiteSpace(idSavingRecette)) return BadRequest("Id invalide.");

            var current = _context.SavingRecettes.Count(s => s.UserId == userId && s.IdSavingRecette == idSavingRecette);

            _context.SavingRecettes.Add(new SavingRecette { IdSavingRecette = idSavingRecette, UserId = userId.Value });
            _context.SaveChanges();

            return Ok(new { message = "added", qty = current + 1 });
        }
        catch (Exception ex)
        {
            return StatusCode(500, new { message = "Erreur serveur", error = ex.Message });
        }
    }

    [HttpGet("GetRecipesOwned")]
    public IActionResult GetRecipesOwned([FromQuery] bool isUserImportant = true)
    {
        try
        {
            var sessionUserId = HttpContext.Session.GetInt32("UserId");
            if (sessionUserId == null) return Unauthorized("Utilisateur non connecté.");

            static string Key(string? s) => (s ?? "").Trim().ToLowerInvariant();

            if (isUserImportant)
            {
                var rows = (
                    from s in _context.SavingRecettes.AsNoTracking()
                    where s.UserId == sessionUserId
                    join r in _context.Recettes.AsNoTracking() on s.IdSavingRecette equals r.Id
                    select new
                    {
                        r.Id,
                        r.Name,
                        r.Image,
                        r.TempsDePreparation,
                        r.Tags,
                        r.Pdf,
                        Portion = 1
                    }
                ).ToList();

                var result = rows
                    .GroupBy(x => Key(x.Name))
                    .Select(g =>
                    {
                        var withPdf = g.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.Pdf));
                        var pick = withPdf ?? g.OrderByDescending(x => x.Id).First();

                        return new
                        {
                            id = pick.Id,
                            name = pick.Name,
                            image = pick.Image,
                            tempsDePreparation = pick.TempsDePreparation,
                            portions = g.Sum(x => x.Portion),
                            tags = pick.Tags,
                            pdf = pick.Pdf
                        };
                    })
                    .OrderBy(x => x.name)
                    .ToList();

                return Ok(result);
            }
            else
            {
                var rows = (
                    from s in _context.SavingRecettes.AsNoTracking()
                    join r in _context.Recettes.AsNoTracking() on s.IdSavingRecette equals r.Id
                    join u in _layout.Users.AsNoTracking() on s.UserId equals u.Id
                    select new
                    {
                        r.Id,
                        r.Name,
                        r.Image,
                        r.TempsDePreparation,
                        r.Tags,
                        r.Pdf,
                        User = u.Username,
                        Portion = 1
                    }
                ).ToList();

                var result = rows
                    .GroupBy(x => Key(x.Name))
                    .Select(g =>
                    {
                        var withPdf = g.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.Pdf));
                        var pick = withPdf ?? g.OrderByDescending(x => x.Id).First();

                        return new
                        {
                            id = pick.Id,
                            name = pick.Name,
                            image = pick.Image,
                            tempsDePreparation = pick.TempsDePreparation,
                            portions = g.Sum(x => x.Portion),
                            tags = pick.Tags,
                            pdf = pick.Pdf,
                            users = g.Select(x => x.User).Distinct().ToList()
                        };
                    })
                    .OrderBy(x => x.name)
                    .ToList();

                return Ok(result);
            }
        }
        catch (Exception ex)
        {
            return StatusCode(500, new { message = "Erreur serveur", error = ex.Message });
        }
    }

    [HttpPost("DeleteRecipesOwned")]
    public IActionResult DeleteRecipesOwned([FromBody] string idSavingRecette)
    {
        try
        {
            var userId = HttpContext.Session.GetInt32("UserId");
            if (userId == null) return Unauthorized("Utilisateur non connecté.");

            var row = _context.SavingRecettes
                .Where(s => s.UserId == userId && s.IdSavingRecette == idSavingRecette)
                .OrderByDescending(s => s.Id)
                .FirstOrDefault();

            if (row == null) return Ok(new { message = "empty", qty = 0 });

            _context.SavingRecettes.Remove(row);
            _context.SaveChanges();

            var remaining = _context.SavingRecettes.Count(s => s.UserId == userId && s.IdSavingRecette == idSavingRecette);
            return Ok(new { message = "removed", qty = remaining });
        }
        catch (Exception ex)
        {
            return StatusCode(500, new { message = "Erreur serveur", error = ex.Message });
        }
    }

    [HttpPost("ClearRecipeOwned")]
    public IActionResult ClearRecipeOwned([FromBody] string idSavingRecette)
    {
        try
        {
            var userId = HttpContext.Session.GetInt32("UserId");
            if (userId == null) return Unauthorized("Utilisateur non connecté.");
            if (string.IsNullOrWhiteSpace(idSavingRecette)) return BadRequest("Id invalide.");

            var rows = _context.SavingRecettes.Where(s => s.UserId == userId && s.IdSavingRecette == idSavingRecette).ToList();
            if (rows.Count == 0) return Ok(new { message = "empty", qty = 0 });

            _context.SavingRecettes.RemoveRange(rows);
            _context.SaveChanges();

            return Ok(new { message = "cleared", qty = 0 });
        }
        catch (Exception ex)
        {
            return StatusCode(500, new { message = "Erreur serveur", error = ex.Message });
        }
    }
    #endregion

    [AllowAnonymous]
    [HttpGet("ProxyPdf")]
    public async Task<IActionResult> ProxyPdf([FromQuery] string url, [FromServices] IHttpClientFactory httpClientFactory)
    {
        if (string.IsNullOrWhiteSpace(url)) return BadRequest("URL manquante.");
        if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) return BadRequest("URL invalide.");

        var allowedHosts = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
        {
            "www.hellofresh.fr","hellofresh.fr","img.hellofresh.com","assets.hellofresh.com",
            "cdn.hellofresh.com","images.ctfassets.net"
        };
        if (!allowedHosts.Contains(uri.Host)) return BadRequest($"Domaine non autorisé: {uri.Host}");

        try
        {
            var client = httpClientFactory.CreateClient();
            client.Timeout = TimeSpan.FromSeconds(30);
            client.DefaultRequestHeaders.UserAgent.ParseAdd(
                "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36");
            client.DefaultRequestHeaders.Accept.ParseAdd("application/pdf");
            client.DefaultRequestHeaders.AcceptLanguage.ParseAdd("fr-FR,fr;q=0.9,en;q=0.8");

            using var resp = await client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, HttpContext.RequestAborted);
            if (!resp.IsSuccessStatusCode)
            {
                var preview = await resp.Content.ReadAsStringAsync();
                if (preview?.Length > 500) preview = preview[..500];
                return StatusCode((int)resp.StatusCode,
                    $"Téléchargement impossible. Code distant: {(int)resp.StatusCode}. Aperçu: {preview}");
            }

            var mediaType = resp.Content.Headers.ContentType?.MediaType ?? "application/pdf";
            var isPdf = mediaType.Contains("pdf", StringComparison.OrdinalIgnoreCase)
                        || uri.AbsolutePath.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase);
            if (!isPdf) return BadRequest($"Le document récupéré n'est pas un PDF. Content-Type: {mediaType}");

            await using var upstream = await resp.Content.ReadAsStreamAsync(HttpContext.RequestAborted);
            var ms = new MemoryStream();
            await upstream.CopyToAsync(ms, HttpContext.RequestAborted);
            ms.Position = 0;

            Response.Headers["Cache-Control"] = "public, max-age=3600";
            Response.Headers["Content-Disposition"] = "inline; filename=\"recette.pdf\"";

            // ✅ Range activé → scroll multi-pages iOS/iframe/pdf.js OK
            return new FileStreamResult(ms, "application/pdf") { EnableRangeProcessing = true };
        }
        catch (TaskCanceledException)
        {
            return StatusCode(504, "Timeout lors de la récupération du PDF.");
        }
        catch (Exception ex)
        {
            return StatusCode(500, $"Erreur ProxyPdf: {ex.Message}");
        }
    }

    // ------------------------
    // Ingrédients (agrégation)
    // ------------------------
    private sealed record QtyParse(double? Value, string Unit);

    private static QtyParse ParseQuantity(string? q)
    {
        if (string.IsNullOrWhiteSpace(q)) return new QtyParse(null, "");
        var cleaned = q.Trim().Replace('\u00A0', ' ').Replace(',', '.');
        var m = Regex.Match(cleaned, @"^\s*([0-9]+(?:\.[0-9]+)?)(?:\s+([A-Za-zéèêàùûïîç%()\/\.-]+))?\s*$");
        if (!m.Success) return new QtyParse(null, "");
        var val = double.TryParse(m.Groups[1].Value, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var v)
            ? v : (double?)null;
        var unit = m.Groups[2].Success ? m.Groups[2].Value.Trim().ToLowerInvariant() : "";
        if (Regex.IsMatch(unit, @"pi[eè]ce", RegexOptions.IgnoreCase)) unit = "pièce(s)";
        return new QtyParse(val, unit);
    }

    private static List<IngredientItemDto> AggregateIngredients(IEnumerable<IngredientItemDto> items, HashSet<string> owned)
    {
        var map = new Dictionary<string, (double? sum, string unit, IngredientItemDto item)>(StringComparer.Ordinal);

        foreach (var it in items)
        {
            var name = (it.Name ?? "").Trim();
            if (string.IsNullOrEmpty(name)) continue;

            var kName = name.ToLowerInvariant();
            var (val, unit) = ParseQuantity(it.Quantity);
            var unitKey = (unit ?? "").ToLowerInvariant();

            if (val is null)
            {
                var k = $"{kName}||{it.Quantity}";
                if (!map.ContainsKey(k))
                    map[k] = (null, unitKey, new IngredientItemDto { Name = name, Quantity = it.Quantity, Image = it.Image, Owned = owned.Contains(kName) });
                continue;
            }

            var key = $"{kName}##{unitKey}";
            if (!map.TryGetValue(key, out var acc))
            {
                map[key] = (val, unitKey, new IngredientItemDto { Name = name, Quantity = it.Quantity, Image = it.Image, Owned = owned.Contains(kName) });
            }
            else
            {
                var newSum = (acc.sum ?? 0) + val.Value;
                var keep = acc.item;
                if (string.IsNullOrWhiteSpace(keep.Image) && !string.IsNullOrWhiteSpace(it.Image))
                    keep.Image = it.Image;
                map[key] = (newSum, unitKey, keep);
            }
        }

        var outList = new List<IngredientItemDto>();
        foreach (var kv in map)
        {
            var (sum, unit, item) = kv.Value;
            if (sum is not null)
            {
                var qty = ((sum % 1.0) == 0.0)
                    ? sum.Value.ToString("0", System.Globalization.CultureInfo.InvariantCulture)
                    : sum.Value.ToString("0.##", System.Globalization.CultureInfo.InvariantCulture);
                item.Quantity = string.IsNullOrWhiteSpace(unit) ? qty : $"{qty} {unit}";
            }
            outList.Add(item);
        }
        return outList;
    }

    [HttpGet("AggregatedIngredients")]
    public IActionResult AggregatedIngredients()
    {
        var userId = HttpContext.Session.GetInt32("UserId");
        if (userId == null) return Unauthorized("Utilisateur non connecté.");

        var ownedNames = _context.Ingredients
            .Select(i => i.NameOwnedIngredients)
            .ToList()
            .Select(s => (s ?? "").Trim().ToLowerInvariant())
            .ToHashSet();

        var perRecipe = _context.SavingRecettes
            .GroupBy(s => s.IdSavingRecette)
            .Select(g => new { RecetteId = g.Key, Portions = g.Count() })
            .ToList();

        if (perRecipe.Count == 0) return Ok(new List<IngredientItemDto>());

        var recettes = _context.Recettes
            .Where(r => perRecipe.Select(x => x.RecetteId).Contains(r.Id))
            .Select(r => new { r.Id, r.Ingredients })
            .ToList();

        // ✅ on ne borne plus à 4 pour le calcul; si >4, on prend la table "4" et on scale
        var portionsById = perRecipe.ToDictionary(x => x.RecetteId, x => Math.Max(1, x.Portions));

        var rawItems = new List<IngredientItemDto>();
        foreach (var r in recettes)
        {
            if (!portionsById.TryGetValue(r.Id, out var portions)) portions = 1;

            int basePortions = Math.Min(4, Math.Max(1, portions));
            double scale = portions <= 4 ? 1.0 : (double)portions / 4.0;

            try
            {
                using var doc = JsonDocument.Parse(r.Ingredients ?? "{}");
                if (!doc.RootElement.TryGetProperty(basePortions.ToString(), out var arr) || arr.ValueKind != JsonValueKind.Array)
                    continue;

                foreach (var el in arr.EnumerateArray())
                {
                    var name = el.TryGetProperty("Name", out var n) ? n.GetString() ?? "" : "";
                    var qty = el.TryGetProperty("Quantity", out var q) ? q.GetString() : null;
                    var img = el.TryGetProperty("Image", out var im) ? im.GetString() : null;
                    if (string.IsNullOrWhiteSpace(name)) continue;

                    // ✅ scale si >4 et quantité numérique
                    if (scale != 1.0 && !string.IsNullOrWhiteSpace(qty))
                    {
                        var parsed = ParseQuantity(qty);
                        if (parsed.Value is double v)
                        {
                            var scaled = v * scale;
                            var qtyStr = ((scaled % 1.0) == 0.0)
                                ? scaled.ToString("0", System.Globalization.CultureInfo.InvariantCulture)
                                : scaled.ToString("0.##", System.Globalization.CultureInfo.InvariantCulture);
                            qty = string.IsNullOrWhiteSpace(parsed.Unit) ? qtyStr : $"{qtyStr} {parsed.Unit}";
                        }
                    }

                    rawItems.Add(new IngredientItemDto
                    {
                        Name = name.Trim(),
                        Quantity = qty,
                        Image = img,
                        Owned = ownedNames.Contains(name.Trim().ToLowerInvariant())
                    });
                }
            }
            catch { /* ignore */ }
        }

        var aggregated = AggregateIngredients(rawItems, ownedNames);

        var sorted = aggregated
            .OrderBy(a => a.Owned ? 1 : 0)
            .ThenBy(a => a.Name, StringComparer.Create(new System.Globalization.CultureInfo("fr-FR"), ignoreCase: true))
            .ToList();

        return Ok(sorted);
    }

    // ------------- IA SmartSearch : passe par /api/ai/select -------------
    public sealed class AiRequestDto { public string Query { get; set; } = ""; }

    public sealed class SmartSearchResponse
    {
        public List<IngredientItemDto> Items { get; set; } = new();
        public SmartSearchDebug? Debug { get; set; }
    }
    public sealed class SmartSearchDebug
    {
        public object? Sent { get; set; }
        public string? Llm { get; set; }
    }

    [HttpPost("SmartSearch")]
    public async Task<IActionResult> SmartSearch(
       [FromBody] AiRequestDto body,
       [FromServices] IHttpClientFactory httpClientFactory,
       [FromQuery] bool debug = false)
    {
        var userId = HttpContext.Session.GetInt32("UserId");
        if (userId == null) return Unauthorized("Utilisateur non connecté.");

        var ownedNames = _context.Ingredients.Select(i => i.NameOwnedIngredients)
            .ToList()
            .Select(s => (s ?? "").Trim().ToLowerInvariant())
            .ToHashSet();

        var perRecipe = _context.SavingRecettes
            .Where(s => s.UserId == userId)
            .GroupBy(s => s.IdSavingRecette)
            .Select(g => new { RecetteId = g.Key, Portions = g.Count() })
            .ToList();

        if (perRecipe.Count == 0)
            return Ok(new { items = Array.Empty<IngredientItemDto>(), debug = (object?)null });

        var recettes = _context.Recettes
            .Where(r => perRecipe.Select(x => x.RecetteId).Contains(r.Id))
            .Select(r => new { r.Id, r.Ingredients })
            .ToList();

        var portionsById = perRecipe.ToDictionary(x => x.RecetteId, x => Math.Max(1, x.Portions));

        var rawItems = new List<IngredientItemDto>();
        foreach (var r in recettes)
        {
            if (!portionsById.TryGetValue(r.Id, out var portions)) portions = 1;

            int basePortions = Math.Min(4, Math.Max(1, portions));
            double scale = portions <= 4 ? 1.0 : (double)portions / 4.0;

            try
            {
                using var doc = JsonDocument.Parse(r.Ingredients ?? "{}");
                if (!doc.RootElement.TryGetProperty(basePortions.ToString(), out var arr) || arr.ValueKind != JsonValueKind.Array)
                    continue;

                foreach (var el in arr.EnumerateArray())
                {
                    var name = el.TryGetProperty("Name", out var n) ? n.GetString() ?? "" : "";
                    var qty = el.TryGetProperty("Quantity", out var q) ? q.GetString() : null;
                    var img = el.TryGetProperty("Image", out var im) ? im.GetString() : null;
                    if (string.IsNullOrWhiteSpace(name)) continue;

                    if (scale != 1.0 && !string.IsNullOrWhiteSpace(qty))
                    {
                        var parsed = ParseQuantity(qty);
                        if (parsed.Value is double v)
                        {
                            var scaled = v * scale;
                            var qtyStr = ((scaled % 1.0) == 0.0)
                                ? scaled.ToString("0", System.Globalization.CultureInfo.InvariantCulture)
                                : scaled.ToString("0.##", System.Globalization.CultureInfo.InvariantCulture);
                            qty = string.IsNullOrWhiteSpace(parsed.Unit) ? qtyStr : $"{qtyStr} {parsed.Unit}";
                        }
                    }

                    rawItems.Add(new IngredientItemDto
                    {
                        Name = name.Trim(),
                        Quantity = qty,
                        Image = img,
                        Owned = ownedNames.Contains(name.Trim().ToLowerInvariant())
                    });
                }
            }
            catch { /* ignore per-recette */ }
        }

        var aggregated = AggregateIngredients(rawItems, ownedNames);
        var allowedNames = aggregated.Select(a => a.Name).Distinct().ToList();

        var client = httpClientFactory.CreateClient();
        client.Timeout = TimeSpan.FromSeconds(60);

        var baseUrl = $"{Request.Scheme}://{Request.Host}";
        var reqBody = new
        {
            query = body?.Query ?? "",
            allowed = allowedNames,
            debug = debug
        };

        var req = new HttpRequestMessage(HttpMethod.Post, $"{baseUrl}/api/ai/select");
        req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        req.Content = new StringContent(JsonSerializer.Serialize(reqBody), Encoding.UTF8, "application/json");

        try
        {
            using var resp = await client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, HttpContext?.RequestAborted ?? default);
            var raw = await resp.Content.ReadAsStringAsync();
            if (!resp.IsSuccessStatusCode)
            {
                var nq = (body?.Query ?? "").Trim().ToLowerInvariant();
                var fallback = string.IsNullOrEmpty(nq)
                    ? aggregated
                    : aggregated.Where(a => (a.Name ?? "").ToLowerInvariant().Contains(nq)).ToList();

                return Ok(new
                {
                    items = fallback,
                    debug = debug ? new { selectorError = new { status = (int)resp.StatusCode, raw, url = $"{baseUrl}/api/ai/select" } } : null
                });
            }

            var parsed = JsonDocument.Parse(raw).RootElement;
            var names = parsed.TryGetProperty("names", out var nEl) && nEl.ValueKind == JsonValueKind.Array
                ? nEl.EnumerateArray().Select(x => x.GetString() ?? "").Where(s => !string.IsNullOrWhiteSpace(s)).ToList()
                : new List<string>();

            var order = names.Select((n, i) => new { n = n.Trim().ToLowerInvariant(), i })
                             .ToDictionary(x => x.n, x => x.i);
            var selected = aggregated
                .Where(a => order.ContainsKey((a.Name ?? "").Trim().ToLowerInvariant()))
                .OrderBy(a => order[(a.Name ?? "").Trim().ToLowerInvariant()])
                .ToList();

            if (selected.Count == 0)
            {
                var nq = (body?.Query ?? "").Trim().ToLowerInvariant();
                selected = string.IsNullOrEmpty(nq)
                    ? aggregated
                    : aggregated.Where(a => (a.Name ?? "").ToLowerInvariant().Contains(nq)).ToList();
            }

            return Ok(new
            {
                items = selected,
                debug = debug ? JsonSerializer.Deserialize<object>(raw) : null
            });
        }
        catch (TaskCanceledException)
        {
            return StatusCode(504, new { error = "Timeout IA selector" });
        }
        catch (Exception ex)
        {
            return StatusCode(500, new { error = ex.Message });
        }
    }

    [HttpGet("GetRecipesDetails")]
    public IActionResult GetRecipesDetails([FromQuery] string ids)
    {
        if (string.IsNullOrWhiteSpace(ids)) return BadRequest("ids manquant");
        var idList = ids.Split(',').Select(s => s.Trim()).Where(s => s.Length > 0).ToList();

        var recipes = _context.Recettes
            .Where(r => idList.Contains(r.Id))
            .Select(r => new
            {
                id = r.Id,
                name = r.Name,
                image = r.Image,
                difficulte = r.Difficulte,
                ingredientsJson = r.Ingredients,
                pdf = r.Pdf
            })
            .ToList();

        return Ok(recipes);
    }

    [HttpPost("ToggleOwnedIngredient")]
    public IActionResult ToggleOwnedIngredient([FromBody] string ingredientName)
    {
        try
        {
            var existing = _context.Ingredients
                .FirstOrDefault(i => i.NameOwnedIngredients == ingredientName);

            if (existing != null)
            {
                _context.Ingredients.Remove(existing);
                _context.SaveChanges();
                return Ok(new { status = "removed" });
            }
            else
            {
                _context.Ingredients.Add(new Ingredient { NameOwnedIngredients = ingredientName });
                _context.SaveChanges();
                return Ok(new { status = "added" });
            }
        }
        catch (Exception ex)
        {
            return StatusCode(500, new { message = "Erreur serveur", error = ex.Message });
        }
    }

    [HttpGet("GetOwnedIngredients")]
    public IActionResult GetOwnedIngredients()
    {
        try
        {
            var names = _context.Ingredients
                .Select(i => i.NameOwnedIngredients)
                .Distinct()
                .ToList();

            return Ok(names);
        }
        catch (Exception ex)
        {
            return StatusCode(500, new { message = "Erreur serveur", error = ex.Message });
        }
    }

    [HttpGet("GetAllRecipes")]
    public IActionResult GetAllRecipes()
    {
        var counts = _context.SavingRecettes
            .AsNoTracking()
            .GroupBy(s => new { s.IdSavingRecette, s.UserId })
            .Select(g => new
            {
                RecetteId = g.Key.IdSavingRecette,
                UserId = g.Key.UserId,
                Portions = g.Count()
            })
            .ToList();

        var recettes = _context.Recettes
            .AsNoTracking()
            .Select(r => new
            {
                r.Id,
                r.Name,
                r.Image,
                r.TempsDePreparation,
                r.Tags,
                IngredientsToHad = r.IngredientsToHad
            })
            .ToList();

        var neededUserIds = counts.Select(x => x.UserId).Distinct().ToList();

        var users = _layout.Users
            .AsNoTracking()
            .Where(u => neededUserIds.Contains(u.Id))
            .Select(u => new { u.Id, u.Username })
            .ToList();

        var baseRows =
            from c in counts
            join r in recettes on c.RecetteId equals r.Id
            join u in users on c.UserId equals u.Id into gj
            from u in gj.DefaultIfEmpty()
            select new
            {
                RecetteId = r.Id,
                r.Name,
                r.Image,
                r.TempsDePreparation,
                c.Portions,
                r.Tags,
                r.IngredientsToHad,
                User = u?.Username
            };

        var result = baseRows
            .GroupBy(x => x.RecetteId)
            .Select(g => new
            {
                id = g.Key,
                name = g.First().Name,
                image = g.First().Image,
                tempsDePreparation = g.First().TempsDePreparation,
                portions = g.Sum(x => x.Portions),
                tags = g.First().Tags,
                Users = g.Where(x => x.User != null)
                         .Select(x => x.User)
                         .Distinct()
                         .ToList(),
                ingredientsToHad = g.First().IngredientsToHad
            })
            .ToList();

        return Ok(result);
    }

    [HttpGet("/HelloFresh/GetAllRecipesWithUsers")]
    public IActionResult GetAllRecipesWithUsers()
    {
        var data = _context.SavingRecettes
            .GroupBy(s => new { s.IdSavingRecette, s.UserId })
            .Select(g => new
            {
                RecetteId = g.Key.IdSavingRecette,
                UserId = g.Key.UserId,
                Portions = g.Count()
            })
            .Join(_context.Recettes,
                  g => g.RecetteId, r => r.Id,
                  (g, r) => new
                  {
                      id = r.Id,
                      name = r.Name,
                      image = r.Image,
                      tempsDePreparation = r.TempsDePreparation,
                      tags = r.Tags,
                      userId = g.UserId,
                      portions = g.Portions
                  })
            .ToList();
        return Ok(data);
    }

    [HttpGet("GetHistory")]
    public IActionResult GetHistory([FromQuery] bool isUserImportant = true, [FromQuery] string? search = null)
    {
        try
        {
            var sessionUserId = HttpContext.Session.GetInt32("UserId");
            if (isUserImportant && sessionUserId == null)
                return Unauthorized("Utilisateur non connecté.");

            var histQ = _context.HistoriqueRecettes.AsNoTracking();
            if (isUserImportant) histQ = histQ.Where(h => h.UserId == sessionUserId);

            var hist = histQ
                .Select(h => new { h.DateHistorique, h.RecetteId, h.UserId })
                .ToList();

            string K(string? s) => (s ?? "").Trim().ToLowerInvariant();
            var recDict = _context.Recettes.AsNoTracking()
                .Select(r => new { r.Id, r.Name, r.Image, r.TempsDePreparation })
                .ToList()
                .GroupBy(r => K(r.Id))
                .ToDictionary(g => g.Key, g => g.First());

            var joined = hist.Select(h =>
            {
                recDict.TryGetValue(K(h.RecetteId), out var r);
                return new
                {
                    Date = h.DateHistorique,
                    RecetteId = h.RecetteId,
                    Name = r != null ? r.Name : null,
                    Image = r != null ? r.Image : null,
                    TempsDePreparation = r != null ? r.TempsDePreparation : (int?)null,
                    UserId = h.UserId
                };
            }).ToList();

            if (!string.IsNullOrWhiteSpace(search))
            {
                var s = search.Trim().ToLowerInvariant();
                joined = joined.Where(x => (x.Name ?? "").ToLowerInvariant().Contains(s)).ToList();
            }

            var userNames = new Dictionary<int, string>();
            if (!isUserImportant)
            {
                var ids = joined.Select(x => x.UserId).Distinct().ToList();
                userNames = _layout.Users.AsNoTracking()
                    .Where(u => ids.Contains(u.Id))
                    .Select(u => new { u.Id, u.Username })
                    .ToList()
                    .GroupBy(x => x.Id)
                    .ToDictionary(g => g.Key, g => g.First().Username ?? $"User#{g.Key}");
            }

            var rows = joined
                .GroupBy(x => new { x.Date, x.RecetteId })
                .Select(g => new
                {
                    g.Key.Date,
                    id = g.Key.RecetteId,
                    name = g.Select(x => x.Name).FirstOrDefault(n => !string.IsNullOrWhiteSpace(n)) ?? $"Recette {g.Key.RecetteId}",
                    image = g.Select(x => x.Image).FirstOrDefault(i => !string.IsNullOrWhiteSpace(i)),
                    tempsDePreparation = g.Select(x => x.TempsDePreparation).FirstOrDefault() ?? 0,
                    portions = g.Count(),
                    Users = !isUserImportant
                        ? g.Select(x => userNames.TryGetValue(x.UserId, out var u) ? u : $"User#{x.UserId}")
                           .Distinct()
                           .ToList()
                        : new List<string>()
                })
                .ToList();

            var culture = new System.Globalization.CultureInfo("fr-FR");
            var result = rows
                .GroupBy(x => x.Date)
                .OrderByDescending(g => g.Key)
                .Select(g => new
                {
                    date = g.Key.ToString("dd MMMM yyyy", culture),
                    rawDate = g.Key,
                    items = g.OrderBy(x => x.name, StringComparer.Create(culture, true)).ToList()
                })
                .ToList();

            return Ok(result);
        }
        catch (Exception ex)
        {
            return StatusCode(500, new { message = "history_failed", error = ex.Message });
        }
    }

    [HttpPost("ArchiveAll")]
    public IActionResult ArchiveAll()
    {
        var userId = HttpContext.Session.GetInt32("UserId");
        if (userId == null) return Unauthorized("Utilisateur non connecté.");

        try
        {
            var selections = _context.SavingRecettes
                .Where(s => s.UserId == userId)
                .AsNoTracking()
                .ToList();

            if (selections.Count == 0)
                return Ok(new { message = "empty", archivedPortions = 0, archivedRecipes = 0, skippedIds = Array.Empty<string>() });

            var groups = selections
                .GroupBy(s => s.IdSavingRecette)
                .Select(g => new { RecetteId = g.Key, Portions = g.Count() })
                .ToList();

            var validIds = _context.Recettes
                .AsNoTracking()
                .Select(r => r.Id)
                .ToHashSet(StringComparer.Ordinal);

            var validGroups = groups.Where(g => validIds.Contains(g.RecetteId)).ToList();
            var skippedIds = groups.Where(g => !validIds.Contains(g.RecetteId)).Select(g => g.RecetteId).Distinct().ToList();

            if (validGroups.Count == 0)
                return Ok(new { message = "nothing_valid", archivedPortions = 0, archivedRecipes = 0, skippedIds });

            DateOnly todayParis;
            try
            {
                var tz = TimeZoneInfo.FindSystemTimeZoneById("Europe/Paris");
                todayParis = DateOnly.FromDateTime(TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, tz));
            }
            catch { todayParis = DateOnly.FromDateTime(DateTime.Now); }

            using var tx = _context.Database.BeginTransaction();

            var toInsert = new List<HistoriqueRecette>(capacity: validGroups.Sum(x => x.Portions));
            foreach (var g in validGroups)
            {
                for (int i = 0; i < g.Portions; i++)
                {
                    toInsert.Add(new HistoriqueRecette
                    {
                        RecetteId = g.RecetteId,
                        UserId = userId.Value,
                        DateHistorique = todayParis
                    });
                }
            }

            if (toInsert.Count > 0)
                _context.HistoriqueRecettes.AddRange(toInsert);

            var rowsToDelete = _context.SavingRecettes
                .Where(s => s.UserId == userId && validIds.Contains(s.IdSavingRecette))
                .ToList();

            _context.SavingRecettes.RemoveRange(rowsToDelete);

            _context.SaveChanges();
            tx.Commit();

            var archivedRecipes = validGroups.Count;
            var archivedPieces = validGroups.Sum(g => g.Portions);

            return Ok(new
            {
                message = "archived_all",
                archivedPortions = archivedPieces,
                archivedRecipes,
                skippedIds
            });
        }
        catch (DbUpdateException dbex)
        {
            return StatusCode(500, new
            {
                message = "archive_failed",
                reason = "db_update_exception",
                error = dbex.GetBaseException().Message
            });
        }
        catch (Exception ex)
        {
            return StatusCode(500, new
            {
                message = "archive_failed",
                reason = "exception",
                error = ex.Message
            });
        }
    }

    // POST /HelloFresh/HasHistory
    [HttpPost("HasHistory")]
    [IgnoreAntiforgeryToken]
    public async Task<IActionResult> HasHistory([FromBody] string recipeId)
    {
        var sessionUserId = HttpContext.Session.GetInt32("UserId");
        if (sessionUserId == null) return Unauthorized("Utilisateur non connecté.");

        if (string.IsNullOrWhiteSpace(recipeId))
            return Ok(new { exists = false });

        bool exists = await _context.HistoriqueRecettes
            .AsNoTracking()
            .AnyAsync(h => h.RecetteId == recipeId && h.UserId == sessionUserId.Value);

        return Ok(new { exists });
    }

    [HttpGet("GetAllIngredients")]
    public IActionResult GetAllIngredients()
    {
        var names = new HashSet<string>(StringComparer.Create(new System.Globalization.CultureInfo("fr-FR"), true));
        foreach (var json in _context.Recettes.AsNoTracking().Select(r => r.Ingredients ?? "{}"))
        {
            try
            {
                using var doc = JsonDocument.Parse(json);
                if (doc.RootElement.ValueKind != JsonValueKind.Object) continue;

                foreach (var prop in doc.RootElement.EnumerateObject())
                {
                    if (prop.Value.ValueKind != JsonValueKind.Array) continue;
                    foreach (var el in prop.Value.EnumerateArray())
                    {
                        var n = el.TryGetProperty("Name", out var nEl) ? (nEl.GetString() ?? "").Trim() : "";
                        if (!string.IsNullOrWhiteSpace(n)) names.Add(n);
                    }
                }
            }
            catch { /* ignore */ }
        }

        var list = names.OrderBy(s => s, StringComparer.Create(new System.Globalization.CultureInfo("fr-FR"), true)).ToList();
        return Ok(list);
    }

    [HttpGet("GetAllTags")]
    public IActionResult GetAllTags()
    {
        var tags = new HashSet<string>(StringComparer.Create(new System.Globalization.CultureInfo("fr-FR"), true));
        foreach (var row in _context.Recettes.AsNoTracking().Select(r => r.Tags))
        {
            if (string.IsNullOrWhiteSpace(row)) continue;
            foreach (var t in Regex.Split(row, @"[,•]"))
            {
                var v = (t ?? "").Trim();
                if (string.IsNullOrEmpty(v) || v == "•") continue;
                tags.Add(v);
            }
        }
        var list = tags.OrderBy(s => s, StringComparer.Create(new System.Globalization.CultureInfo("fr-FR"), true)).ToList();
        return Ok(list);
    }

    // API: recettes choisies distinctes par nom, avec PDF
    [HttpGet("GetOwnedForReader")]
    public IActionResult GetOwnedForReader()
    {
        var userId = HttpContext.Session.GetInt32("UserId");
        if (userId == null) return Unauthorized("Utilisateur non connecté.");

        var owned = _context.SavingRecettes
            .Where(s => s.UserId == userId)
            .GroupBy(s => s.IdSavingRecette)
            .Select(g => new { RecetteId = g.Key, Portions = g.Count() })
            .ToList();

        if (owned.Count == 0) return Ok(Array.Empty<object>());

        var joined = owned
            .Join(_context.Recettes,
                  g => g.RecetteId,
                  r => r.Id,
                  (g, r) => new
                  {
                      id = r.Id,
                      name = r.Name,
                      image = r.Image,
                      pdf = r.Pdf,
                      portions = g.Portions,
                      prep = r.TempsDePreparation
                  })
            .ToList();

        string Key(string s) => (s ?? "").Trim().ToLowerInvariant();
        var distinct = joined
            .GroupBy(x => Key(x.name))
            .Select(g => g.OrderByDescending(x => x.portions).ThenBy(x => x.name).First())
            .OrderBy(x => x.name)
            .ToList();

        return Ok(distinct);
    }
}
