using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text.RegularExpressions;
using GxMcp.Worker.Models;
using GxMcp.Worker.Helpers;
using Newtonsoft.Json.Linq;

namespace GxMcp.Worker.Services
{
    public class SearchService
    {
        private readonly IndexCacheService _indexCacheService;
        private readonly ObjectService _objectService;
        private readonly VectorService _vectorService = new VectorService();
        private static readonly BoundedStringCache _queryCache = new BoundedStringCache(512);
        private static DateTime _lastIndexTime = DateTime.MinValue;

        public SearchService(IndexCacheService indexCacheService, ObjectService objectService = null)
        {
            _indexCacheService = indexCacheService;
            _objectService = objectService;
        }

        public string Search(string query, string typeFilter = null, string domainFilter = null, int limit = 50, bool exactMatch = false,
            string sort = null, DateTime since = default(DateTime), DateTime modifiedBefore = default(DateTime), string cursor = null)
        {
            // PERFORMANCE (W-M3): instrument the search hot path so regressions surface in logs.
            // Threshold 50ms keeps noise low while catching any real degradation.
            var sw = Stopwatch.StartNew();
            try
            {
                // Fast-path: a literal name lookup that doesn't need the index. Cold-start
                // indexing on huge KBs takes minutes — agents asking for a known-name object
                // shouldn't block on it. Falls through to the normal index path if the
                // direct SDK lookup misses.
                var directHit = TryDirectLookup(query, typeFilter, exactMatch);
                if (directHit != null) return directHit;

                bool indexMissing = _indexCacheService.IsIndexMissing;
                bool scanning = _indexCacheService.IsScanning;
                if (indexMissing && !scanning)
                {
                    try { _indexCacheService.KbService?.BulkIndex(); } catch { }
                    scanning = _indexCacheService.IsScanning;
                }

                var index = _indexCacheService.GetIndex();
                bool indexEmpty = index == null || index.Objects.Count == 0;

                // If we genuinely have nothing yet, return progress info — but DON'T pretend
                // it's a "zero results" search. _meta.indexStatus = "warming" tells the agent
                // to retry once indexing progresses, while still reporting the (zero) snapshot.
                if (indexEmpty && scanning)
                {
                    return BuildPartialResponse(query, new object[0], 0, scanning: true);
                }
                if (indexEmpty)
                {
                    try { _indexCacheService.KbService?.BulkIndex(); } catch { }
                    return BuildPartialResponse(query, new object[0], 0, scanning: true);
                }

                if (index.LastUpdated > _lastIndexTime) { _queryCache.Clear(); _lastIndexTime = index.LastUpdated; }

                bool isQuick = !string.IsNullOrEmpty(query) && query.IndexOf("@quick", StringComparison.OrdinalIgnoreCase) >= 0;
                if (isQuick)
                {
                    query = Regex.Replace(query, @"\s*@quick\b", "", RegexOptions.IgnoreCase).Trim();
                }

                // v2.6.8: temporal/sort/cursor controls participate in the cache key
                // so callers paging by cursor or filtering by date don't collide with
                // the cached relevance-sorted page.
                string cacheKey = string.Format("{0}|{1}|{2}|{3}|{4}|{5}|s={6}|sn={7}|mb={8}|cu={9}",
                    query ?? "", typeFilter ?? "", domainFilter ?? "", limit,
                    isQuick ? "quick" : "full", exactMatch ? "exact" : "fuzzy",
                    sort ?? "", since.Ticks, modifiedBefore.Ticks, cursor ?? "");
                if (_queryCache.TryGetValue(cacheKey, out var cached)) return cached;

                var criteria = ParseQuery(query);
                if (!string.IsNullOrEmpty(typeFilter)) criteria.TypeFilter = typeFilter;
                if (!string.IsNullOrEmpty(domainFilter)) criteria.DomainFilter = domainFilter;

                if (exactMatch && criteria.Terms.Count > 0)
                {
                    var exactCandidates = index.Objects.Values
                        .Where(e => criteria.Terms.Any(t => string.Equals(e.Name, t, StringComparison.OrdinalIgnoreCase)));
                    if (!string.IsNullOrEmpty(criteria.TypeFilter))
                        exactCandidates = exactCandidates.Where(e => IsTypeMatch(e.Type, criteria.TypeFilter));
                    var exactList = exactCandidates.ToList();
                    var exactObj = JObject.FromObject(new {
                        count = exactList.Count,
                        total = exactList.Count,
                        hasMore = false,
                        results = exactList.Select(e => new {
                            guid = e.Guid, name = e.Name, type = e.Type, description = e.Description,
                            parent = e.Parent, module = e.Module, path = e.Path, parentPath = e.ParentPath,
                            dataType = e.DataType, table = e.RootTable
                        })
                    });
                    if (exactList.Count > 0)
                    {
                        var top = exactList[0];
                        exactObj["_meta"] = new JObject
                        {
                            ["suggested_next"] = new JObject
                            {
                                ["tool"] = "genexus_read",
                                ["args"] = new JObject { ["name"] = top.Name, ["type"] = top.Type }
                            }
                        };
                    }
                    var exactJson = exactObj.ToString(Newtonsoft.Json.Formatting.None);
                    _queryCache.TryAdd(cacheKey, exactJson);
                    return exactJson;
                }

                IEnumerable<SearchIndex.IndexEntry> sourceSet = null;

                if (criteria.ParentPathFilter != null && index.ChildrenByParent != null)
                {
                    if (index.ChildrenByParent.TryGetValue(criteria.ParentPathFilter, out var children))
                    {
                        sourceSet = children;
                    }
                    else
                    {
                        sourceSet = Enumerable.Empty<SearchIndex.IndexEntry>();
                    }
                }
                else if (!string.IsNullOrEmpty(criteria.ParentFilter) && index.ChildrenByParent != null)
                {
                    if (index.ChildrenByParent.TryGetValue(criteria.ParentFilter, out var children))
                    {
                        sourceSet = children;
                    }
                    else
                    {
                        sourceSet = Enumerable.Empty<SearchIndex.IndexEntry>();
                    }
                }
                else
                {
                    sourceSet = index.Objects.Values;
                }

                // PERFORMANCE (W-M4): cap PLINQ parallelism so large KBs (50k+ objects) on
                // 16+ core machines don't spawn one task per core and pressure the GC.
                int dop = Math.Min(4, Math.Max(1, Environment.ProcessorCount));
                var queryResults = sourceSet.AsParallel().WithDegreeOfParallelism(dop);

                if (!string.IsNullOrEmpty(criteria.TypeFilter))
                    queryResults = queryResults.Where(e => IsTypeMatch(e.Type, criteria.TypeFilter));
                
                if (!string.IsNullOrEmpty(criteria.DomainFilter))
                    queryResults = queryResults.Where(e => string.Equals(e.BusinessDomain, criteria.DomainFilter, StringComparison.OrdinalIgnoreCase));

                if (!string.IsNullOrEmpty(criteria.DescriptionFilter))
                    queryResults = queryResults.Where(e => (e.Description ?? "").IndexOf(criteria.DescriptionFilter, StringComparison.OrdinalIgnoreCase) >= 0);

                if (!string.IsNullOrEmpty(criteria.MetadataFilter))
                    queryResults = queryResults.Where(e => 
                        (e.ParmRule ?? "").IndexOf(criteria.MetadataFilter, StringComparison.OrdinalIgnoreCase) >= 0 ||
                        (e.DataType ?? "").IndexOf(criteria.MetadataFilter, StringComparison.OrdinalIgnoreCase) >= 0 ||
                        (e.RootTable ?? "").IndexOf(criteria.MetadataFilter, StringComparison.OrdinalIgnoreCase) >= 0
                    );

                if (criteria.ParentPathFilter != null && (sourceSet == index.Objects.Values))
                    queryResults = queryResults.Where(e => string.Equals(e.ParentPath ?? string.Empty, criteria.ParentPathFilter, StringComparison.OrdinalIgnoreCase));

                if (!string.IsNullOrEmpty(criteria.ParentFilter) && (sourceSet == index.Objects.Values))
                    queryResults = queryResults.Where(e => string.Equals(e.Parent, criteria.ParentFilter, StringComparison.OrdinalIgnoreCase));

                // v2.6.8: temporal bounds. Same semantics as list_objects — Since
                // inclusive, ModifiedBefore exclusive. Applied before ranking so
                // the score budget doesn't get spent on out-of-window items.
                if (since > DateTime.MinValue)
                    queryResults = queryResults.Where(e => e.LastUpdate >= since);
                if (modifiedBefore > DateTime.MinValue)
                    queryResults = queryResults.Where(e => e.LastUpdate > DateTime.MinValue && e.LastUpdate < modifiedBefore);

                if (!string.IsNullOrEmpty(criteria.UsedByFilter))
                {
                    // Build the set of objects that reference the target via the inverted CalledBy index.
                    // Multiple entries can share a name across types (e.g. Attribute:X and Domain:X), so collect all.
                    var consumerNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
                    foreach (var t in index.Objects.Values)
                    {
                        if (!string.Equals(t.Name, criteria.UsedByFilter, StringComparison.OrdinalIgnoreCase)) continue;
                        if (t.CalledBy == null) continue;
                        lock (t.CalledBy)
                        {
                            foreach (var c in t.CalledBy)
                            {
                                if (!string.IsNullOrEmpty(c)) consumerNames.Add(c);
                            }
                        }
                    }

                    queryResults = queryResults.Where(e =>
                        consumerNames.Contains(e.Name) ||
                        (e.RootTable != null && string.Equals(e.RootTable, criteria.UsedByFilter, StringComparison.OrdinalIgnoreCase)) ||
                        (e.Calls != null && e.Calls.Exists(c => string.Equals(c, criteria.UsedByFilter, StringComparison.OrdinalIgnoreCase))) ||
                        (e.Tables != null && e.Tables.Exists(t => string.Equals(t, criteria.UsedByFilter, StringComparison.OrdinalIgnoreCase)))
                    );
                }

                float[] queryEmbedding = null;
                if (!isQuick && criteria.Terms.Count > 0)
                {
                    queryEmbedding = _vectorService.ComputeEmbedding(query);
                }

                var rankedAll = queryResults
                    .Select(entry =>
                    {
                        int score = 0;
                        float vectorScore = 0;

                        if (criteria.Terms.Count > 0)
                        {
                            score = CalculateSemanticScore(entry, criteria.Terms, criteria.TypeFilter);

                            // SHORT-CIRCUIT: structural noise types should never surface unless
                            // the caller explicitly asked for them via typeFilter. Index/Folder/
                            // Module had been polluting query results when there was no exact
                            // name match (see Probe#2: "Country" returned 15 Index objects).
                            bool isNoiseType = entry.Type == "Folder" || entry.Type == "Module" || entry.Type == "Index";
                            bool noiseExplicitlyRequested = !string.IsNullOrEmpty(criteria.TypeFilter)
                                && IsTypeMatch(entry.Type, criteria.TypeFilter);
                            if (score <= 0 && isNoiseType && !noiseExplicitlyRequested)
                                return new RankedResult { Score = -1 };
                            if (isNoiseType && !noiseExplicitlyRequested)
                                return new RankedResult { Score = -1 };

                            if (!isQuick && entry.Embedding != null && queryEmbedding != null)
                            {
                                vectorScore = _vectorService.CosineSimilarity(queryEmbedding, entry.Embedding);
                            }
                            if (!isQuick && score <= 0 && vectorScore < 0.45f)
                                return new RankedResult { Score = -1 };
                        }
                        else
                        {
                            score = (entry.Type == "Folder" || entry.Type == "Module") ? 1000 : 1; // Default browsing order
                        }

                        int finalScore = score + (int)(vectorScore * 1000);
                        return new RankedResult { Entry = entry, Score = finalScore, VectorSimilarity = vectorScore };
                    })
                    .Where(r => r != null) // Safety check
                    .Where(r => r.Score > 0)
                    .ToList();

                // v2.6.8: sort selector. "lastUpdate" bypasses relevance ranking and
                // returns newest-first — the agent explicitly opted out of score
                // ordering in favor of recency.
                bool sortByLastUpdate = !string.IsNullOrEmpty(sort) &&
                    string.Equals(sort, "lastUpdate", StringComparison.OrdinalIgnoreCase);
                if (sortByLastUpdate)
                {
                    rankedAll = rankedAll
                        .OrderByDescending(r => r.Entry.LastUpdate)
                        .ThenBy(r => r.Entry.Name ?? string.Empty, StringComparer.OrdinalIgnoreCase)
                        .ThenBy(r => r.Entry.Guid ?? string.Empty, StringComparer.OrdinalIgnoreCase)
                        .ToList();
                }
                else
                {
                    rankedAll = rankedAll
                        .OrderByDescending(r => r.Score)
                        .ThenBy(r => r.Entry.Name)
                        .ToList();
                }

                int total = rankedAll.Count;

                // v2.6.8: stable cursor (only honored with sort=lastUpdate, same as list_objects).
                int startIndex = 0;
                if (sortByLastUpdate && !string.IsNullOrEmpty(cursor))
                {
                    var decoded = ListService.DecodeCursor(cursor);
                    if (decoded.HasValue)
                    {
                        var (cursorTs, cursorName, cursorGuid) = decoded.Value;
                        // v2.6.8 (review C1): predicate mirrors the sort tuple
                        // (LastUpdate desc, Name asc, Guid asc) — see ListService
                        // for the rationale.
                        int resumeAt = rankedAll.FindIndex(r =>
                        {
                            if (r.Entry.LastUpdate < cursorTs) return true;
                            if (r.Entry.LastUpdate != cursorTs) return false;
                            int byName = string.Compare(r.Entry.Name ?? string.Empty, cursorName ?? string.Empty, StringComparison.OrdinalIgnoreCase);
                            if (byName > 0) return true;
                            if (byName < 0) return false;
                            return string.Compare(r.Entry.Guid ?? string.Empty, cursorGuid ?? string.Empty, StringComparison.OrdinalIgnoreCase) > 0;
                        });
                        if (resumeAt >= 0) startIndex = resumeAt;
                        else startIndex = total;
                    }
                }

                var effectiveLimit = limit <= 0 ? total : limit;
                var scoredResults = rankedAll.Skip(startIndex).Take(effectiveLimit).ToList();
                bool hasMore = (startIndex + scoredResults.Count) < total;

                JObject responseObj;
                // v2.6.8: stringify lastUpdate once per row; null when unknown so the
                // gateway projector can detect "no lifecycle data" cleanly.
                string FormatLu(SearchIndex.IndexEntry e) =>
                    e.LastUpdate > DateTime.MinValue ? e.LastUpdate.ToUniversalTime().ToString("o") : null;

                if (isQuick)
                {
                    responseObj = JObject.FromObject(new {
                        count = scoredResults.Count,
                        total,
                        hasMore,
                        results = scoredResults.Select(r => new {
                            guid = r.Entry.Guid,
                            name = r.Entry.Name,
                            type = r.Entry.Type,
                            parent = r.Entry.Parent,
                            module = r.Entry.Module,
                            path = r.Entry.Path,
                            parentPath = r.Entry.ParentPath,
                            lastUpdate = FormatLu(r.Entry)
                        })
                    });
                }
                else
                {
                    responseObj = JObject.FromObject(new {
                        count = scoredResults.Count,
                        total,
                        hasMore,
                        results = scoredResults.Select(r => new {
                            guid = r.Entry.Guid,
                            name = r.Entry.Name,
                            type = r.Entry.Type,
                            description = r.Entry.Description,
                            parm = r.Entry.ParmRule,
                            snippet = r.Entry.SourceSnippet,
                            parent = r.Entry.Parent,
                            module = r.Entry.Module,
                            path = r.Entry.Path,
                            parentPath = r.Entry.ParentPath,
                            dataType = r.Entry.DataType,
                            length = r.Entry.Length,
                            decimals = r.Entry.Decimals,
                            table = r.Entry.RootTable,
                            similarity = r.VectorSimilarity,
                            lastUpdate = FormatLu(r.Entry)
                        })
                    });
                }

                // v2.6.8: nextCursor for stable temporal paging. Mirrors list_objects.
                if (sortByLastUpdate && hasMore && scoredResults.Count > 0)
                {
                    var last = scoredResults[scoredResults.Count - 1].Entry;
                    var token = ListService.EncodeCursor(last.LastUpdate, last.Name, last.Guid);
                    if (!string.IsNullOrEmpty(token)) responseObj["nextCursor"] = token;
                }

                // Only surface a "suggested_next" when the top result is a confident
                // name match (exact >= 10000, prefix >= 1000). Substring/vector-only
                // hits had been driving agents to wrong objects (e.g. literal
                // "Country" suggesting "QueryViewerCountry"). Always emit a
                // match_quality so the caller can decide.
                var meta = (responseObj["_meta"] as JObject) ?? new JObject();
                string matchQuality = "none";
                if (scoredResults.Count > 0)
                {
                    int topScore = scoredResults[0].Score;
                    string topName = scoredResults[0].Entry?.Name ?? "";
                    bool topIsExact = criteria.Terms.Any(t => string.Equals(topName, t, StringComparison.OrdinalIgnoreCase));
                    bool topIsPrefix = !topIsExact && criteria.Terms.Any(t => topName.StartsWith(t, StringComparison.OrdinalIgnoreCase));
                    if (topIsExact) matchQuality = "exact";
                    else if (topIsPrefix) matchQuality = "prefix";
                    else if (topScore >= 500) matchQuality = "substring";
                    else matchQuality = "vector";
                }
                meta["match_quality"] = matchQuality;
                if (matchQuality == "exact" || matchQuality == "prefix")
                {
                    var suggestion = BuildSuggestedNext(scoredResults);
                    if (suggestion != null) meta["suggested_next"] = suggestion;
                }
                responseObj["_meta"] = meta;

                if (_indexCacheService.IsScanning)
                {
                    AnnotatePartial(responseObj);
                }

                string json = responseObj.ToString(Newtonsoft.Json.Formatting.None);
                if (!_indexCacheService.IsScanning) _queryCache.TryAdd(cacheKey, json);

                if (!isQuick && criteria.Terms.Count > 0 && scoredResults.Count > 0)
                {
                    var topGuids = scoredResults.Take(5)
                        .Where(r => !string.IsNullOrEmpty(r.Entry.Guid))
                        .Select(r => new Guid(r.Entry.Guid))
                        .ToList();

                    Program.EnqueueBackground(() => {
                        try {
                            var kb = _indexCacheService.KbService?.GetKB();
                            if (kb == null) return;
                            foreach (var guid in topGuids) {
                                var obj = kb.DesignModel.Objects.Get(guid);
                                if (obj != null) Logger.Debug($"[Warm-up] Loaded {obj.Name} into SDK cache.");
                            }
                        } catch { }
                    });
                }

                return json;
            }
            catch (Exception ex) { return "{\"status\":\"Error\",\"message\": \"" + CommandDispatcher.EscapeJsonString(ex.Message) + "\"}"; }
            finally
            {
                sw.Stop();
                if (sw.ElapsedMilliseconds > 50)
                {
                    Logger.Info($"[SEARCH-SLOW] {sw.ElapsedMilliseconds}ms query='{query}' type='{typeFilter}' limit={limit} exact={exactMatch}");
                }
            }
        }

