//// Controllers/SearchController.cs
//using Microsoft.AspNetCore.Mvc;
//using Microsoft.Extensions.Logging;
//using System.Net.Http.Headers;
//using System.Text;
//using System.Text.Json;
//using System.Text.Json.Serialization;
//using System.Text.RegularExpressions;

//namespace Administration.Controllers
//{
//    [ApiController]
//    [Route("api/ai")]
//    public class SearchController : ControllerBase
//    {
//        private readonly IHttpClientFactory _httpFactory;
//        private readonly IConfiguration _cfg;
//        private readonly ILogger<SearchController> _log;

//        public SearchController(IHttpClientFactory httpFactory, IConfiguration cfg, ILogger<SearchController> log)
//        {
//            _httpFactory = httpFactory;
//            _cfg = cfg;
//            _log = log;
//        }

//        // =========================
//        // DTOs
//        // =========================
//        public sealed class AiSelectRequest
//        {
//            public string Query { get; set; } = "";
//            public List<string> Allowed { get; set; } = new();
//            public bool Debug { get; set; } = false;
//        }

//        public sealed class AiSelectResponse
//        {
//            [JsonPropertyName("names")]
//            public List<string> Names { get; set; } = new();

//            [JsonPropertyName("debug")]
//            public object? Debug { get; set; }
//        }

//        // =========================
//        // POST /api/ai/select
//        // =========================
//        [HttpPost("select")]
//        public async Task<IActionResult> Select([FromBody] AiSelectRequest body)
//        {
//            if (body is null) return BadRequest(new { error = "Body requis." });
//            if (string.IsNullOrWhiteSpace(body.Query)) return BadRequest(new { error = "Query requis." });
//            if (body.Allowed is null || body.Allowed.Count == 0)
//            {
//                _log.LogWarning("AI Select: Allowed vide pour query '{Query}'", body.Query);
//                return Ok(new AiSelectResponse { Names = new() });
//            }

//            // Normalisation unique
//            var allowed = body.Allowed
//                .Where(a => !string.IsNullOrWhiteSpace(a))
//                .Select(a => a.Trim())
//                .Distinct(StringComparer.OrdinalIgnoreCase)
//                .ToList();

//            // --- 1) Raccourcis instantanés : catégories & synonymes riches
//            var catHits = TryCategoryExpand(body.Query, allowed);
//            if (catHits.Count > 0)
//                return Ok(new AiSelectResponse { Names = SortByAllowedOrder(catHits, allowed) });

//            // --- 2) Match "classique" tolérant (FR/EN, accents, pluriels)
//            var classicHits = ClassicMatch(body.Query, allowed);
//            if (classicHits.Count > 0)
//                return Ok(new AiSelectResponse { Names = SortByAllowedOrder(classicHits, allowed) });

//            // --- 3) Heuristique "prompt" -> on tente l'IA (court)
//            var ollamaUrl = _cfg["Ollama:Url"] ?? "http://ollama:11434";
//            var model = _cfg["Ollama:Model"] ?? "qwen2.5:1.5b-instruct";

//            // prompt ultra directif pour forcer un tableau JSON plat
//            var prompt =
//               $"Liste autorisée: [{string.Join(",", allowed.Select(a => $"\"{a}\""))}].\n" +
//               $"Requête: \"{body.Query}\".\n" +
//               "Réponds UNIQUEMENT par un tableau JSON plat des éléments EXACTEMENT issus de la liste autorisée. " +
//               "Exemple: [\"Tomate\",\"Oignon\"]. Interdit: objets {name:...}, clés ingredients:, texte hors JSON.";

//            var client = _httpFactory.CreateClient();
//            client.Timeout = Timeout.InfiniteTimeSpan; // on contrôle avec CTS

//            var payload = new
//            {
//                model,
//                prompt,
//                stream = false,
//                format = "json",
//                keep_alive = "10m",
//                options = new
//                {
//                    temperature = 0.0,
//                    num_ctx = 64,
//                    num_predict = 64
//                }
//            };

//            var req = new HttpRequestMessage(HttpMethod.Post, $"{ollamaUrl.TrimEnd('/')}/api/generate");
//            req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
//            req.Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");

//            _log.LogInformation("AI Select ▶ Query='{Query}', AllowedCount={AllowedCount}, Model={Model}, Url={Url}",
//                body.Query, allowed.Count, model, ollamaUrl);
//            if (body.Debug)
//            {
//                _log.LogDebug("AI Select ▶ Prompt envoyé:\n{Prompt}", payload.prompt);
//            }

