using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;

namespace UnityAssistant
{
    [InitializeOnLoad]
    public static class IndexerTools
    {
        private class IndexEntry
        {
            public string path;
            public string guid;
            public string root;
            public string type;
            public string name;
            public long size;
            public long mtime;
            public string[] symbols;
        }

        private static readonly ConcurrentDictionary<string, IndexEntry> _index = new ConcurrentDictionary<string, IndexEntry>();
        private static volatile bool _indexing;
        private static long _lastIndexUtc;

        static IndexerTools()
        {
            EditorApplication.delayCall += () => Task.Run(BuildIndex);
            AssetDatabase.importPackageStarted += _ => MarkDirty();
        }

        public static void MarkDirty() => Task.Run(BuildIndex);

        public static string IndexProject(Dictionary<string, object> args)
        {
            bool force = JsonUtil.GetBool(args, "force");
            if (!force && _indexing) return "{\"ok\":true,\"status\":\"already indexing\"}";
            if (force)
            {
                BuildIndex();
                return "{\"ok\":true,\"status\":\"complete\",\"total\":" + _index.Count + "}";
            }
            Task.Run(BuildIndex);
            return "{\"ok\":true,\"status\":\"started\"}";
        }

        private static void BuildIndex()
        {
            if (_indexing) return;
            _indexing = true;
            try
            {
                _index.Clear();
                int count = 0;
                foreach (var rootName in new[] { "Assets", "Packages", "ProjectSettings" })
                {
                    var root = Path.Combine(Directory.GetCurrentDirectory(), rootName);
                    if (!Directory.Exists(root)) continue;
                    foreach (var file in Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories))
                    {
                        if (ShouldSkip(file)) continue;
                        var info = new FileInfo(file);
                        var rel = rootName + file.Substring(root.Length).Replace('\\', '/');
                        var entry = new IndexEntry
                        {
                            path = rel,
                            guid = ReadGuidFromMeta(file + ".meta"),
                            root = rootName,
                            type = info.Extension.ToLower().TrimStart('.'),
                            name = Path.GetFileNameWithoutExtension(rel),
                            size = info.Length,
                            mtime = ((DateTimeOffset)info.LastWriteTimeUtc).ToUnixTimeSeconds()
                        };

                        if (entry.type == "cs" && info.Length < 500_000)
                            entry.symbols = ExtractSymbols(file);

                        _index[rel] = entry;
                        count++;
                    }
                }
                _lastIndexUtc = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
                Debug.Log($"[Gumo] Indexed {count} assets");
            }
            catch (Exception e) { Debug.LogError("[Gumo] Index error: " + e.Message); }
            finally { _indexing = false; }
        }

        private static bool ShouldSkip(string file)
        {
            if (file.EndsWith(".meta", StringComparison.OrdinalIgnoreCase)) return true;
            var normalized = file.Replace('\\', '/');
            return normalized.Contains("/Library/") || normalized.Contains("/Temp/") ||
                   normalized.Contains("/Obj/") || normalized.Contains("/obj/") ||
                   normalized.Contains("/.git/");
        }

        private static string ReadGuidFromMeta(string metaPath)
        {
            try
            {
                if (!File.Exists(metaPath)) return "";
                foreach (var line in File.ReadLines(metaPath))
                {
                    if (!line.StartsWith("guid:")) continue;
                    return line.Substring(5).Trim();
                }
            }
            catch { }
            return "";
        }

        private static string[] ExtractSymbols(string file)
        {
            try
            {
                var text = File.ReadAllText(file);
                var symbols = new List<string>();
                foreach (Match m in Regex.Matches(text, @"\b(class|struct|interface|enum)\s+([A-Za-z_][A-Za-z0-9_]*)"))
                    symbols.Add(m.Groups[1].Value + " " + m.Groups[2].Value);
                foreach (Match m in Regex.Matches(text, @"\b(public|private|protected|internal)\s+(?:static\s+|virtual\s+|override\s+|async\s+)*[\w<>\[\]]+\s+([A-Za-z_][A-Za-z0-9_]*)\s*\("))
                {
                    var name = m.Groups[2].Value;
                    if (name != "if" && name != "for" && name != "while" && name != "switch")
                        symbols.Add("method " + name);
                }
                return symbols.Distinct().Take(50).ToArray();
            }
            catch { return Array.Empty<string>(); }
        }