        // Returns a serialized result if the query looks like a literal object name
        // and an SDK direct lookup succeeded. Returns null to fall through to the
        // normal index-based path.
        private string TryDirectLookup(string query, string typeFilter, bool exactMatch)
        {
            if (_objectService == null) return null;
            if (string.IsNullOrWhiteSpace(query)) return null;

            string trimmed = query.Trim();
            // Skip if the query carries filter syntax, wildcards, or multi-term semantics.
            if (trimmed.IndexOfAny(new[] { ' ', ':', '*', '@', '"', '?', '/' }) >= 0) return null;
            // Object names are typically reasonable identifiers.
            if (trimmed.Length > 80) return null;

            try
            {
                var obj = _objectService.FindObject(trimmed, typeFilter);
                if (obj == null) return null;

                string typeName = null;
                try { typeName = obj.TypeDescriptor?.Name; } catch { }
                string guid = null;
                try { guid = obj.Guid.ToString(); } catch { }

                var single = new JObject
                {
                    ["guid"] = guid,
                    ["name"] = obj.Name,
                    ["type"] = typeName
                };

                var responseObj = new JObject
                {
                    ["count"] = 1,
                    ["total"] = 1,
                    ["hasMore"] = false,
                    ["results"] = new JArray(single),
                    ["_meta"] = new JObject
                    {
                        ["direct_lookup"] = true,
                        // Direct lookup means the SDK returned an exact name match —
                        // surface match_quality so the envelope is consistent with the
                        // index path (callers can branch on this field uniformly).
                        ["match_quality"] = "exact",
                        ["suggested_next"] = BuildSuggestedReadFor(obj.Name, typeName)
                    }
                };

                // Annotate partial whenever the background bulk index hasn't finished —
                // even on a direct-lookup success — so the agent knows that a re-query
                // (e.g. broader search by attribute) may yield more results once the
                // index is warm. Probe#1 observed cold-state queries silently omitting
                // this flag.
                if (_indexCacheService.IsScanning || _indexCacheService.IsIndexMissing)
                    AnnotatePartial(responseObj);

                return responseObj.ToString(Newtonsoft.Json.Formatting.None);
            }
            catch (Exception ex)
            {
                Logger.Debug("Direct lookup failed (" + trimmed + "): " + ex.Message);
                return null;
            }
        }

