using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using UnityEditor;
using UnityEditor.Compilation;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;

namespace UnityAssistant
{
    [InitializeOnLoad]
    public static class EditorTools
    {
        private struct LogEntry { public string message; public string stack; public LogType type; public DateTime time; }
        private static readonly List<LogEntry> _logs = new List<LogEntry>();
        private const int MaxLogs = 1000;

        static EditorTools()
        {
            Application.logMessageReceivedThreaded -= OnLog;
            Application.logMessageReceivedThreaded += OnLog;
        }

        private static void OnLog(string message, string stack, LogType type)
        {
            lock (_logs)
            {
                _logs.Add(new LogEntry { message = message, stack = stack, type = type, time = DateTime.UtcNow });
                if (_logs.Count > MaxLogs) _logs.RemoveAt(0);
            }
        }

        public static string Compile(Dictionary<string, object> args)
        {
            CompilationPipeline.RequestScriptCompilation();
            AssetDatabase.Refresh();
            return "{\"ok\":true}";
        }

        public static string Play(Dictionary<string, object> args)
        {
            EditorApplication.isPlaying = true;
            return "{\"ok\":true}";
        }

        public static string Stop(Dictionary<string, object> args)
        {
            EditorApplication.isPlaying = false;
            return "{\"ok\":true}";
        }

        public static string GetConsoleLogs(Dictionary<string, object> args)
        {
            int limit = (int)JsonUtil.GetNum(args, "limit", 100);
            string filter = JsonUtil.Get(args, "filter", "");
            var sb = new StringBuilder("{\"ok\":true,\"logs\":[");
            lock (_logs)
            {
                int start = Math.Max(0, _logs.Count - limit);
                bool first = true;
                for (int i = start; i < _logs.Count; i++)
                {
                    var l = _logs[i];
                    if (!string.IsNullOrEmpty(filter) && !string.Equals(filter, l.type.ToString(), StringComparison.OrdinalIgnoreCase))
                        continue;
                    if (!first) sb.Append(",");
                    first = false;
                    sb.Append("{\"type\":").Append(JsonUtil.Quote(l.type.ToString()));
                    sb.Append(",\"message\":").Append(JsonUtil.Quote(l.message));
                    sb.Append(",\"stack\":").Append(JsonUtil.Quote(l.stack));
                    sb.Append(",\"time\":").Append(JsonUtil.Quote(l.time.ToString("o"))).Append("}");
                }
            }
            sb.Append("]}");
            return sb.ToString();
        }

        public static string GetConsoleSummary(Dictionary<string, object> args)
        {
            int errors = 0;
            int warnings = 0;
            int logs = 0;
            LogEntry latestError = default;
            bool hasLatestError = false;

            lock (_logs)
            {
                for (int i = 0; i < _logs.Count; i++)
                {
                    var entry = _logs[i];
                    if (entry.type == LogType.Error || entry.type == LogType.Exception || entry.type == LogType.Assert)
                    {
                        errors++;
                        latestError = entry;
                        hasLatestError = true;
                    }
                    else if (entry.type == LogType.Warning)
                    {
                        warnings++;
                    }
                    else
                    {
                        logs++;
                    }
                }
            }

            var sb = new StringBuilder("{\"ok\":true");
            sb.Append(",\"errors\":").Append(errors);
            sb.Append(",\"warnings\":").Append(warnings);
            sb.Append(",\"logs\":").Append(logs);
            sb.Append(",\"is_compiling\":").Append(EditorApplication.isCompiling ? "true" : "false");
            sb.Append(",\"is_updating\":").Append(EditorApplication.isUpdating ? "true" : "false");
            if (hasLatestError)
            {
                string latestKey = latestError.time.ToString("o") + "|" + latestError.type + "|" + latestError.message;
                sb.Append(",\"latest_error\":").Append(JsonUtil.Quote(latestError.message));
                sb.Append(",\"latest_error_time\":").Append(JsonUtil.Quote(latestError.time.ToString("o")));
                sb.Append(",\"latest_error_key\":").Append(JsonUtil.Quote(latestKey));
            }
            sb.Append("}");
            return sb.ToString();
        }

        public static string ClearConsole(Dictionary<string, object> args)
        {
            lock (_logs) _logs.Clear();
            try
            {
                var t = Type.GetType("UnityEditor.LogEntries,UnityEditor.dll");
                t?.GetMethod("Clear", BindingFlags.Static | BindingFlags.Public)?.Invoke(null, null);
            }
            catch { }
            return "{\"ok\":true}";
        }