        public static string SearchIndex(Dictionary<string, object> args)
        {
            if (_index.Count == 0 && !_indexing) BuildIndex();
            string q = JsonUtil.Get(args, "query", "").Trim().ToLowerInvariant();
            if (string.IsNullOrEmpty(q))
                return "{\"ok\":false,\"error\":\"empty search query; use get_project_graph for broad context or pass a specific asset/script/symbol name\"}";
            string typeFilter = NormalizeTypeFilter(JsonUtil.Get(args, "type", ""));
            int limit = (int)JsonUtil.GetNum(args, "limit", 50);

            var matches = _index.Values
                .Where(e => MatchesTypeFilter(e, typeFilter))
                .Select(e => new { e, score = Score(e, q) })
                .Where(x => x.score > 0)
                .OrderByDescending(x => x.score)
                .Take(limit)
                .Select(x => x.e)
                .ToList();

            var sb = new StringBuilder("{\"ok\":true,\"total\":").Append(_index.Count);
            sb.Append(",\"indexed_at\":").Append(_lastIndexUtc);
            sb.Append(",\"indexing\":").Append(_indexing ? "true" : "false");
            sb.Append(",\"results\":[");
            for (int i = 0; i < matches.Count; i++)
            {
                if (i > 0) sb.Append(",");
                var e = matches[i];
                sb.Append("{\"path\":").Append(JsonUtil.Quote(e.path));
                sb.Append(",\"guid\":").Append(JsonUtil.Quote(e.guid));
                sb.Append(",\"root\":").Append(JsonUtil.Quote(e.root));
                sb.Append(",\"name\":").Append(JsonUtil.Quote(e.name));
                sb.Append(",\"type\":").Append(JsonUtil.Quote(e.type));
                sb.Append(",\"size\":").Append(e.size);
                if (e.symbols != null && e.symbols.Length > 0)
                {
                    sb.Append(",\"symbols\":[");
                    for (int j = 0; j < e.symbols.Length; j++)
                    { if (j > 0) sb.Append(","); sb.Append(JsonUtil.Quote(e.symbols[j])); }
                    sb.Append("]");
                }
                sb.Append("}");
            }
            sb.Append("]}");
            return sb.ToString();
        }

        public static string ResolveAsset(Dictionary<string, object> args)
        {
            if (_index.Count == 0 && !_indexing) BuildIndex();
            string q = JsonUtil.Get(args, "query", "").Trim();
            if (string.IsNullOrEmpty(q))
                return "{\"ok\":false,\"error\":\"empty resolve query; pass a specific asset, script, prefab, clip, controller, or class name\"}";
            string typeFilter = NormalizeTypeFilter(JsonUtil.Get(args, "type", ""));
            int limit = (int)JsonUtil.GetNum(args, "limit", 8);
            var matches = _index.Values
                .Where(e => MatchesTypeFilter(e, typeFilter))
                .Select(e => new { e, score = Score(e, q.ToLowerInvariant()) })
                .Where(x => x.score > 0)
                .OrderByDescending(x => x.score)
                .Take(limit)
                .ToList();

            var sb = new StringBuilder("{\"ok\":true,\"query\":").Append(JsonUtil.Quote(q));
            sb.Append(",\"resolved\":").Append(matches.Count > 0 && matches[0].score >= 18 ? "true" : "false");
            if (matches.Count > 0)
            {
                var best = matches[0].e;
                sb.Append(",\"best\":{\"path\":").Append(JsonUtil.Quote(best.path));
                sb.Append(",\"name\":").Append(JsonUtil.Quote(best.name));
                sb.Append(",\"type\":").Append(JsonUtil.Quote(best.type));
                sb.Append(",\"guid\":").Append(JsonUtil.Quote(best.guid));
                sb.Append(",\"score\":").Append(matches[0].score).Append("}");
            }
            sb.Append(",\"candidates\":[");
            for (int i = 0; i < matches.Count; i++)
            {
                if (i > 0) sb.Append(",");
                var e = matches[i].e;
                sb.Append("{\"path\":").Append(JsonUtil.Quote(e.path));
                sb.Append(",\"name\":").Append(JsonUtil.Quote(e.name));
                sb.Append(",\"type\":").Append(JsonUtil.Quote(e.type));
                sb.Append(",\"score\":").Append(matches[i].score).Append("}");
            }
            sb.Append("]}");
            return sb.ToString();
        }