        // Build a zero/partial-result payload that signals indexing is still running,
        // so agents know to retry and clients can render a progress hint.
        private string BuildPartialResponse(string query, object[] results, int total, bool scanning)
        {
            var resp = new JObject
            {
                ["count"] = results?.Length ?? 0,
                ["total"] = total,
                ["hasMore"] = false,
                ["results"] = results == null ? new JArray() : JArray.FromObject(results)
            };
            AnnotatePartial(resp);
            if (!scanning) resp["_meta"]["indexStatus"] = "empty";
            return resp.ToString(Newtonsoft.Json.Formatting.None);
        }

        private void AnnotatePartial(JObject responseObj)
        {
            var meta = responseObj["_meta"] as JObject ?? new JObject();
            int processed = _indexCacheService.KbService?.IndexProcessed ?? 0;
            int total = _indexCacheService.KbService?.IndexTotal ?? 0;
            int pct = total > 0 ? (int)Math.Round(100.0 * processed / total) : 0;

            meta["partial"] = true;
            meta["indexStatus"] = "scanning";
            meta["indexed_count"] = processed;
            meta["index_total_estimated"] = total;
            meta["indexed_pct"] = pct;
            meta["progress_token"] = "genexus-mcp-bulk-index";
            meta["retry_hint"] = "Indexing in background. Re-run query for more results; read/edit/build/list tools work without the index.";
            responseObj["_meta"] = meta;
        }