//            // Si la requête ressemble à une phrase libre -> 4s, sinon 2s
//            bool prompty = body.Query.Length >= 18
//                || Regex.IsMatch(body.Query, @"\b(avec|sans|pour|idée|idées|recette|faire|préparer|quoi|envie)\b", RegexOptions.IgnoreCase);
//            using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(prompty ? 4 : 2));

//            try
//            {
//                using var resp = await client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, cts.Token);
//                var raw = await resp.Content.ReadAsStringAsync();

//                _log.LogInformation("AI Select ◀ HTTP {Status} ({Reason})", (int)resp.StatusCode, resp.ReasonPhrase);
//                _log.LogDebug("AI Select ◀ RAW:\n{Raw}", raw);

//                if (!resp.IsSuccessStatusCode)
//                {
//                    _log.LogWarning("AI Select: statut non OK, on renvoie l’erreur au client.");
//                    return StatusCode((int)resp.StatusCode, new
//                    {
//                        error = "IA error",
//                        raw,
//                        url = $"{ollamaUrl.TrimEnd('/')}/api/generate"
//                    });
//                }

//                // Extraire "response" puis convertir en liste de chaînes
//                using var doc = JsonDocument.Parse(raw);
//                var responseText = doc.RootElement.TryGetProperty("response", out var rEl)
//                    ? (rEl.GetString() ?? "")
//                    : "";

//                var iaNames = ExtractNames(responseText);
//                var mapped = MapToAllowed(iaNames, allowed);

//                return Ok(new AiSelectResponse
//                {
//                    Names = SortByAllowedOrder(mapped, allowed),
//                    Debug = body.Debug ? new { sent = payload, url = $"{ollamaUrl.TrimEnd('/')}/api/generate", raw } : null
//                });
//            }
//            catch (TaskCanceledException)
//            {
//                _log.LogWarning("AI Select: timeout côté Ollama ({Url})", ollamaUrl);
//                return StatusCode(504, new { error = "Timeout IA" });
//            }
//            catch (Exception ex)
//            {
//                _log.LogError(ex, "AI Select: exception pendant l’appel Ollama.");
//                return StatusCode(500, new { error = ex.Message });
//            }
//        }

//        // =========================
//        // Helpers — Normalisation
//        // =========================
//        static string RemoveDiacritics(string s)
//        {
//            if (string.IsNullOrEmpty(s)) return "";
//            var formD = s.Normalize(NormalizationForm.FormD);
//            var sb = new StringBuilder(formD.Length);
//            foreach (var ch in formD)
//            {
//                var uc = System.Globalization.CharUnicodeInfo.GetUnicodeCategory(ch);
//                if (uc != System.Globalization.UnicodeCategory.NonSpacingMark)
//                    sb.Append(ch);
//            }
//            return sb.ToString().Normalize(NormalizationForm.FormC);
//        }

//        static string Key(string? s)
//        {
//            s ??= "";
//            var noAcc = RemoveDiacritics(s).ToLowerInvariant();
//            // retire ponctuation + normalise espaces
//            noAcc = Regex.Replace(noAcc, @"[^\p{L}\p{Nd}\s]", " ");
//            noAcc = Regex.Replace(noAcc, @"\s+", " ").Trim();
//            // singulier approximatif (enlève 's' final si pertinent)
//            if (noAcc.EndsWith("s") && noAcc.Length > 3) noAcc = noAcc[..^1];
//            return noAcc;
//        }

//        static List<string> SortByAllowedOrder(IEnumerable<string> items, List<string> allowed)
//        {
//            var set = new HashSet<string>(items, StringComparer.OrdinalIgnoreCase);
//            var ordered = new List<string>();
//            foreach (var a in allowed)
//                if (set.Contains(a)) ordered.Add(a);
//            return ordered;
//        }

//        // =========================
//        // Helpers — Matching local
//        // =========================