        public static string GetProjectGraph(Dictionary<string, object> args)
        {
            int prefabLimit = (int)JsonUtil.GetNum(args, "prefab_limit", 30);
            int sceneLimit = (int)JsonUtil.GetNum(args, "scene_limit", 50);
            int assetLimit = (int)JsonUtil.GetNum(args, "asset_limit", 80);

            var entries = _index.Values.ToList();
            var byType = entries.GroupBy(e => e.type).OrderByDescending(g => g.Count()).Take(24).ToList();

            var sb = new StringBuilder("{\"ok\":true");
            sb.Append(",\"project_path\":").Append(JsonUtil.Quote(Directory.GetCurrentDirectory().Replace('\\', '/')));
            sb.Append(",\"indexed_at\":").Append(_lastIndexUtc);
            sb.Append(",\"indexing\":").Append(_indexing ? "true" : "false");
            sb.Append(",\"total\":").Append(entries.Count);

            sb.Append(",\"roots\":[");
            AppendRoot(sb, entries, "Assets", 0);
            AppendRoot(sb, entries, "Packages", 1);
            AppendRoot(sb, entries, "ProjectSettings", 2);
            sb.Append("]");

            sb.Append(",\"types\":[");
            for (int i = 0; i < byType.Count; i++)
            {
                if (i > 0) sb.Append(",");
                sb.Append("{\"type\":").Append(JsonUtil.Quote(byType[i].Key));
                sb.Append(",\"count\":").Append(byType[i].Count()).Append("}");
            }
            sb.Append("]");

            AppendPackages(sb);
            AppendScenes(sb, sceneLimit);
            AppendPrefabs(sb, prefabLimit);
            AppendImportantAssets(sb, entries, assetLimit);
            sb.Append("}");
            return sb.ToString();
        }

        private static void AppendRoot(StringBuilder sb, List<IndexEntry> entries, string root, int index)
        {
            if (index > 0) sb.Append(",");
            var rootEntries = entries.Where(e => e.root == root).ToList();
            sb.Append("{\"name\":").Append(JsonUtil.Quote(root));
            sb.Append(",\"count\":").Append(rootEntries.Count);
            sb.Append(",\"bytes\":").Append(rootEntries.Sum(e => e.size)).Append("}");
        }

        private static void AppendPackages(StringBuilder sb)
        {
            sb.Append(",\"packages\":[");
            var manifest = Path.Combine(Directory.GetCurrentDirectory(), "Packages", "manifest.json");
            bool first = true;
            if (File.Exists(manifest))
            {
                var text = File.ReadAllText(manifest);
                var matches = Regex.Matches(text, "\"([^\"]+)\"\\s*:\\s*\"([^\"]+)\"");
                foreach (Match m in matches)
                {
                    if (!m.Groups[1].Value.StartsWith("com.")) continue;
                    if (!first) sb.Append(",");
                    first = false;
                    sb.Append("{\"name\":").Append(JsonUtil.Quote(m.Groups[1].Value));
                    sb.Append(",\"version\":").Append(JsonUtil.Quote(m.Groups[2].Value)).Append("}");
                }
            }
            sb.Append("]");
        }

        private static void AppendScenes(StringBuilder sb, int limit)
        {
            sb.Append(",\"scenes\":[");
            var scenes = AssetDatabase.FindAssets("t:Scene")
                .Select(AssetDatabase.GUIDToAssetPath)
                .Where(p => p.StartsWith("Assets/"))
                .Take(limit)
                .ToArray();
            for (int i = 0; i < scenes.Length; i++)
            {
                if (i > 0) sb.Append(",");
                sb.Append("{\"path\":").Append(JsonUtil.Quote(scenes[i]));
                sb.Append(",\"name\":").Append(JsonUtil.Quote(Path.GetFileNameWithoutExtension(scenes[i]))).Append("}");
            }
            sb.Append("]");
        }

        private static void AppendPrefabs(StringBuilder sb, int limit)
        {
            sb.Append(",\"prefabs\":[");
            var prefabs = AssetDatabase.FindAssets("t:Prefab")
                .Select(AssetDatabase.GUIDToAssetPath)
                .Where(p => p.StartsWith("Assets/"))
                .Take(limit)
                .ToArray();
            for (int i = 0; i < prefabs.Length; i++)
            {
                if (i > 0) sb.Append(",");
                AppendPrefabSummary(sb, prefabs[i]);
            }
            sb.Append("]");
        }