        public static string GetActiveScene(Dictionary<string, object> args)
        {
            var s = EditorSceneManager.GetActiveScene();
            return "{\"ok\":true,\"name\":" + JsonUtil.Quote(s.name)
                + ",\"path\":" + JsonUtil.Quote(s.path)
                + ",\"is_dirty\":" + (s.isDirty ? "true" : "false") + "}";
        }

        public static string SaveScene(Dictionary<string, object> args)
        {
            EditorSceneManager.SaveOpenScenes();
            return "{\"ok\":true}";
        }

        public static string OpenScene(Dictionary<string, object> args)
        {
            string path = JsonUtil.Get(args, "path");
            EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo();
            EditorSceneManager.OpenScene(path);
            return "{\"ok\":true}";
        }

        public static string CreateScene(Dictionary<string, object> args)
        {
            string path = JsonUtil.Get(args, "path", "Assets/New Scene.unity");
            if (!path.StartsWith("Assets/")) path = "Assets/" + path;
            if (!path.EndsWith(".unity")) path += ".unity";
            var dir = System.IO.Path.GetDirectoryName(System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), path));
            if (!System.IO.Directory.Exists(dir)) System.IO.Directory.CreateDirectory(dir);
            var scene = EditorSceneManager.NewScene(NewSceneSetup.DefaultGameObjects, NewSceneMode.Single);
            EditorSceneManager.SaveScene(scene, path);
            AssetDatabase.Refresh();
            return "{\"ok\":true,\"path\":" + JsonUtil.Quote(path) + "}";
        }

        public static string CreateTag(Dictionary<string, object> args)
        {
            string tag = JsonUtil.Get(args, "tag").Trim();
            if (string.IsNullOrEmpty(tag)) return "{\"ok\":false,\"error\":\"missing tag\"}";
            var asset = AssetDatabase.LoadAllAssetsAtPath("ProjectSettings/TagManager.asset");
            if (asset == null || asset.Length == 0) return "{\"ok\":false,\"error\":\"TagManager not found\"}";
            var so = new SerializedObject(asset[0]);
            var tags = so.FindProperty("tags");
            for (int i = 0; i < tags.arraySize; i++)
                if (tags.GetArrayElementAtIndex(i).stringValue == tag)
                    return "{\"ok\":true,\"tag\":" + JsonUtil.Quote(tag) + ",\"existed\":true}";
            tags.InsertArrayElementAtIndex(tags.arraySize);
            tags.GetArrayElementAtIndex(tags.arraySize - 1).stringValue = tag;
            so.ApplyModifiedProperties();
            AssetDatabase.SaveAssets();
            return "{\"ok\":true,\"tag\":" + JsonUtil.Quote(tag) + "}";
        }

        public static string ExecuteMenu(Dictionary<string, object> args)
        {
            string item = JsonUtil.Get(args, "item");
            bool ok = EditorApplication.ExecuteMenuItem(item);
            return ok ? "{\"ok\":true}" : "{\"ok\":false,\"error\":\"menu item not found\"}";
        }

        public static string TakeSceneViewImage(Dictionary<string, object> args)
        {
            int maxSize = (int)JsonUtil.GetNum(args, "max_size", 768);
            var sceneView = SceneView.lastActiveSceneView;
            if (sceneView == null || sceneView.camera == null) return "{\"ok\":false,\"error\":\"no active Scene view\"}";
            return CaptureCamera(sceneView.camera, maxSize, "scene_view");
        }

        public static string TakeGameViewImage(Dictionary<string, object> args)
        {
            int maxSize = (int)JsonUtil.GetNum(args, "max_size", 768);
            var cam = Camera.main;
            if (cam == null && Camera.allCamerasCount > 0)
            {
                var cameras = Camera.allCameras;
                cam = cameras.Length > 0 ? cameras[0] : null;
            }
            if (cam == null) return "{\"ok\":false,\"error\":\"no camera in scene\"}";
            // Pass the Game View's configured size so portrait (mobile) games capture at
            // their correct 9:16 aspect rather than defaulting to the monitor's 16:9.
            // GetMainGameViewSize() reads the actual render resolution via reflection and
            // no longer falls back to EditorWindow.GetWindow (which created floating windows).
            return CaptureCamera(cam, maxSize, "game_view", GetMainGameViewSize());
        }

        private static string CaptureRenderedGameView(int maxSize)
        {
            try
            {
                FocusGameView();
                EditorApplication.QueuePlayerLoopUpdate();

                var screenshot = ScreenCapture.CaptureScreenshotAsTexture();
                if (screenshot != null && screenshot.width > 1 && screenshot.height > 1)
                {
                    try
                    {
                        return EncodeTextureResponse(screenshot, screenshot.width, screenshot.height, maxSize, "game_view_screen");
                    }
                    finally
                    {
                        UnityEngine.Object.DestroyImmediate(screenshot);
                    }
                }
            }
            catch { }

            try
            {
                var size = GetMainGameViewSize();
                int sourceWidth = Mathf.Max(1, size.HasValue ? Mathf.RoundToInt(size.Value.x) : Screen.width);
                int sourceHeight = Mathf.Max(1, size.HasValue ? Mathf.RoundToInt(size.Value.y) : Screen.height);
                if (sourceWidth <= 1 || sourceHeight <= 1) return null;

                var rt = RenderTexture.GetTemporary(sourceWidth, sourceHeight, 0, RenderTextureFormat.ARGB32);
                var active = RenderTexture.active;
                try
                {
                    FocusGameView();
                    EditorApplication.QueuePlayerLoopUpdate();
                    ScreenCapture.CaptureScreenshotIntoRenderTexture(rt);
                    return EncodeRenderTextureResponse(rt, sourceWidth, sourceHeight, "game_view_screen");
                }
                finally
                {
                    RenderTexture.active = active;
                    RenderTexture.ReleaseTemporary(rt);
                }
            }
            catch
            {
                return null;
            }
        }

        private static string EncodeTextureResponse(Texture sourceTexture, int sourceWidth, int sourceHeight, int maxSize, string source)
        {
            ScaleToMax(sourceWidth, sourceHeight, Mathf.Clamp(maxSize, 128, 2048), out int width, out int height);
            var rt = RenderTexture.GetTemporary(width, height, 0, RenderTextureFormat.ARGB32);
            var active = RenderTexture.active;
            try
            {
                Graphics.Blit(sourceTexture, rt);
                return EncodeRenderTextureResponse(rt, width, height, source);
            }
            finally
            {
                RenderTexture.active = active;
                RenderTexture.ReleaseTemporary(rt);
            }
        }

        private static string EncodeRenderTextureResponse(RenderTexture rt, int width, int height, string source)
        {
            var active = RenderTexture.active;
            try
            {
                RenderTexture.active = rt;
                var tex = new Texture2D(width, height, TextureFormat.RGB24, false);
                tex.ReadPixels(new Rect(0, 0, width, height), 0, 0);
                tex.Apply();
                var bytes = tex.EncodeToPNG();
                UnityEngine.Object.DestroyImmediate(tex);
                return "{\"ok\":true,\"source\":" + JsonUtil.Quote(source)
                    + ",\"width\":" + width
                    + ",\"height\":" + height
                    + ",\"mime\":\"image/png\",\"base64\":"
                    + JsonUtil.Quote(Convert.ToBase64String(bytes)) + "}";
            }
            finally
            {
                RenderTexture.active = active;
            }
        }

        private static void ScaleToMax(int sourceWidth, int sourceHeight, int maxSize, out int width, out int height)
        {
            if (sourceWidth >= sourceHeight)
            {
                width = maxSize;
                height = Mathf.Clamp(Mathf.RoundToInt(maxSize * (sourceHeight / (float)sourceWidth)), 128, 2048);
            }
            else
            {
                height = maxSize;
                width = Mathf.Clamp(Mathf.RoundToInt(maxSize * (sourceWidth / (float)sourceHeight)), 128, 2048);
            }
        }

        private static void FocusGameView()
        {
            try
            {
                var gameViewType = Type.GetType("UnityEditor.GameView,UnityEditor");
                if (gameViewType == null) return;
                EditorWindow.FocusWindowIfItsOpen(gameViewType);
                var window = EditorWindow.GetWindow(gameViewType);
                window?.Repaint();
            }
            catch { }
        }

        private static string CaptureCamera(Camera camera, int maxSize, string source, Vector2? targetSize = null)
        {
            GetCaptureDimensions(camera, maxSize, targetSize, out int width, out int height);
            var rt = RenderTexture.GetTemporary(width, height, 24, RenderTextureFormat.ARGB32);
            var old = camera.targetTexture;
            var active = RenderTexture.active;
            float oldAspect = camera.aspect;
            Rect oldRect = camera.rect;
            try
            {
                camera.targetTexture = rt;
                // Force the viewport to fill the entire render texture — non-default
                // camera.rect values (split-screen, partial viewport) would otherwise
                // render to only part of the RT, producing a black-bordered image.
                camera.rect = new Rect(0f, 0f, 1f, 1f);
                camera.aspect = width / (float)Mathf.Max(1, height);
                camera.Render();
                RenderTexture.active = rt;
                var tex = new Texture2D(width, height, TextureFormat.RGB24, false);
                tex.ReadPixels(new Rect(0, 0, width, height), 0, 0);
                tex.Apply();
                var bytes = tex.EncodeToPNG();
                UnityEngine.Object.DestroyImmediate(tex);
                return "{\"ok\":true,\"source\":" + JsonUtil.Quote(source)
                    + ",\"width\":" + width
                    + ",\"height\":" + height
                    + ",\"mime\":\"image/png\",\"base64\":"
                    + JsonUtil.Quote(Convert.ToBase64String(bytes)) + "}";
            }
            finally
            {
                camera.targetTexture = old;
                camera.rect = oldRect;
                if (old != null) camera.aspect = oldAspect;
                else camera.ResetAspect();
                RenderTexture.active = active;
                RenderTexture.ReleaseTemporary(rt);
            }
        }

        private static void GetCaptureDimensions(Camera camera, int maxSize, Vector2? targetSize, out int width, out int height)
        {
            int max = Mathf.Clamp(maxSize, 128, 2048);
            float aspect = Mathf.Max(camera.aspect, 0.1f);
            if (targetSize.HasValue && targetSize.Value.x > 1f && targetSize.Value.y > 1f)
                aspect = Mathf.Clamp(targetSize.Value.x / targetSize.Value.y, 0.1f, 10f);

            if (aspect >= 1f)
            {
                width = max;
                height = Mathf.Clamp(Mathf.RoundToInt(max / aspect), 128, 2048);
            }
            else
            {
                height = max;
                width = Mathf.Clamp(Mathf.RoundToInt(max * aspect), 128, 2048);
            }
        }

        private static Vector2? GetMainGameViewSize()
        {
            try
            {
                var handles = typeof(Handles);
                var getMainGameViewSize = handles.GetMethod("GetMainGameViewSize", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
                if (getMainGameViewSize != null)
                {
                    var value = getMainGameViewSize.Invoke(null, null);
                    if (value is Vector2 size && size.x > 1f && size.y > 1f) return size;
                }
            }
            catch { }

            try
            {
                var gameViewType = Type.GetType("UnityEditor.GameView,UnityEditor");
                var getTargetSize = gameViewType?.GetMethod("GetMainGameViewTargetSize", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
                if (getTargetSize != null)
                {
                    var value = getTargetSize.Invoke(null, null);
                    if (value is Vector2 size && size.x > 1f && size.y > 1f) return size;
                }
                // NOTE: EditorWindow.GetWindow(gameViewType) intentionally omitted — it
                // creates a floating Game View window if none exists AND returns window
                // rect including the toolbar, both of which produce incorrect results.
            }
            catch { }

            return null;
        }

        public static string GetProjectInfo(Dictionary<string, object> args)
        {
            var sb = new System.Text.StringBuilder("{\"ok\":true");
            sb.Append(",\"project_path\":").Append(JsonUtil.Quote(System.IO.Directory.GetCurrentDirectory().Replace('\\', '/')));
            sb.Append(",\"assets_path\":").Append(JsonUtil.Quote(Application.dataPath.Replace('\\', '/')));
            sb.Append(",\"product_name\":").Append(JsonUtil.Quote(Application.productName));
            sb.Append(",\"unity_version\":").Append(JsonUtil.Quote(Application.unityVersion));
            sb.Append(",\"is_playing\":").Append(EditorApplication.isPlaying ? "true" : "false");
            sb.Append(",\"is_compiling\":").Append(EditorApplication.isCompiling ? "true" : "false");
            sb.Append(",\"is_updating\":").Append(EditorApplication.isUpdating ? "true" : "false");
            sb.Append("}");
            return sb.ToString();
        }

        public static string GetProjectHealth(Dictionary<string, object> args)
        {
            var issues = new List<object>();
            int compileErrors = 0;
            int warningCount = 0;

            lock (_logs)
            {
                foreach (var entry in _logs)
                {
                    if (entry.type == LogType.Error || entry.type == LogType.Exception || entry.type == LogType.Assert) compileErrors++;
                    else if (entry.type == LogType.Warning) warningCount++;
                }
            }

            if (compileErrors > 0)
            {
                AddHealthIssue(issues, "error", "compile_errors", compileErrors,
                    compileErrors + " console error" + (compileErrors == 1 ? "" : "s") + " need attention",
                    "Read the Unity console errors, identify the root causes, patch the relevant scripts, then compile again.");
            }

            int missingScripts = 0;
            int emptyObjects = 0;
            int sceneObjects = 0;
            for (int i = 0; i < SceneManager.sceneCount; i++)
            {
                var scene = SceneManager.GetSceneAt(i);
                if (!scene.isLoaded) continue;
                foreach (var root in scene.GetRootGameObjects())
                {
                    WalkHealthObject(root.transform, ref missingScripts, ref emptyObjects, ref sceneObjects);
                }
            }

            if (missingScripts > 0)
            {
                AddHealthIssue(issues, "error", "missing_scripts", missingScripts,
                    missingScripts + " missing script reference" + (missingScripts == 1 ? "" : "s") + " in loaded scenes",
                    "Find all missing MonoBehaviour references in the loaded Unity scenes and repair or remove them safely.");
            }

            if (emptyObjects > 8)
            {
                AddHealthIssue(issues, "info", "empty_objects", emptyObjects,
                    emptyObjects + " GameObjects only have a Transform",
                    "Review the empty GameObjects in the loaded scenes and suggest which ones are intentional containers and which can be cleaned up.");
            }

            int scriptCount;
            int todoCount = CountTodoMarkers(out scriptCount);
            if (todoCount > 0)
            {
                AddHealthIssue(issues, "warn", "todos", todoCount,
                    todoCount + " TODO/FIXME marker" + (todoCount == 1 ? "" : "s") + " in scripts",
                    "List the TODO and FIXME markers across project scripts, group them by risk, and recommend the next fixes.");
            }

            if (warningCount > 20)
            {
                AddHealthIssue(issues, "warn", "console_warnings", warningCount,
                    warningCount + " console warning" + (warningCount == 1 ? "" : "s") + " recorded",
                    "Review recent Unity console warnings and fix the ones that can become runtime bugs.");
            }

            int score = 100;
            score -= Math.Min(45, compileErrors * 12);
            score -= Math.Min(30, missingScripts * 10);
            score -= Math.Min(18, todoCount);
            score -= Math.Min(10, Math.Max(0, emptyObjects - 8));
            score -= Math.Min(10, warningCount / 4);
            score = Mathf.Clamp(score, 0, 100);

            var payload = new Dictionary<string, object>
            {
                { "ok", true },
                { "score", score },
                { "issues", issues },
                { "script_count", scriptCount },
                { "scene_object_count", sceneObjects },
                { "is_compiling", EditorApplication.isCompiling },
                { "is_updating", EditorApplication.isUpdating }
            };
            return JsonUtil.ToJson(payload);
        }

        private static void WalkHealthObject(Transform transform, ref int missingScripts, ref int emptyObjects, ref int sceneObjects)
        {
            if (transform == null) return;
            sceneObjects++;
            var go = transform.gameObject;
            if (go != null)
            {
                missingScripts += GameObjectUtility.GetMonoBehavioursWithMissingScriptCount(go);
                var components = go.GetComponents<Component>();
                if (components.Length == 1 && components[0] is Transform) emptyObjects++;
            }

            for (int i = 0; i < transform.childCount; i++)
            {
                WalkHealthObject(transform.GetChild(i), ref missingScripts, ref emptyObjects, ref sceneObjects);
            }
        }

        private static int CountTodoMarkers(out int scriptCount)
        {
            scriptCount = 0;
            int todoCount = 0;
            string[] guids = AssetDatabase.FindAssets("t:MonoScript", new[] { "Assets" });
            foreach (var guid in guids)
            {
                string path = AssetDatabase.GUIDToAssetPath(guid);
                if (string.IsNullOrEmpty(path) || !path.StartsWith("Assets/")) continue;
                string fullPath = System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), path);
                if (!System.IO.File.Exists(fullPath)) continue;
                var info = new System.IO.FileInfo(fullPath);
                if (info.Length > 1_000_000) continue;
                scriptCount++;
                string content = System.IO.File.ReadAllText(fullPath);
                todoCount += CountOccurrences(content, "TODO");
                todoCount += CountOccurrences(content, "FIXME");
            }
            return todoCount;
        }

        private static int CountOccurrences(string source, string pattern)
        {
            if (string.IsNullOrEmpty(source) || string.IsNullOrEmpty(pattern)) return 0;
            int count = 0;
            int index = 0;
            while ((index = source.IndexOf(pattern, index, StringComparison.OrdinalIgnoreCase)) >= 0)
            {
                count++;
                index += pattern.Length;
            }
            return count;
        }

        private static void AddHealthIssue(List<object> issues, string severity, string type, int count, string message, string fixPrompt)
        {
            issues.Add(new Dictionary<string, object>
            {
                { "severity", severity },
                { "type", type },
                { "count", count },
                { "message", message },
                { "fix_prompt", fixPrompt }
            });
        }
    }
}