//        // Synonymes simples EN -> FR (complète librement)
//        static readonly Dictionary<string, string[]> EN_FR_SYNONYMS = new(StringComparer.OrdinalIgnoreCase)
//        {
//            ["meat"] = new[] { "viande", "viandes", "boeuf", "bœuf", "porc", "poulet", "agneau", "veau", "saucisse", "jambon" },
//            ["fish"] = new[] { "poisson", "saumon", "thon", "cabillaud", "lieu", "filet de lieu", "colin", "merlu" },
//            ["spicy"] = new[] { "piquant", "épicé", "piment", "harissa", "sriracha", "curry", "paprika", "poivre", "gingembre" },
//            ["fresh"] = new[] { "frais", "fraîche", "fraîches", "réfrigéré", "perissable", "périssable" },
//            ["dairy"] = new[] { "laitier", "laitiers", "lait", "beurre", "fromage", "crème", "yaourt" },
//            ["herbs"] = new[] { "herbes", "persil", "ciboulette", "basilic", "menthe", "aneth", "romarin", "thym" },
//            ["sauce"] = new[] { "sauce", "sauces", "condiment", "condiments", "mayonnaise", "ketchup", "moutarde", "soja", "sauce soja", "BBQ" },
//            ["veggie"] = new[] { "légume", "légumes", "tomate", "oignon", "carotte", "brocoli", "courgette", "poivron", "ail", "salade" },
//            ["protein"] = new[] { "protéine", "protéines", "oeuf", "œuf", "tofu", "tempeh", "pois chiches", "lentilles", "haricots", "poulet", "fromage" },
//        };

//        static List<string> ClassicMatch(string query, List<string> allowed)
//        {
//            var nq = Key(query);
//            var tokens = nq.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);

//            // EN->FR élargit
//            var expanded = new HashSet<string>(tokens);
//            foreach (var t in tokens)
//                if (EN_FR_SYNONYMS.TryGetValue(t, out var frs))
//                    foreach (var fr in frs) expanded.Add(Key(fr));

//            var hits = new List<string>();
//            foreach (var a in allowed)
//            {
//                if (AnyTokenMatches(a, expanded))
//                    hits.Add(a);
//            }

//            return hits.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
//        }


//        // =========================
//        // Catégories & règles
//        // =========================
//        sealed class CategoryRule
//        {
//            public string Name { get; init; } = "";
//            public string[] Triggers { get; init; } = Array.Empty<string>();
//            public string[] Must { get; init; } = Array.Empty<string>();
//            public string[] Any { get; init; } = Array.Empty<string>();
//            public string[] Not { get; init; } = Array.Empty<string>();
//        }