        private static void AppendPrefabSummary(StringBuilder sb, string path)
        {
            var go = AssetDatabase.LoadAssetAtPath<GameObject>(path);
            sb.Append("{\"path\":").Append(JsonUtil.Quote(path));
            sb.Append(",\"name\":").Append(JsonUtil.Quote(Path.GetFileNameWithoutExtension(path)));
            if (!go)
            {
                sb.Append("}");
                return;
            }
            var transforms = go.GetComponentsInChildren<Transform>(true);
            var componentTypes = transforms
                .SelectMany(t => t.GetComponents<Component>())
                .Where(c => c != null)
                .Select(c => c.GetType().Name)
                .GroupBy(n => n)
                .OrderByDescending(g => g.Count())
                .Take(12)
                .Select(g => g.Key + ":" + g.Count())
                .ToArray();
            sb.Append(",\"object_count\":").Append(transforms.Length);
            sb.Append(",\"components\":[");
            for (int i = 0; i < componentTypes.Length; i++)
            {
                if (i > 0) sb.Append(",");
                sb.Append(JsonUtil.Quote(componentTypes[i]));
            }
            sb.Append("]}");
        }

        private static void AppendImportantAssets(StringBuilder sb, List<IndexEntry> entries, int limit)
        {
            var important = entries
                .Where(e => e.root == "Assets" && (e.type == "cs" || e.type == "prefab" || e.type == "unity" || e.type == "asset" || e.type == "mat" || e.type == "inputactions"))
                .OrderByDescending(e => e.mtime)
                .Take(limit)
                .ToList();
            sb.Append(",\"recent_important_assets\":[");
            for (int i = 0; i < important.Count; i++)
            {
                if (i > 0) sb.Append(",");
                var e = important[i];
                sb.Append("{\"path\":").Append(JsonUtil.Quote(e.path));
                sb.Append(",\"type\":").Append(JsonUtil.Quote(e.type));
                sb.Append(",\"mtime\":").Append(e.mtime).Append("}");
            }
            sb.Append("]");
        }

        private static int Score(IndexEntry e, string q)
        {
            if (string.IsNullOrEmpty(q)) return 1;
            int score = 0;
            var nameLower = e.name.ToLowerInvariant();
            var pathLower = e.path.ToLowerInvariant();
            var normalizedQ = NormalizeSearch(q);
            var normalizedName = NormalizeSearch(e.name);
            var normalizedPath = NormalizeSearch(e.path);
            if (nameLower == q) score += 100;
            else if (nameLower.StartsWith(q)) score += 50;
            else if (nameLower.Contains(q)) score += 20;
            if (pathLower == q) score += 90;
            else if (pathLower.EndsWith("/" + q) || pathLower.EndsWith("\\" + q)) score += 45;
            else if (pathLower.Contains(q)) score += 10;
            if (normalizedName == normalizedQ) score += 90;
            else if (normalizedName.StartsWith(normalizedQ)) score += 45;
            else if (normalizedName.Contains(normalizedQ)) score += 24;
            if (normalizedPath.Contains(normalizedQ)) score += 12;
            if (IsSubsequence(normalizedQ, normalizedName)) score += 14;
            if (IsSubsequence(normalizedQ, normalizedPath)) score += 7;
            var nameDistance = EditDistanceCapped(normalizedQ, normalizedName, 3);
            if (nameDistance == 1) score += 30;
            else if (nameDistance == 2) score += 18;
            else if (nameDistance == 3) score += 8;
            if (e.symbols != null)
                foreach (var s in e.symbols)
                {
                    var symbolLower = s.ToLowerInvariant();
                    var symbolNorm = NormalizeSearch(s);
                    if (symbolLower.Contains(q)) score += 10;
                    if (symbolNorm.Contains(normalizedQ)) score += 16;
                    if (IsSubsequence(normalizedQ, symbolNorm)) score += 6;
                }
            return score;
        }