        private static JObject BuildSuggestedNext(List<RankedResult> results)
        {
            if (results == null || results.Count == 0) return null;
            var top = results[0].Entry;
            if (top == null) return null;
            return BuildSuggestedReadFor(top.Name, top.Type);
        }

        public static JObject BuildSuggestedReadFor(string name, string type)
        {
            if (string.IsNullOrEmpty(name)) return null;
            return new JObject
            {
                ["tool"] = "genexus_read",
                ["args"] = new JObject { ["name"] = name, ["type"] = type }
            };
        }

        private int CalculateSemanticScore(SearchIndex.IndexEntry entry, HashSet<string> terms, string typeFilter)
        {
            int score = 0;
            string name = entry.Name ?? "";
            string desc = entry.Description ?? "";

            foreach (var term in terms) {
                if (name.Equals(term, StringComparison.OrdinalIgnoreCase)) score += 10000;
                else if (name.StartsWith(term, StringComparison.OrdinalIgnoreCase)) score += 1000;
                else if (name.IndexOf(term, StringComparison.OrdinalIgnoreCase) >= 0) score += 500;

                if (desc.IndexOf(term, StringComparison.OrdinalIgnoreCase) >= 0) score += 300;

                if (entry.Keywords != null && entry.Keywords.Contains(term, StringComparer.OrdinalIgnoreCase)) score += 800;
                if (entry.Tags != null && entry.Tags.Contains(term, StringComparer.OrdinalIgnoreCase)) score += 800;

                if (entry.Tables != null && entry.Tables.Contains(term, StringComparer.OrdinalIgnoreCase))
                {
                    bool boostForAttributeMember = string.Equals(typeFilter, "Table", StringComparison.OrdinalIgnoreCase)
                                                   && string.Equals(entry.Type, "Table", StringComparison.OrdinalIgnoreCase)
                                                   && _indexCacheService.LooksLikeAttributeName(term);
                    score += boostForAttributeMember ? 5000 : 400;
                }
                if (entry.Calls != null && entry.Calls.Contains(term, StringComparer.OrdinalIgnoreCase)) score += 400;
            }
            return score;
        }