//        static readonly List<CategoryRule> CATEGORY_RULES = new()
//        {
//           new CategoryRule {
//                Name = "legumes",
//                Triggers = new[]{"legume","légume","légumes","vegetable","veggie","crudité"},
//                Any = new[]{
//                    "oignon","carotte","pomme de terre","brocoli","courgette",
//                    "poivron","tomate","ail","salade","champignon","haricot vert","épinard"
//                },
//                Not = new[]{
//                    // viande/charcuterie
//                    "saucisse","porc","poulet","boeuf","bœuf","agneau","veau","jambon","lardon","charcuterie","filet",
//                    // poisson/mer
//                    "poisson","saumon","thon","lieu","cabillaud","merlu","colin","crevette","moule",
//                    // laitier/fromage/œuf
//                    "fromage","crème","lait","yaourt","œuf","oeuf","beurre"
//                }
//            },
//            new CategoryRule {
//                Name = "fruits",
//                Triggers = new[]{"fruit","fruits","dessert","sucré"},
//                Any = new[]{"pomme","banane","orange","citron","citron vert","fraise","poire","raisin","tomate"}
//            },
//            new CategoryRule {
//                Name = "epices",
//                Triggers = new[]{"epice","épice","epices","épices","spice","spicy","assaisonnement"},
//                Any = new[]{"cumin","paprika","curcuma","vadouvan","curry","poivre","piment","gingembre","muscade","thym"}
//            },
//            new CategoryRule {
//                Name = "herbes",
//                Triggers = new[]{"herbe","herbes","aromatique","aromatiques"},
//                Any = new[]{"persil","ciboulette","basilic","menthe","aneth","romarin","thym","coriandre"}
//            },
//            new CategoryRule {
//                Name = "laitiers",
//                Triggers = new[]{"laitier","laitiers","dairy","fromager","crémier"},
//                Any = new[]{"lait","beurre","fromage","crème","yaourt","crème aigre","mozzarella","parmesan","lait de coco"}
//            },
//            new CategoryRule {
//                Name = "viandes",
//                Triggers = new[]{"viande","viandes","carné","meat","steak","charcuterie"},
//                Any = new[]{"boeuf","bœuf","porc","poulet","dinde","agneau","veau","saucisse","jambon","lardons","filet de porc"}
//            },
//            new CategoryRule {
//                Name = "poissons",
//                Triggers = new[]{"poisson","poissons","seafood","poissonnerie","poissonnier"},
//                Any = new[]{"saumon","thon","lieu","filet de lieu","cabillaud","merlu","colin","crevette","moule"}
//            },
//            new CategoryRule {
//                Name = "proteines",
//                Triggers = new[]{"protéine","protéines","protein","proteins"},
//                Any = new[]{"oeuf","œuf","tofu","tempeh","pois chiches","lentilles","haricots","saucisse","jambon","poulet","filet de porc","fromage"}
//            },
//            new CategoryRule {
//                Name = "frais",
//                Triggers = new[]{"frais","fraiche","fraîche","fraîches","réfrigéré","frigo","perissable","périssable"},
//                Any = new[]{"salade","tomate","concombre","yaourt","fromage","beurre","crème","herbes","viande","poisson"}
//            },
//            new CategoryRule {
//                Name = "piquant",
//                Triggers = new[]{"piquant","pimenté","fort","épicé","spicy","hot"},
//                Any = new[]{"piment","harissa","sriracha","tabasco","curry","paprika","gingembre","poivre","pâte de curry","pâte de curry vert"}
//            },
//            new CategoryRule {
//                Name = "sauces",
//                Triggers = new[]{"sauce","sauces","condiment","condiments","topping"},
//                Any = new[]{"mayonnaise","ketchup","moutarde","soja","sauce soja","worcestershire","BBQ","harissa","sriracha","pâte de curry","pâte de curry vert","lait de coco","crème"}
//            },
//            new CategoryRule {
//                Name = "condiments",
//                Triggers = new[]{"condiment","condiments","pickles","vinaigre","cornichon"},
//                Any = new[]{"moutarde","vinaigre","huile","cornichon","câpres","poivre","sel"}
//            },
//            new CategoryRule {
//                Name = "cereales",
//                Triggers = new[]{"céréale","céréales","grains","cereal"},
//                Any = new[]{"riz","quinoa","semoule","boulgour","avoine","farine"}
//            },
//            new CategoryRule {
//                Name = "pates",
//                Triggers = new[]{"pate","pâtes","nouilles","pasta","spaghetti"},
//                Any = new[]{"pâtes","nouilles","nouilles de riz","spaghetti","penne","farfalle"}
//            },
//            new CategoryRule {
//                Name = "sandwich",
//                Triggers = new[]{"sandwich","sandwichs","sandwiches","wrap","tacos","kebab","burger"},
//                Any = new[]{"pain","salade","tomate","oignon","fromage","jambon","saucisse","poulet","sauce","cornichon"}
//            },
//            new CategoryRule {
//                Name = "sucre",
//                Triggers = new[]{"sucre","sucré","dessert","patisserie","pâtisserie","gâteau"},
//                Any = new[]{"sucre","chocolat","vanille","beurre","farine","lait","œuf","oeuf","pomme","banane","crème"}
//            },
//        };

//        static List<string> TryCategoryExpand(string query, List<string> allowed)
//        {
//            var nq = Key(query);
//            var hits = new List<string>();

//            foreach (var rule in CATEGORY_RULES)
//            {
//                if (!rule.Triggers.Any(t => nq.Contains(Key(t)))) continue;

//                var allowedHits = allowed.Where(a =>
//                {
//                    // normalisé dans les helpers
//                    bool anyOk = rule.Any.Length == 0 || AnyTokenMatches(a, rule.Any);
//                    bool mustOk = rule.Must.Length == 0 || AllTokensMatch(a, rule.Must);
//                    bool notOk = rule.Not.Length > 0 && AnyTokenMatches(a, rule.Not);
//                    return anyOk && mustOk && !notOk;
//                });

//                hits.AddRange(allowedHits);
//            }

//            return hits.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
//        }

//        // =========================
//        // Helpers — Parsing IA
//        // =========================
//        static List<string> ExtractNames(string responseText)
//        {
//            // 1) tableau JSON brut -> ["...","..."]
//            try
//            {
//                var arr = JsonSerializer.Deserialize<string[]>(responseText);
//                if (arr is not null && arr.Length > 0)
//                    return arr.Where(s => !string.IsNullOrWhiteSpace(s)).ToList();
//            }
//            catch { /* not an array */ }