        private static string NormalizeTypeFilter(string value)
        {
            if (string.IsNullOrEmpty(value)) return "";
            value = value.Trim().TrimStart('.').ToLowerInvariant();
            switch (value)
            {
                case "script":
                case "scripts":
                case "c#":
                case "csharp":
                case "monoscript":
                case "t:script":
                case "t:monoscript":
                    return "cs";
                case "animation":
                case "animationclip":
                case "clip":
                case "t:animationclip":
                    return "anim";
                case "animatorcontroller":
                case "controller":
                case "t:animatorcontroller":
                    return "controller";
                case "prefab":
                case "t:prefab":
                    return "prefab";
                case "scene":
                case "t:scene":
                    return "unity";
                case "material":
                case "mat":
                case "t:material":
                    return "mat";
                case "texture":
                case "texture2d":
                case "t:texture":
                case "t:texture2d":
                    return "png|jpg|jpeg|tga|psd";
                default:
                    return value;
            }
        }

        private static bool MatchesTypeFilter(IndexEntry e, string filter)
        {
            if (string.IsNullOrEmpty(filter)) return true;
            foreach (var part in filter.Split('|'))
                if (e.type == part) return true;
            return false;
        }

        private static string NormalizeSearch(string value)
        {
            if (string.IsNullOrEmpty(value)) return "";
            var sb = new StringBuilder(value.Length);
            foreach (var c in value.ToLowerInvariant())
                if (char.IsLetterOrDigit(c)) sb.Append(c);
            return sb.ToString();
        }

        private static bool IsSubsequence(string needle, string haystack)
        {
            if (string.IsNullOrEmpty(needle) || string.IsNullOrEmpty(haystack)) return false;
            int j = 0;
            for (int i = 0; i < haystack.Length && j < needle.Length; i++)
                if (haystack[i] == needle[j]) j++;
            return j == needle.Length;
        }

        private static int EditDistanceCapped(string a, string b, int cap)
        {
            if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return cap + 1;
            if (Math.Abs(a.Length - b.Length) > cap) return cap + 1;
            var prev = new int[b.Length + 1];
            var curr = new int[b.Length + 1];
            for (int j = 0; j <= b.Length; j++) prev[j] = j;
            for (int i = 1; i <= a.Length; i++)
            {
                curr[0] = i;
                int rowMin = curr[0];
                for (int j = 1; j <= b.Length; j++)
                {
                    int cost = a[i - 1] == b[j - 1] ? 0 : 1;
                    curr[j] = Math.Min(Math.Min(curr[j - 1] + 1, prev[j] + 1), prev[j - 1] + cost);
                    if (curr[j] < rowMin) rowMin = curr[j];
                }
                if (rowMin > cap) return cap + 1;
                var tmp = prev; prev = curr; curr = tmp;
            }
            return prev[b.Length];
        }

        public static string FindInFiles(Dictionary<string, object> args)
        {
            string pattern = JsonUtil.Get(args, "pattern");
            string ext = JsonUtil.Get(args, "extension", "cs");
            int maxResults = (int)JsonUtil.GetNum(args, "max_results", 100);
            var regex = new Regex(pattern, RegexOptions.Compiled);
            var sb = new StringBuilder("{\"ok\":true,\"matches\":[");
            int count = 0;
            bool first = true;
            foreach (var entry in _index.Values.Where(e => e.type == ext))
            {
                if (count >= maxResults) break;
                var full = Path.Combine(Directory.GetCurrentDirectory(), entry.path);
                if (!File.Exists(full)) continue;
                try
                {
                    var lines = File.ReadAllLines(full);
                    for (int i = 0; i < lines.Length; i++)
                    {
                        if (regex.IsMatch(lines[i]))
                        {
                            if (!first) sb.Append(",");
                            first = false;
                            sb.Append("{\"path\":").Append(JsonUtil.Quote(entry.path));
                            sb.Append(",\"line\":").Append(i + 1);
                            sb.Append(",\"text\":").Append(JsonUtil.Quote(lines[i].Trim())).Append("}");
                            count++;
                            if (count >= maxResults) break;
                        }
                    }
                }
                catch { }
            }
            sb.Append("]}");
            return sb.ToString();
        }
    }

    public class ProjectGraphAssetPostprocessor : AssetPostprocessor
    {
        private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
        {
            if ((importedAssets != null && importedAssets.Length > 0) ||
                (deletedAssets != null && deletedAssets.Length > 0) ||
                (movedAssets != null && movedAssets.Length > 0))
                IndexerTools.MarkDirty();
        }
    }
}