        private bool IsTypeMatch(string type, string query)
        {
            if (string.IsNullOrEmpty(type) || string.IsNullOrEmpty(query)) return false;
            string t = type.ToLower(); string q = query.ToLower();
            if (q == "prc" || q == "procedure" || q == "proc") return t.Contains("procedure");
            if (q == "trn" || q == "transaction") return t.Contains("transaction");
            if (q == "tab" || q == "table") return t == "table";
            if (q == "wp" || q == "webpanel") return t.Contains("webpanel");
            if (q == "dp" || q == "dataprovider") return t.Contains("dataprovider");
            if (q == "sdt") return t.Contains("sdt");
            if (q == "attr" || q == "attribute") return t.Contains("attribute");
            return t.Contains(q);
        }

        private SearchCriteria ParseQuery(string query)
        {
            var c = new SearchCriteria();
            if (string.IsNullOrEmpty(query)) return c;

            query = ExtractFilter(query, "description", value => c.DescriptionFilter = value);
            query = ExtractFilter(query, "metadata", value => c.MetadataFilter = value);
            query = ExtractFilter(query, "usedby", value => c.UsedByFilter = value);
            query = ExtractFilter(query, "parentPath", value => c.ParentPathFilter = value);
            query = ExtractFilter(query, "parent", value => c.ParentFilter = value);
            query = ExtractFilter(query, "type", value => c.TypeFilter = value);

            foreach (var part in query.Split(new[]{' '}, StringSplitOptions.RemoveEmptyEntries)) {
                c.Terms.Add(part.ToLowerInvariant());
            }
            return c;
        }

        private string ExtractFilter(string query, string filterName, Action<string> assign)
        {
            var pattern = string.Format(@"(?:^|\s){0}:(?:""(?<quoted>[^""]+)""|(?<plain>\S+))", Regex.Escape(filterName));
            var match = Regex.Match(query, pattern, RegexOptions.IgnoreCase);
            if (!match.Success) return query;

            var value = match.Groups["quoted"].Success
                ? match.Groups["quoted"].Value
                : match.Groups["plain"].Value;

            if (!string.IsNullOrWhiteSpace(value))
            {
                assign(value);
            }

            return query.Remove(match.Index, match.Length).Trim();
        }

        private class RankedResult { public SearchIndex.IndexEntry Entry { get; set; } public int Score { get; set; } public float VectorSimilarity { get; set; } }
        private class SearchCriteria { 
            public string TypeFilter { get; set; } public string ParentFilter { get; set; } public string ParentPathFilter { get; set; }
            public string UsedByFilter { get; set; } public string DomainFilter { get; set; } 
            public string DescriptionFilter { get; set; } public string MetadataFilter { get; set; }
            public HashSet<string> Terms { get; set; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase); 
        }
    }
}