//            // 2) objet { "ingredients": [...] } ou { "ingrédients": [...] }
//            try
//            {
//                using var inner = JsonDocument.Parse(responseText);
//                if (inner.RootElement.TryGetProperty("ingredients", out var arr1) && arr1.ValueKind == JsonValueKind.Array)
//                    return arr1.EnumerateArray().Select(e => e.GetString() ?? "").Where(s => !string.IsNullOrWhiteSpace(s)).ToList();

//                if (inner.RootElement.TryGetProperty("ingrédients", out var arr2) && arr2.ValueKind == JsonValueKind.Array)
//                    return arr2.EnumerateArray().Select(e => e.GetString() ?? "").Where(s => !string.IsNullOrWhiteSpace(s)).ToList();
//            }
//            catch { /* not an object */ }

//            // 3) JSON encodé dans une string (rare mais vu chez Qwen)
//            var mObj = Regex.Match(responseText, @"\{[\s\S]*\}");
//            if (mObj.Success)
//            {
//                try
//                {
//                    using var objDoc = JsonDocument.Parse(mObj.Value);
//                    if (objDoc.RootElement.TryGetProperty("ingredients", out var arr1) && arr1.ValueKind == JsonValueKind.Array)
//                        return arr1.EnumerateArray().Select(e => e.GetString() ?? "").Where(s => !string.IsNullOrWhiteSpace(s)).ToList();

//                    if (objDoc.RootElement.TryGetProperty("ingrédients", out var arr2) && arr2.ValueKind == JsonValueKind.Array)
//                        return arr2.EnumerateArray().Select(e => e.GetString() ?? "").Where(s => !string.IsNullOrWhiteSpace(s)).ToList();
//                }
//                catch { /* ignore */ }
//            }

//            // 4) fallback : extrait le premier [...] plausible
//            var mArr = Regex.Match(responseText, @"\[[\s\S]*?\]");
//            if (mArr.Success)
//            {
//                try
//                {
//                    var arr = JsonSerializer.Deserialize<string[]>(mArr.Value);
//                    if (arr is not null && arr.Length > 0)
//                        return arr.Where(s => !string.IsNullOrWhiteSpace(s)).ToList();
//                }
//                catch { /* ignore */ }
//            }

//            return new List<string>();
//        }

//        static List<string> MapToAllowed(IEnumerable<string> names, List<string> allowed)
//        {
//            var mapAllowed = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
//            foreach (var a in allowed)
//            {
//                var k = Key(a);
//                if (!mapAllowed.ContainsKey(k))
//                    mapAllowed[k] = a;
//            }

//            var result = new List<string>();
//            var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

//            foreach (var n in names)
//            {
//                var k = Key(n);
//                if (mapAllowed.TryGetValue(k, out var official))
//                {
//                    if (seen.Add(official))
//                        result.Add(official);
//                }
//            }
//            return result;
//        }

//        // --- Helpers de matching précis ---
//        static bool ContainsPhraseOrWord(string haystack, string token)
//        {
//            // les deux sont déjà normalisés via Key() avant appel
//            if (string.IsNullOrWhiteSpace(token)) return false;

//            if (token.Contains(' '))
//            {
//                // multi-mots : on garde le contains sur la phrase normalisée
//                return haystack.Contains(token);
//            }

//            // 1 mot : on exige un match sur "mot entier"
//            // ex: "oignon" ne doit pas faire matcher "saucisse de porc ... aux oignons caramélisés"
//            var words = haystack.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
//            foreach (var w in words)
//                if (w.Equals(token, StringComparison.Ordinal)) return true;

//            return false;
//        }

//        static bool AnyTokenMatches(string haystack, IEnumerable<string> tokens)
//        {
//            var h = Key(haystack);
//            foreach (var t in tokens)
//            {
//                var tok = Key(t);
//                if (ContainsPhraseOrWord(h, tok)) return true;
//            }
//            return false;
//        }

//        static bool AllTokensMatch(string haystack, IEnumerable<string> tokens)
//        {
//            var h = Key(haystack);
//            foreach (var t in tokens)
//            {
//                var tok = Key(t);
//                if (!ContainsPhraseOrWord(h, tok)) return false;
//            }
//            return true;
//        }

//    }
//}
