using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Runtime.Helpers;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;

namespace MCPForUnity.Editor.Tools
{
    [McpForUnityTool("manage_ui", AutoRegister = false, Group = "ui")]
    public static class ManageUI
    {
        private static readonly HashSet<string> ValidExtensions = new(StringComparer.OrdinalIgnoreCase)
        {
            ".uxml", ".uss"
        };

        // UTF-8 without BOM — UI Builder in Unity 6 can fail to open UXML files with a BOM.
        private static readonly Encoding Utf8NoBom = new UTF8Encoding(false);

        static ManageUI()
        {
            EditorApplication.quitting += CleanupRenderTextures;
            AssemblyReloadEvents.beforeAssemblyReload += CleanupRenderTextures;
        }

        private static void CleanupRenderTextures()
        {
            foreach (var kvp in s_panelRTs)
            {
                if (kvp.Value == null) continue;
                string assetPath = AssetDatabase.GetAssetPath(kvp.Value);
                kvp.Value.Release();
                if (!string.IsNullOrEmpty(assetPath))
                    AssetDatabase.DeleteAsset(assetPath);
                else
                    UnityEngine.Object.DestroyImmediate(kvp.Value);
            }
            s_panelRTs.Clear();
        }

        public static object HandleCommand(JObject @params)
        {
            string action = @params["action"]?.ToString()?.ToLowerInvariant();
            if (string.IsNullOrEmpty(action))
            {
                return new ErrorResponse("Action is required");
            }

            try
            {
                switch (action)
                {
                    case "ping":
                        return new SuccessResponse("pong", new { tool = "manage_ui" });

                    case "create":
                        return CreateFile(@params);

                    case "read":
                        return ReadFile(@params);

                    case "update":
                        return UpdateFile(@params);

                    case "attach_ui_document":
                        return AttachUIDocument(@params);

                    case "create_panel_settings":
                        return CreatePanelSettings(@params);

                    case "update_panel_settings":
                        return UpdatePanelSettings(@params);

                    case "get_visual_tree":
                        return GetVisualTree(@params);

                    case "render_ui":
                        return RenderUI(@params);

                    case "link_stylesheet":
                        return LinkStylesheet(@params);

                    case "delete":
                        return DeleteFile(@params);

                    case "list":
                        return ListUIAssets(@params);

                    case "detach_ui_document":
                        return DetachUIDocument(@params);

                    case "modify_visual_element":
                        return ModifyVisualElement(@params);

                    default:
                        return new ErrorResponse($"Unknown action: {action}");
                }
            }
            catch (Exception ex)
            {
                return new ErrorResponse(ex.Message, new { stackTrace = ex.StackTrace });
            }
        }

        private static string ValidatePath(string path, out string error)
        {
            error = null;
            if (string.IsNullOrEmpty(path))
            {
                error = "'path' parameter is required.";
                return null;
            }

            path = AssetPathUtility.SanitizeAssetPath(path);
            if (path == null)
            {
                error = "Invalid path: contains traversal sequences.";
                return null;
            }

            string ext = Path.GetExtension(path);
            if (!ValidExtensions.Contains(ext))
            {
                error = $"Invalid file extension '{ext}'. Must be .uxml or .uss.";
                return null;
            }

            return path;
        }

        private static object CreateFile(JObject @params)
        {
            var p = new ToolParams(@params);
            string path = ValidatePath(p.Get("path"), out string pathError);
            if (pathError != null) return new ErrorResponse(pathError);

            string contents;
            try
            {
                contents = GetDecodedContents(p);
            }
            catch (ArgumentException ex)
            {
                return new ErrorResponse(ex.Message);
            }

            if (contents == null)
            {
                return new ErrorResponse("'contents' parameter is required for create.");
            }

            string fullPath = Path.Combine(Application.dataPath,
                path.Substring("Assets/".Length));
            fullPath = fullPath.Replace('/', Path.DirectorySeparatorChar);

            if (File.Exists(fullPath))
            {
                return new ErrorResponse($"File already exists at {path}. Use 'update' action to overwrite.");
            }

            string dir = Path.GetDirectoryName(fullPath);
            if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
            {
                Directory.CreateDirectory(dir);
            }

            bool isUxml = path.EndsWith(".uxml", StringComparison.OrdinalIgnoreCase);
            var validationWarnings = new List<string>();

            if (isUxml)
            {
                string xmlError = ValidateUxmlContent(contents, validationWarnings);
                if (xmlError != null)
                {
                    return new ErrorResponse($"UXML validation failed — file was NOT written. {xmlError}");
                }
                contents = EnsureEditorExtensionMode(contents);
            }

            File.WriteAllText(fullPath, contents, Utf8NoBom);
            AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);

            if (isUxml)
            {
                ValidateUxmlPostImport(path, validationWarnings);
            }

            string ext = Path.GetExtension(path).TrimStart('.');
            if (validationWarnings.Count > 0)
            {
                return new SuccessResponse(
                    $"Created {ext} file at {path} with {validationWarnings.Count} warning(s)",
                    new { path, validationWarnings });
            }

            return new SuccessResponse($"Created {ext} file at {path}",
                new { path });
        }

        private static object ReadFile(JObject @params)
        {
            var p = new ToolParams(@params);
            string path = ValidatePath(p.Get("path"), out string pathError);
            if (pathError != null) return new ErrorResponse(pathError);

            string fullPath = Path.Combine(Application.dataPath,
                path.Substring("Assets/".Length));
            fullPath = fullPath.Replace('/', Path.DirectorySeparatorChar);

            if (!File.Exists(fullPath))
            {
                return new ErrorResponse($"File not found: {path}");
            }

            string contents = File.ReadAllText(fullPath, Encoding.UTF8);
            string encoded = Convert.ToBase64String(Encoding.UTF8.GetBytes(contents));

            return new SuccessResponse($"Read {Path.GetExtension(path).TrimStart('.')} file at {path}",
                new
                {
                    path,
                    contents,
                    encodedContents = encoded,
                    contentsEncoded = true,
                    lengthBytes = Encoding.UTF8.GetByteCount(contents)
                });
        }

        private static object UpdateFile(JObject @params)
        {
            var p = new ToolParams(@params);
            string path = ValidatePath(p.Get("path"), out string pathError);
            if (pathError != null) return new ErrorResponse(pathError);

            string contents;
            try
            {
                contents = GetDecodedContents(p);
            }
            catch (ArgumentException ex)
            {
                return new ErrorResponse(ex.Message);
            }

            if (contents == null)
            {
                return new ErrorResponse("'contents' parameter is required for update.");
            }

            string fullPath = Path.Combine(Application.dataPath,
                path.Substring("Assets/".Length));
            fullPath = fullPath.Replace('/', Path.DirectorySeparatorChar);

            if (!File.Exists(fullPath))
            {
                return new ErrorResponse($"File not found: {path}. Use 'create' action for new files.");
            }

            bool isUxml = path.EndsWith(".uxml", StringComparison.OrdinalIgnoreCase);
            var validationWarnings = new List<string>();

            if (isUxml)
            {
                string xmlError = ValidateUxmlContent(contents, validationWarnings);
                if (xmlError != null)
                {
                    return new ErrorResponse($"UXML validation failed — file was NOT updated. {xmlError}");
                }
                contents = EnsureEditorExtensionMode(contents);
            }

            File.WriteAllText(fullPath, contents, Utf8NoBom);
            AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);

            if (isUxml)
            {
                ValidateUxmlPostImport(path, validationWarnings);
            }

            string ext = Path.GetExtension(path).TrimStart('.');
            if (validationWarnings.Count > 0)
            {
                return new SuccessResponse(
                    $"Updated {ext} file at {path} with {validationWarnings.Count} warning(s)",
                    new { path, validationWarnings });
            }

            return new SuccessResponse($"Updated {ext} file at {path}",
                new { path });
        }

        private static object AttachUIDocument(JObject @params)
        {
            var p = new ToolParams(@params);

            var targetResult = p.GetRequired("target");
            var targetError = targetResult.GetOrError(out string target);
            if (targetError != null) return targetError;

            var sourceResult = p.GetRequired("source_asset");
            var sourceError = sourceResult.GetOrError(out string sourceAssetPath);
            if (sourceError != null) return sourceError;

            sourceAssetPath = AssetPathUtility.SanitizeAssetPath(sourceAssetPath);
            if (sourceAssetPath == null)
            {
                return new ErrorResponse("Invalid source_asset path.");
            }

            // Find the GameObject
            var goInstruction = new JObject { ["find"] = target };
            GameObject go = ObjectResolver.Resolve(goInstruction, typeof(GameObject)) as GameObject;
            if (go == null)
            {
                return new ErrorResponse($"Could not find target GameObject: {target}");
            }

            // Load the VisualTreeAsset
            var vta = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(sourceAssetPath);
            if (vta == null)
            {
                return new ErrorResponse($"Could not load VisualTreeAsset at: {sourceAssetPath}");
            }

            // Load or create PanelSettings
            string panelSettingsPath = p.Get("panel_settings") ?? p.Get("panelSettings");
            PanelSettings panelSettings = null;

            if (!string.IsNullOrEmpty(panelSettingsPath))
            {
                panelSettingsPath = AssetPathUtility.SanitizeAssetPath(panelSettingsPath);
                if (panelSettingsPath != null)
                {
                    panelSettings = AssetDatabase.LoadAssetAtPath<PanelSettings>(panelSettingsPath);
                }
                if (panelSettings == null)
                {
                    return new ErrorResponse($"Could not load PanelSettings at: {panelSettingsPath}");
                }
            }
            else
            {
                // Find existing or create default PanelSettings
                string[] guids = AssetDatabase.FindAssets("t:PanelSettings");
                if (guids.Length > 0)
                {
                    string existingPath = AssetDatabase.GUIDToAssetPath(guids[0]);
                    panelSettings = AssetDatabase.LoadAssetAtPath<PanelSettings>(existingPath);
                }

                if (panelSettings == null)
                {
                    panelSettings = CreateDefaultPanelSettings("Assets/UI/DefaultPanelSettings.asset");
                    if (panelSettings == null)
                    {
                        return new ErrorResponse("Failed to create default PanelSettings.");
                    }
                }
            }

            Undo.RecordObject(go, "Attach UIDocument");

            // Add or get UIDocument component
            var uiDoc = go.GetComponent<UIDocument>();
            if (uiDoc == null)
            {
                uiDoc = Undo.AddComponent<UIDocument>(go);
            }

            uiDoc.visualTreeAsset = vta;
            uiDoc.panelSettings = panelSettings;

            int sortOrder = p.GetInt("sort_order") ?? 0;
            uiDoc.sortingOrder = sortOrder;

            EditorUtility.SetDirty(go);

            return new SuccessResponse($"Attached UIDocument to {go.name}",
                new
                {
                    gameObject = go.name,
                    sourceAsset = sourceAssetPath,
                    panelSettings = AssetDatabase.GetAssetPath(panelSettings),
                    sortOrder
                });
        }

        private static object CreatePanelSettings(JObject @params)
        {
            var p = new ToolParams(@params);

            var pathResult = p.GetRequired("path");
            var pathError = pathResult.GetOrError(out string path);
            if (pathError != null) return pathError;

            path = AssetPathUtility.SanitizeAssetPath(path);
            if (path == null)
            {
                return new ErrorResponse("Invalid path: contains traversal sequences.");
            }

            if (!path.EndsWith(".asset", StringComparison.OrdinalIgnoreCase))
            {
                path += ".asset";
            }

            if (AssetDatabase.LoadAssetAtPath<PanelSettings>(path) != null)
            {
                return new ErrorResponse($"PanelSettings already exists at {path}");
            }

            var ps = CreateDefaultPanelSettings(path);
            if (ps == null)
            {
                return new ErrorResponse("Failed to create PanelSettings asset.");
            }

            // Apply any settings passed as a flat dict
            JToken settingsToken = p.GetRaw("settings");
            var changes = new List<string>();
            if (settingsToken is JObject settingsObj)
            {
                ApplyPanelSettingsProperties(ps, settingsObj, changes);
            }
            else
            {
                // Legacy: support top-level scale_mode / reference_resolution
                string scaleMode = p.Get("scale_mode");
                if (!string.IsNullOrEmpty(scaleMode))
                {
                    if (Enum.TryParse<PanelScaleMode>(scaleMode, true, out var mode))
                    {
                        ps.scaleMode = mode;
                        changes.Add("scaleMode");
                    }
                }

                JToken refResToken = p.GetRaw("reference_resolution");
                if (refResToken is JObject refRes)
                {
                    int w = refRes["width"]?.ToObject<int>() ?? 1920;
                    int h = refRes["height"]?.ToObject<int>() ?? 1080;
                    ps.referenceResolution = new Vector2Int(w, h);
                    changes.Add("referenceResolution");
                }
            }

            EditorUtility.SetDirty(ps);
            AssetDatabase.SaveAssets();

            return new SuccessResponse($"Created PanelSettings at {path}",
                new { path, applied = changes });
        }

        private static object UpdatePanelSettings(JObject @params)
        {
            var p = new ToolParams(@params);

            var pathResult = p.GetRequired("path");
            var pathError = pathResult.GetOrError(out string path);
            if (pathError != null) return pathError;

            path = AssetPathUtility.SanitizeAssetPath(path);
            if (path == null)
                return new ErrorResponse("Invalid path: contains traversal sequences.");

            if (!path.EndsWith(".asset", StringComparison.OrdinalIgnoreCase))
                path += ".asset";

            var ps = AssetDatabase.LoadAssetAtPath<PanelSettings>(path);
            if (ps == null)
                return new ErrorResponse($"No PanelSettings found at {path}");

            JToken settingsToken = p.GetRaw("settings");
            if (settingsToken is not JObject settingsObj || settingsObj.Count == 0)
                return new ErrorResponse("'settings' dict is required with at least one property to update.");

            var changes = new List<string>();
            ApplyPanelSettingsProperties(ps, settingsObj, changes);

            if (changes.Count == 0)
                return new ErrorResponse("No recognised properties were applied. Check the key names.");

            EditorUtility.SetDirty(ps);
            AssetDatabase.SaveAssets();

            return new SuccessResponse($"Updated PanelSettings at {path}",
                new { path, applied = changes });
        }

        private static PanelSettings CreateDefaultPanelSettings(string path)
        {
            string dir = Path.GetDirectoryName(path);
            if (!string.IsNullOrEmpty(dir))
            {
                EnsureFolderExists(dir);
            }

            var ps = ScriptableObject.CreateInstance<PanelSettings>();
            AssetDatabase.CreateAsset(ps, path);
            AssetDatabase.SaveAssets();
            return ps;
        }

        /// <summary>
        /// Generic, data-driven applicator for PanelSettings properties.
        /// Accepts a flat JObject where each key maps to a PanelSettings property.
        /// Recognised keys (case-insensitive matching via snake_case/camelCase):
        ///   scaleMode, referenceResolution, screenMatchMode, match,
        ///   referenceDpi, fallbackDpi, sortingOrder, targetDisplay,
        ///   clearColor, colorClearValue, clearDepthStencil,
        ///   themeStyleSheet, dynamicAtlasSettings.
        /// </summary>
        private static void ApplyPanelSettingsProperties(PanelSettings ps, JObject settings, List<string> changes)
        {
            foreach (var prop in settings)
            {
                string key = NormalizeKey(prop.Key);
                JToken val = prop.Value;

                switch (key)
                {
                    // ── Enum properties ─────────────────────────────────────
                    case "scalemode":
                        if (TryParseEnum<PanelScaleMode>(val, out var sm)) { ps.scaleMode = sm; changes.Add("scaleMode"); }
                        break;

                    case "screenmatchmode":
                        if (TryParseEnum<PanelScreenMatchMode>(val, out var smm)) { ps.screenMatchMode = smm; changes.Add("screenMatchMode"); }
                        break;

                    // ── Numeric properties ──────────────────────────────────
                    case "match":
                        if (TryFloat(val, out float matchVal)) { ps.match = Mathf.Clamp01(matchVal); changes.Add("match"); }
                        break;

                    case "referencedpi":
                        if (TryFloat(val, out float refDpi)) { ps.referenceDpi = refDpi; changes.Add("referenceDpi"); }
                        break;

                    case "fallbackdpi":
                        if (TryFloat(val, out float fbDpi)) { ps.fallbackDpi = fbDpi; changes.Add("fallbackDpi"); }
                        break;

                    case "sortingorder":
                        if (TryInt(val, out int so)) { ps.sortingOrder = so; changes.Add("sortingOrder"); }
                        break;

                    case "targetdisplay":
                        if (TryInt(val, out int td)) { ps.targetDisplay = td; changes.Add("targetDisplay"); }
                        break;

                    // ── Bool properties ──────────────────────────────────────
                    case "clearcolor":
                        ps.clearColor = ParamCoercion.CoerceBool(val, false);
                        changes.Add("clearColor");
                        break;

                    case "cleardepthstencil":
                        ps.clearDepthStencil = ParamCoercion.CoerceBool(val, false);
                        changes.Add("clearDepthStencil");
                        break;

                    // ── Composite properties ────────────────────────────────
                    case "referenceresolution":
                        if (val is JObject resObj)
                        {
                            int w = resObj["width"]?.ToObject<int>() ?? ps.referenceResolution.x;
                            int h = resObj["height"]?.ToObject<int>() ?? ps.referenceResolution.y;
                            ps.referenceResolution = new Vector2Int(w, h);
                            changes.Add("referenceResolution");
                        }
                        break;

                    case "colorclearvalue":
                        if (TryParseColor(val, out Color clr)) { ps.colorClearValue = clr; changes.Add("colorClearValue"); }
                        break;

                    case "dynamicatlassettings":
                        if (val is JObject daObj) { ApplyDynamicAtlasSettings(ps, daObj, changes); }
                        break;

                    // ── Asset reference properties ──────────────────────────
                    case "themestylesheet":
                    {
                        string tsPath = val?.ToString();
                        if (!string.IsNullOrEmpty(tsPath))
                        {
                            var ts = AssetDatabase.LoadAssetAtPath<ThemeStyleSheet>(tsPath);
                            if (ts != null) { ps.themeStyleSheet = ts; changes.Add("themeStyleSheet"); }
                        }
                        break;
                    }

                    // unknown keys are silently ignored
                }
            }
        }

        private static void ApplyDynamicAtlasSettings(PanelSettings ps, JObject da, List<string> changes)
        {
            var daCopy = ps.dynamicAtlasSettings;

            if (da["minAtlasSize"] != null && TryInt(da["minAtlasSize"], out int minSize))
                daCopy.minAtlasSize = minSize;
            if (da["maxAtlasSize"] != null && TryInt(da["maxAtlasSize"], out int maxSize))
                daCopy.maxAtlasSize = maxSize;
            if (da["maxSubTextureSize"] != null && TryInt(da["maxSubTextureSize"], out int maxSub))
                daCopy.maxSubTextureSize = maxSub;
            if (da["activeFilters"] != null && TryParseEnum<DynamicAtlasFilters>(da["activeFilters"], out var af))
                daCopy.activeFilters = af;

            ps.dynamicAtlasSettings = daCopy;
            changes.Add("dynamicAtlasSettings");
        }

        // ── Tiny helpers to keep the switch compact ─────────────────────────

        private static string NormalizeKey(string key)
        {
            // Strip underscores and lowercase so "scale_mode", "scaleMode", "ScaleMode"
            // all match the same case label.
            return key.Replace("_", "").ToLowerInvariant();
        }

        private static bool TryParseEnum<T>(JToken token, out T result) where T : struct, Enum
        {
            result = default;
            string s = token?.ToString();
            return !string.IsNullOrEmpty(s) && Enum.TryParse(s, true, out result);
        }

        private static bool TryFloat(JToken token, out float result)
        {
            result = 0f;
            if (token == null) return false;
            if (token.Type == JTokenType.Float || token.Type == JTokenType.Integer)
            {
                result = token.ToObject<float>();
                return true;
            }
            return float.TryParse(token.ToString(), out result);
        }

        private static bool TryInt(JToken token, out int result)
        {
            result = 0;
            if (token == null) return false;
            if (token.Type == JTokenType.Integer)
            {
                result = token.ToObject<int>();
                return true;
            }
            return int.TryParse(token.ToString(), out result);
        }

        private static bool TryParseColor(JToken token, out Color color)
        {
            color = Color.clear;
            if (token == null) return false;

            // Accept "#RRGGBB", "#RRGGBBAA", or {r,g,b,a} object
            if (token.Type == JTokenType.String)
            {
                return ColorUtility.TryParseHtmlString(token.ToString(), out color);
            }

            if (token is JObject cObj)
            {
                color = new Color(
                    cObj["r"]?.ToObject<float>() ?? 0f,
                    cObj["g"]?.ToObject<float>() ?? 0f,
                    cObj["b"]?.ToObject<float>() ?? 0f,
                    cObj["a"]?.ToObject<float>() ?? 1f
                );
                return true;
            }

            return false;
        }

        private static void EnsureFolderExists(string assetFolderPath)
        {
            if (AssetDatabase.IsValidFolder(assetFolderPath))
                return;

            string[] parts = assetFolderPath.Replace('\\', '/').Split('/');
            string current = parts[0]; // "Assets"
            for (int i = 1; i < parts.Length; i++)
            {
                string next = current + "/" + parts[i];
                if (!AssetDatabase.IsValidFolder(next))
                {
                    AssetDatabase.CreateFolder(current, parts[i]);
                }
                current = next;
            }
        }

        private static object GetVisualTree(JObject @params)
        {
            var p = new ToolParams(@params);

            var targetResult = p.GetRequired("target");
            var targetError = targetResult.GetOrError(out string target);
            if (targetError != null) return targetError;

            int maxDepth = p.GetInt("max_depth") ?? 10;

            var goInstruction = new JObject { ["find"] = target };
            GameObject go = ObjectResolver.Resolve(goInstruction, typeof(GameObject)) as GameObject;
            if (go == null)
            {
                return new ErrorResponse($"Could not find target GameObject: {target}");
            }

            var uiDoc = go.GetComponent<UIDocument>();
            if (uiDoc == null)
            {
                return new ErrorResponse($"GameObject {go.name} has no UIDocument component.");
            }

            var root = uiDoc.rootVisualElement;
            if (root == null)
            {
                return new SuccessResponse($"UIDocument on {go.name} has no visual tree (not yet built).",
                    new
                    {
                        gameObject = go.name,
                        sourceAsset = uiDoc.visualTreeAsset != null
                            ? AssetDatabase.GetAssetPath(uiDoc.visualTreeAsset)
                            : null,
                        tree = (object)null
                    });
            }

            var tree = SerializeVisualElement(root, 0, maxDepth);

            return new SuccessResponse($"Visual tree for UIDocument on {go.name}",
                new
                {
                    gameObject = go.name,
                    sourceAsset = uiDoc.visualTreeAsset != null
                        ? AssetDatabase.GetAssetPath(uiDoc.visualTreeAsset)
                        : null,
                    tree
                });
        }

        private static object SerializeVisualElement(VisualElement element, int depth, int maxDepth)
        {
            var result = new Dictionary<string, object>
            {
                ["type"] = element.GetType().Name,
                ["name"] = element.name ?? "",
                ["classes"] = new List<string>(element.GetClasses()),
            };

            // Include basic computed style info
            var style = new Dictionary<string, object>();
            var resolved = element.resolvedStyle;

            if (resolved.width > 0) style["width"] = resolved.width;
            if (resolved.height > 0) style["height"] = resolved.height;
            if (resolved.color != Color.clear)
                style["color"] = ColorToHex(resolved.color);
            if (resolved.backgroundColor != Color.clear)
                style["backgroundColor"] = ColorToHex(resolved.backgroundColor);
            if (resolved.fontSize > 0) style["fontSize"] = resolved.fontSize;

            if (style.Count > 0)
                result["resolvedStyle"] = style;

            // Include text content for labels/buttons
            if (element is TextElement textEl && !string.IsNullOrEmpty(textEl.text))
            {
                result["text"] = textEl.text;
            }

            // Serialize children
            if (depth < maxDepth && element.childCount > 0)
            {
                var children = new List<object>();
                foreach (var child in element.Children())
                {
                    children.Add(SerializeVisualElement(child, depth + 1, maxDepth));
                }
                result["children"] = children;
            }
            else if (element.childCount > 0)
            {
                result["childCount"] = element.childCount;
                result["truncated"] = true;
            }

            return result;
        }

        // ---- Render UI ----

        // Persistent RenderTextures keyed by PanelSettings instance ID so the panel
        // renders into them automatically every frame.
        private static readonly Dictionary<int, RenderTexture> s_panelRTs = new();

        // Play-mode coroutine capture state.  Only one capture is in-flight at a
        // time; concurrent render_ui calls while a capture is pending are rejected
        // with an explicit error.
        private static Texture2D s_pendingCaptureTex;
        private static bool s_pendingCaptureDone;
        private static bool s_pendingCaptureStarted;

        private static object RenderUI(JObject @params)
        {
            var p = new ToolParams(@params);

            string target = p.Get("target");
            string uxmlPath = p.Get("path");
            int width = p.GetInt("width") ?? 1920;
            int height = p.GetInt("height") ?? 1080;
            bool includeImage = p.GetBool("include_image") || p.GetBool("includeImage");
            int maxResolution = p.GetInt("max_resolution") ?? p.GetInt("maxResolution") ?? 640;
            string fileName = p.Get("file_name") ?? p.Get("fileName");
            string outputFolderOverride = p.Get("output_folder") ?? p.Get("outputFolder");

            if (string.IsNullOrEmpty(target) && string.IsNullOrEmpty(uxmlPath))
            {
                return new ErrorResponse("Either 'target' (GameObject with UIDocument) or 'path' (UXML asset path) is required.");
            }

            string resolvedFolderSpec = ScreenshotPreferences.Resolve(outputFolderOverride);
            string resolvedFolderAbs;
            try
            {
                resolvedFolderAbs = ScreenshotUtility.ResolveFolderAbsolute(resolvedFolderSpec);
            }
            catch (InvalidOperationException ex)
            {
                return new ErrorResponse(ex.Message);
            }

            // ── Play-mode capture via ScreenCapture coroutine ──────────────────────
            // PanelSettings.targetTexture is read in the same frame it is assigned,
            // so the RT is always blank in a synchronous tool call.  In play mode we
            // dispatch a WaitForEndOfFrame coroutine that uses ScreenCapture, which
            // captures the fully-composited game view (including UI Toolkit overlays).
            // First call: queues the capture and returns "pending".
            // Second call: result is ready – save PNG and return data.
            if (Application.isPlaying)
            {
                // Build the output paths (used by both the pending and ready branches)
                string resolvedPlayName = string.IsNullOrWhiteSpace(fileName)
                    ? $"ui-render-{DateTime.Now:yyyyMMdd-HHmmss}.png"
                    : fileName.Trim();
                if (!resolvedPlayName.EndsWith(".png", StringComparison.OrdinalIgnoreCase))
                    resolvedPlayName += ".png";

                Directory.CreateDirectory(resolvedFolderAbs);
                string playFullPath = Path.Combine(resolvedFolderAbs, resolvedPlayName).Replace('\\', '/');
                playFullPath = EnsureUniqueFilePath(playFullPath);
                string playProjectRelPath = ScreenshotUtility.ToProjectRelativePath(playFullPath);

                // ── Case 1: capture is ready ──────────────────────────────────────
                if (s_pendingCaptureDone && s_pendingCaptureTex != null)
                {
                    var captureTex = s_pendingCaptureTex;
                    s_pendingCaptureDone = false;
                    s_pendingCaptureTex = null;

                    int captureW = captureTex.width;
                    int captureH = captureTex.height;
                    byte[] capturePng = captureTex.EncodeToPNG();
                    UnityEngine.Object.DestroyImmediate(captureTex);

                    File.WriteAllBytes(playFullPath, capturePng);
                    if (ScreenshotUtility.IsUnderAssets(playProjectRelPath))
                        AssetDatabase.ImportAsset(playProjectRelPath, ImportAssetOptions.ForceSynchronousImport);

                    var playData = new Dictionary<string, object>
                    {
                        { "path", playProjectRelPath },
                        { "fullPath", playFullPath },
                        { "width", captureW },
                        { "height", captureH },
                        { "hasContent", true },
                    };

                    if (!string.IsNullOrEmpty(target)) playData["gameObject"] = target;
                    if (!string.IsNullOrEmpty(uxmlPath)) playData["sourceAsset"] = uxmlPath;

                    if (includeImage)
                    {
                        int targetMax = maxResolution > 0 ? maxResolution : 640;
                        Texture2D downscaled = null;
                        try
                        {
                            var fullTex = new Texture2D(captureW, captureH, TextureFormat.RGBA32, false);
                            fullTex.LoadImage(capturePng);
                            if (captureW > targetMax || captureH > targetMax)
                            {
                                downscaled = ScreenshotUtility.DownscaleTexture(fullTex, targetMax);
                                playData["imageBase64"] = Convert.ToBase64String(downscaled.EncodeToPNG());
                                playData["imageWidth"] = downscaled.width;
                                playData["imageHeight"] = downscaled.height;
                            }
                            else
                            {
                                playData["imageBase64"] = Convert.ToBase64String(capturePng);
                                playData["imageWidth"] = captureW;
                                playData["imageHeight"] = captureH;
                            }
                            UnityEngine.Object.DestroyImmediate(fullTex);
                        }
                        finally
                        {
                            if (downscaled != null) UnityEngine.Object.DestroyImmediate(downscaled);
                        }
                    }

                    return new SuccessResponse($"UI render saved to '{playProjectRelPath}'.", playData);
                }

                // ── Case 2: start a new capture ───────────────────────────────────
                // Only one capture in flight at a time.  If one is already pending,
                // reject rather than silently overwriting the state.
                if (s_pendingCaptureStarted)
                {
                    return new ErrorResponse(
                        "Cannot capture: another capture is already in progress.",
                        new { retry_after_ms = 100, reason = "capture_in_progress" });
                }

                s_pendingCaptureDone = false;
                s_pendingCaptureTex = null;
                s_pendingCaptureStarted = true;
                ScreenshotCapturer.Begin(1, tex =>
                {
                    s_pendingCaptureTex = tex;
                    s_pendingCaptureDone = true;
                    s_pendingCaptureStarted = false;
                });

                return new SuccessResponse(
                    "Play-mode screenshot capture queued (WaitForEndOfFrame). Call render_ui again to retrieve the rendered image.",
                    new Dictionary<string, object>
                    {
                        { "pending", true },
                        { "gameObject", (object)target ?? uxmlPath },
                        { "note", "A screen capture was scheduled for the end of this frame. Call render_ui once more to get the result." }
                    });
            }
            // ── End play-mode branch ────────────────────────────────────────────────

            // Resolve UIDocument
            UIDocument uiDoc = null;
            GameObject tempGo = null;
            PanelSettings tempPs = null;

            try
            {
                if (!string.IsNullOrEmpty(target))
                {
                    var goInstruction = new JObject { ["find"] = target };
                    GameObject go = ObjectResolver.Resolve(goInstruction, typeof(GameObject)) as GameObject;
                    if (go == null)
                        return new ErrorResponse($"Could not find target GameObject: {target}");

                    uiDoc = go.GetComponent<UIDocument>();
                    if (uiDoc == null)
                        return new ErrorResponse($"GameObject '{go.name}' has no UIDocument component.");
                }
                else
                {
                    uxmlPath = AssetPathUtility.SanitizeAssetPath(uxmlPath);
                    if (uxmlPath == null)
                        return new ErrorResponse("Invalid UXML path.");

                    var vta = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(uxmlPath);
                    if (vta == null)
                        return new ErrorResponse($"Could not load VisualTreeAsset at: {uxmlPath}");

                    tempGo = new GameObject("__MCP_UI_Render_Temp__");
                    tempGo.hideFlags = HideFlags.HideAndDontSave;
                    uiDoc = tempGo.AddComponent<UIDocument>();

                    string[] guids = AssetDatabase.FindAssets("t:PanelSettings");
                    PanelSettings ps = null;
                    if (guids.Length > 0)
                        ps = AssetDatabase.LoadAssetAtPath<PanelSettings>(AssetDatabase.GUIDToAssetPath(guids[0]));
                    if (ps == null)
                    {
                        ps = CreateDefaultPanelSettings("Assets/UI/DefaultPanelSettings.asset");
                        tempPs = ps;
                    }

                    uiDoc.panelSettings = ps;
                    uiDoc.visualTreeAsset = vta;
                }

                if (uiDoc.panelSettings == null)
                    return new ErrorResponse("UIDocument has no PanelSettings assigned.");

                var panelSettings = uiDoc.panelSettings;
                int psId = panelSettings.GetInstanceIDCompat();

                // Check if we already have a persistent RT assigned to this PanelSettings.
                // If the RT exists and its size matches, the panel has been rendering into it.
                // If not, create one and assign it — content will be available on the next call.
                // Look up from our cache rather than panelSettings.targetTexture,
                // because we set targetTexture = null after each successful read
                // to restore on-screen rendering.  The RT itself stays alive in s_panelRTs.
                bool rtJustAssigned = false;
                RenderTexture rt = null;

                if (s_panelRTs.TryGetValue(psId, out var cachedRt) && cachedRt != null)
                {
                    if (cachedRt.width == width && cachedRt.height == height)
                    {
                        rt = cachedRt;
                        // Re-attach if it was detached after the previous read
                        if (panelSettings.targetTexture != rt)
                        {
                            panelSettings.targetTexture = rt;
                            rtJustAssigned = true;

                            uiDoc.rootVisualElement?.MarkDirtyRepaint();
                            EditorUtility.SetDirty(panelSettings);
                            UnityEditorInternal.InternalEditorUtility.RepaintAllViews();
                            Canvas.ForceUpdateCanvases();
                        }
                    }
                    else
                    {
                        // Size changed — release the old RT
                        panelSettings.targetTexture = null;
                        string oldPath = AssetDatabase.GetAssetPath(cachedRt);
                        cachedRt.Release();
                        if (!string.IsNullOrEmpty(oldPath))
                            AssetDatabase.DeleteAsset(oldPath);
                        else
                            UnityEngine.Object.DestroyImmediate(cachedRt);
                        s_panelRTs.Remove(psId);
                    }
                }

                if (rt == null)
                {
                    // Create RT as an asset so PanelSettings can serialize the reference properly
                    rt = new RenderTexture(width, height, 32, RenderTextureFormat.ARGB32);
                    rt.name = $"MCP_UI_Render_{psId}";
                    rt.Create();

                    string rtFolder = "Assets/UI";
                    if (!AssetDatabase.IsValidFolder(rtFolder))
                        AssetDatabase.CreateFolder("Assets", "UI");
                    string rtAssetPath = $"{rtFolder}/RT_MCP_UI_Render_{psId}.renderTexture";
                    AssetDatabase.CreateAsset(rt, rtAssetPath);
                    AssetDatabase.SaveAssets();

                    panelSettings.targetTexture = rt;
                    s_panelRTs[psId] = rt;
                    rtJustAssigned = true;

                    // Mark dirty and force editor repaint so the panel renders into the RT
                    uiDoc.rootVisualElement?.MarkDirtyRepaint();
                    EditorUtility.SetDirty(panelSettings);
                    UnityEditorInternal.InternalEditorUtility.RepaintAllViews();

                    // Force a synchronous layout + repaint pass
                    Canvas.ForceUpdateCanvases();
                }

                // Read pixels from the RT
                RenderTexture prevActive = RenderTexture.active;
                RenderTexture.active = rt;
                var tex = new Texture2D(width, height, TextureFormat.RGBA32, false);
                tex.ReadPixels(new Rect(0, 0, width, height), 0, 0);
                tex.Apply();
                RenderTexture.active = prevActive;

                // Restore targetTexture to null so the UI renders back to the
                // actual display / camera.  The RT stays cached in s_panelRTs
                // and will be re-attached on the next render_ui call.
                if (!rtJustAssigned)
                {
                    panelSettings.targetTexture = null;
                    EditorUtility.SetDirty(panelSettings);
                }

                // Check if any content was rendered
                bool hasContent = false;
                var pixels = tex.GetPixels32();
                for (int i = 0; i < pixels.Length; i += Mathf.Max(1, pixels.Length / 100))
                {
                    if (pixels[i].a > 0) { hasContent = true; break; }
                }

                // Save to Screenshots folder
                string resolvedName = string.IsNullOrWhiteSpace(fileName)
                    ? $"ui-render-{DateTime.Now:yyyyMMdd-HHmmss}.png"
                    : fileName.Trim();
                if (!resolvedName.EndsWith(".png", StringComparison.OrdinalIgnoreCase))
                    resolvedName += ".png";

                Directory.CreateDirectory(resolvedFolderAbs);
                string fullPath = Path.Combine(resolvedFolderAbs, resolvedName).Replace('\\', '/');
                fullPath = EnsureUniqueFilePath(fullPath);

                byte[] png = tex.EncodeToPNG();
                File.WriteAllBytes(fullPath, png);

                string projectRelPath = ScreenshotUtility.ToProjectRelativePath(fullPath);
                if (ScreenshotUtility.IsUnderAssets(projectRelPath))
                    AssetDatabase.ImportAsset(projectRelPath, ImportAssetOptions.ForceSynchronousImport);

                var data = new Dictionary<string, object>
                {
                    { "path", projectRelPath },
                    { "fullPath", fullPath },
                    { "width", width },
                    { "height", height },
                    { "hasContent", hasContent },
                };

                if (rtJustAssigned)
                    data["note"] = "RenderTexture was just assigned to PanelSettings. Call render_ui again to capture the rendered UI.";

                if (!string.IsNullOrEmpty(target))
                    data["gameObject"] = target;
                if (!string.IsNullOrEmpty(uxmlPath))
                    data["sourceAsset"] = uxmlPath;

                if (includeImage)
                {
                    int targetMax = maxResolution > 0 ? maxResolution : 640;
                    Texture2D downscaled = null;
                    try
                    {
                        if (width > targetMax || height > targetMax)
                        {
                            downscaled = ScreenshotUtility.DownscaleTexture(tex, targetMax);
                            data["imageBase64"] = Convert.ToBase64String(downscaled.EncodeToPNG());
                            data["imageWidth"] = downscaled.width;
                            data["imageHeight"] = downscaled.height;
                        }
                        else
                        {
                            data["imageBase64"] = Convert.ToBase64String(png);
                            data["imageWidth"] = width;
                            data["imageHeight"] = height;
                        }
                    }
                    finally
                    {
                        if (downscaled != null) UnityEngine.Object.DestroyImmediate(downscaled);
                    }
                }

                UnityEngine.Object.DestroyImmediate(tex);

                string msg = hasContent
                    ? $"UI rendered to '{projectRelPath}'."
                    : rtJustAssigned
                        ? $"RenderTexture assigned to PanelSettings. Call render_ui again to capture the rendered content."
                        : $"UI render saved to '{projectRelPath}' (no visible content detected).";

                return new SuccessResponse(msg, data);
            }
            finally
            {
                if (tempGo != null) UnityEngine.Object.DestroyImmediate(tempGo);
                if (tempPs != null)
                {
                    string tempPsPath = AssetDatabase.GetAssetPath(tempPs);
                    if (!string.IsNullOrEmpty(tempPsPath))
                        AssetDatabase.DeleteAsset(tempPsPath);
                    else
                        UnityEngine.Object.DestroyImmediate(tempPs, true);
                }
            }
        }

        // ---- Link Stylesheet ----

        private static object LinkStylesheet(JObject @params)
        {
            var p = new ToolParams(@params);

            string uxmlPathRaw = p.Get("path");
            string uxmlPath = ValidatePath(uxmlPathRaw, out string pathError);
            if (pathError != null) return new ErrorResponse(pathError);

            // Validate the UXML path is actually a .uxml
            if (!uxmlPath.EndsWith(".uxml", StringComparison.OrdinalIgnoreCase))
                return new ErrorResponse("'path' must point to a .uxml file.");

            string stylesheetPath = p.Get("stylesheet");
            if (string.IsNullOrEmpty(stylesheetPath))
                return new ErrorResponse("'stylesheet' parameter is required.");

            stylesheetPath = AssetPathUtility.SanitizeAssetPath(stylesheetPath);
            if (stylesheetPath == null)
                return new ErrorResponse("Invalid stylesheet path: contains traversal sequences.");

            if (!stylesheetPath.EndsWith(".uss", StringComparison.OrdinalIgnoreCase))
                return new ErrorResponse("'stylesheet' must point to a .uss file.");

            // Read the UXML file
            string fullPath = Path.Combine(Application.dataPath,
                uxmlPath.Substring("Assets/".Length)).Replace('/', Path.DirectorySeparatorChar);

            if (!File.Exists(fullPath))
                return new ErrorResponse($"UXML file not found: {uxmlPath}");

            string content = File.ReadAllText(fullPath, Encoding.UTF8);

            // Check if stylesheet is already linked
            if (content.Contains($"src=\"{stylesheetPath}\"") ||
                content.Contains($"src=\"project://database/{stylesheetPath}\""))
            {
                return new SuccessResponse($"Stylesheet already linked in '{uxmlPath}'.",
                    new { path = uxmlPath, stylesheet = stylesheetPath, alreadyLinked = true });
            }

            // Find the insertion point (after the opening <ui:UXML ...> or <UXML ...> tag)
            int insertIdx = FindUxmlBodyStart(content);
            if (insertIdx < 0)
                return new ErrorResponse("Could not find insertion point. Ensure UXML has a root <ui:UXML> or <UXML> element.");

            string styleTag = $"\n    <ui:Style src=\"project://database/{stylesheetPath}\" />";
            content = content.Insert(insertIdx, styleTag);

            File.WriteAllText(fullPath, content, Utf8NoBom);
            AssetDatabase.ImportAsset(uxmlPath, ImportAssetOptions.ForceUpdate);

            return new SuccessResponse($"Linked stylesheet '{stylesheetPath}' to '{uxmlPath}'.",
                new { path = uxmlPath, stylesheet = stylesheetPath });
        }

        // ---- Delete ----

        private static object DeleteFile(JObject @params)
        {
            var p = new ToolParams(@params);
            string path = ValidatePath(p.Get("path"), out string pathError);
            if (pathError != null) return new ErrorResponse(pathError);

            string fullPath = Path.Combine(Application.dataPath,
                path.Substring("Assets/".Length));
            fullPath = fullPath.Replace('/', Path.DirectorySeparatorChar);

            if (!File.Exists(fullPath))
            {
                return new ErrorResponse($"File not found: {path}");
            }

            try
            {
                bool success = AssetDatabase.DeleteAsset(path);
                if (!success)
                {
                    return new ErrorResponse($"Failed to delete file through AssetDatabase: '{path}'");
                }

                // Fallback: if file still exists after AssetDatabase.DeleteAsset
                if (File.Exists(fullPath))
                {
                    File.Delete(fullPath);
                }

                return new SuccessResponse($"Deleted {Path.GetExtension(path).TrimStart('.')} file at {path}",
                    new { path });
            }
            catch (Exception e)
            {
                return new ErrorResponse($"Failed to delete '{path}': {e.Message}");
            }
        }

        // ---- List UI Assets ----

        private static object ListUIAssets(JObject @params)
        {
            var p = new ToolParams(@params);
            string scope = p.Get("path") ?? "Assets";
            string filterType = p.Get("filter_type") ?? p.Get("filterType");
            int pageSize = p.GetInt("page_size") ?? p.GetInt("pageSize") ?? 50;
            int pageNumber = p.GetInt("page_number") ?? p.GetInt("pageNumber") ?? 1;

            scope = AssetPathUtility.SanitizeAssetPath(scope);
            if (scope == null)
            {
                return new ErrorResponse("Invalid path: contains traversal sequences.");
            }

            string[] folderScope = AssetDatabase.IsValidFolder(scope)
                ? new[] { scope }
                : null;

            // Find UXML and USS assets based on filter
            var allAssets = new List<object>();

            bool includeUxml = string.IsNullOrEmpty(filterType) ||
                               filterType.Equals("uxml", StringComparison.OrdinalIgnoreCase) ||
                               filterType.Equals("VisualTreeAsset", StringComparison.OrdinalIgnoreCase);
            bool includeUss = string.IsNullOrEmpty(filterType) ||
                              filterType.Equals("uss", StringComparison.OrdinalIgnoreCase) ||
                              filterType.Equals("StyleSheet", StringComparison.OrdinalIgnoreCase);
            bool includePanelSettings = string.IsNullOrEmpty(filterType) ||
                                        filterType.Equals("PanelSettings", StringComparison.OrdinalIgnoreCase);

            if (includeUxml)
            {
                string[] guids = AssetDatabase.FindAssets("t:VisualTreeAsset", folderScope);
                foreach (string guid in guids)
                {
                    string assetPath = AssetDatabase.GUIDToAssetPath(guid);
                    if (!string.IsNullOrEmpty(assetPath))
                    {
                        allAssets.Add(new Dictionary<string, object>
                        {
                            ["path"] = assetPath,
                            ["type"] = "uxml",
                            ["name"] = Path.GetFileName(assetPath),
                        });
                    }
                }
            }

            if (includeUss)
            {
                string[] guids = AssetDatabase.FindAssets("t:StyleSheet", folderScope);
                foreach (string guid in guids)
                {
                    string assetPath = AssetDatabase.GUIDToAssetPath(guid);
                    if (!string.IsNullOrEmpty(assetPath))
                    {
                        allAssets.Add(new Dictionary<string, object>
                        {
                            ["path"] = assetPath,
                            ["type"] = "uss",
                            ["name"] = Path.GetFileName(assetPath),
                        });
                    }
                }
            }

            if (includePanelSettings)
            {
                string[] guids = AssetDatabase.FindAssets("t:PanelSettings", folderScope);
                foreach (string guid in guids)
                {
                    string assetPath = AssetDatabase.GUIDToAssetPath(guid);
                    if (!string.IsNullOrEmpty(assetPath))
                    {
                        allAssets.Add(new Dictionary<string, object>
                        {
                            ["path"] = assetPath,
                            ["type"] = "PanelSettings",
                            ["name"] = Path.GetFileName(assetPath),
                        });
                    }
                }
            }

            int total = allAssets.Count;
            int startIndex = (pageNumber - 1) * pageSize;
            var paged = allAssets.Skip(startIndex).Take(pageSize).ToList();

            return new SuccessResponse(
                $"Found {total} UI asset(s). Returning page {pageNumber} ({paged.Count} items).",
                new
                {
                    total,
                    pageSize,
                    pageNumber,
                    assets = paged,
                });
        }

        // ---- Detach UIDocument ----

        private static object DetachUIDocument(JObject @params)
        {
            var p = new ToolParams(@params);

            var targetResult = p.GetRequired("target");
            var targetError = targetResult.GetOrError(out string target);
            if (targetError != null) return targetError;

            var goInstruction = new JObject { ["find"] = target };
            GameObject go = ObjectResolver.Resolve(goInstruction, typeof(GameObject)) as GameObject;
            if (go == null)
            {
                return new ErrorResponse($"Could not find target GameObject: {target}");
            }

            var uiDoc = go.GetComponent<UIDocument>();
            if (uiDoc == null)
            {
                return new ErrorResponse($"GameObject '{go.name}' has no UIDocument component.");
            }

            string sourceAsset = uiDoc.visualTreeAsset != null
                ? AssetDatabase.GetAssetPath(uiDoc.visualTreeAsset)
                : null;

            Undo.DestroyObjectImmediate(uiDoc);
            EditorUtility.SetDirty(go);

            return new SuccessResponse($"Removed UIDocument from {go.name}",
                new
                {
                    gameObject = go.name,
                    removedSourceAsset = sourceAsset,
                });
        }

        // ---- Modify Visual Element ----

        private static object ModifyVisualElement(JObject @params)
        {
            var p = new ToolParams(@params);

            var targetResult = p.GetRequired("target");
            var targetError = targetResult.GetOrError(out string target);
            if (targetError != null) return targetError;

            string elementName = p.Get("element_name") ?? p.Get("elementName");
            if (string.IsNullOrEmpty(elementName))
            {
                return new ErrorResponse("'element_name' parameter is required.");
            }

            var goInstruction = new JObject { ["find"] = target };
            GameObject go = ObjectResolver.Resolve(goInstruction, typeof(GameObject)) as GameObject;
            if (go == null)
            {
                return new ErrorResponse($"Could not find target GameObject: {target}");
            }

            var uiDoc = go.GetComponent<UIDocument>();
            if (uiDoc == null)
            {
                return new ErrorResponse($"GameObject '{go.name}' has no UIDocument component.");
            }

            var root = uiDoc.rootVisualElement;
            if (root == null)
            {
                return new ErrorResponse($"UIDocument on {go.name} has no visual tree (not yet built).");
            }

            // Find the target element by name
            var element = root.Q(elementName);
            if (element == null)
            {
                return new ErrorResponse($"Visual element with name '{elementName}' not found in the visual tree.");
            }

            var modifications = new List<string>();

            // Set text content (Label, Button, etc.)
            string text = p.Get("text");
            if (text != null && element is TextElement textEl)
            {
                textEl.text = text;
                modifications.Add($"text='{text}'");
            }
            else if (text != null)
            {
                return new ErrorResponse($"Element '{elementName}' ({element.GetType().Name}) does not support text content.");
            }

            // Add CSS classes
            JToken addClassesToken = p.GetRaw("add_classes") ?? p.GetRaw("addClasses");
            if (addClassesToken is JArray addArr)
            {
                foreach (var cls in addArr)
                {
                    string className = cls.ToString();
                    if (!element.ClassListContains(className))
                    {
                        element.AddToClassList(className);
                        modifications.Add($"+class '{className}'");
                    }
                }
            }

            // Remove CSS classes
            JToken removeClassesToken = p.GetRaw("remove_classes") ?? p.GetRaw("removeClasses");
            if (removeClassesToken is JArray removeArr)
            {
                foreach (var cls in removeArr)
                {
                    string className = cls.ToString();
                    if (element.ClassListContains(className))
                    {
                        element.RemoveFromClassList(className);
                        modifications.Add($"-class '{className}'");
                    }
                }
            }

            // Toggle CSS classes
            JToken toggleClassesToken = p.GetRaw("toggle_classes") ?? p.GetRaw("toggleClasses");
            if (toggleClassesToken is JArray toggleArr)
            {
                foreach (var cls in toggleArr)
                {
                    string className = cls.ToString();
                    element.ToggleInClassList(className);
                    modifications.Add($"~class '{className}'");
                }
            }

            // Set inline styles
            JToken styleToken = p.GetRaw("style") ?? p.GetRaw("inline_style") ?? p.GetRaw("inlineStyle");
            if (styleToken is JObject styleObj)
            {
                ApplyInlineStyles(element, styleObj, modifications);
            }

            // Set enabled/disabled
            bool? enabled = p.GetNullableBool("enabled");
            if (enabled.HasValue)
            {
                element.SetEnabled(enabled.Value);
                modifications.Add($"enabled={enabled.Value}");
            }

            // Set visibility
            string visibility = p.Get("visible");
            if (visibility != null)
            {
                bool isVisible = visibility.Equals("true", StringComparison.OrdinalIgnoreCase) ||
                                 visibility == "1";
                element.style.display = isVisible ? DisplayStyle.Flex : DisplayStyle.None;
                modifications.Add($"visible={isVisible}");
            }

            // Set tooltip
            string tooltip = p.Get("tooltip");
            if (tooltip != null)
            {
                element.tooltip = tooltip;
                modifications.Add($"tooltip='{tooltip}'");
            }

            // Filter out [skipped] entries so they don't count as real modifications
            var applied = modifications.Where(m => !m.StartsWith("[skipped]")).ToList();
            var skipped = modifications.Where(m => m.StartsWith("[skipped]")).ToList();

            if (applied.Count == 0)
            {
                string msg = skipped.Count > 0
                    ? $"No modifications applied. Skipped unsupported styles: {string.Join(", ", skipped)}"
                    : "No modifications specified. Provide at least one of: text, add_classes, remove_classes, toggle_classes, style, enabled, visible, tooltip.";
                return new ErrorResponse(msg);
            }

            var responseData = new Dictionary<string, object>
            {
                { "gameObject", go.name },
                { "elementName", elementName },
                { "elementType", element.GetType().Name },
                { "modifications", applied },
                { "currentClasses", new List<string>(element.GetClasses()) },
            };
            if (skipped.Count > 0)
                responseData["skipped"] = skipped;

            return new SuccessResponse(
                $"Modified element '{elementName}' on {go.name}: {string.Join(", ", applied)}",
                responseData);
        }

        private static void ApplyInlineStyles(VisualElement element, JObject styleObj, List<string> modifications)
        {
            foreach (var prop in styleObj)
            {
                string key = prop.Key;
                JToken val = prop.Value;

                switch (key.ToLowerInvariant())
                {
                    case "backgroundcolor":
                    case "background-color":
                        if (ColorUtility.TryParseHtmlString(val.ToString(), out Color bgColor))
                        {
                            element.style.backgroundColor = bgColor;
                            modifications.Add($"backgroundColor={val}");
                        }
                        break;

                    case "color":
                        if (ColorUtility.TryParseHtmlString(val.ToString(), out Color fgColor))
                        {
                            element.style.color = fgColor;
                            modifications.Add($"color={val}");
                        }
                        break;

                    case "fontsize":
                    case "font-size":
                        element.style.fontSize = val.ToObject<float>();
                        modifications.Add($"fontSize={val}");
                        break;

                    case "width":
                        element.style.width = val.ToObject<float>();
                        modifications.Add($"width={val}");
                        break;

                    case "height":
                        element.style.height = val.ToObject<float>();
                        modifications.Add($"height={val}");
                        break;

                    case "opacity":
                        element.style.opacity = val.ToObject<float>();
                        modifications.Add($"opacity={val}");
                        break;

                    case "display":
                        if (Enum.TryParse<DisplayStyle>(val.ToString(), true, out var display))
                        {
                            element.style.display = display;
                            modifications.Add($"display={val}");
                        }
                        break;

                    case "visibility":
                        if (Enum.TryParse<Visibility>(val.ToString(), true, out var vis))
                        {
                            element.style.visibility = vis;
                            modifications.Add($"visibility={val}");
                        }
                        break;

                    case "flexgrow":
                    case "flex-grow":
                        element.style.flexGrow = val.ToObject<float>();
                        modifications.Add($"flexGrow={val}");
                        break;

                    case "flexshrink":
                    case "flex-shrink":
                        element.style.flexShrink = val.ToObject<float>();
                        modifications.Add($"flexShrink={val}");
                        break;

                    case "marginleft":
                    case "margin-left":
                        element.style.marginLeft = val.ToObject<float>();
                        modifications.Add($"marginLeft={val}");
                        break;

                    case "marginright":
                    case "margin-right":
                        element.style.marginRight = val.ToObject<float>();
                        modifications.Add($"marginRight={val}");
                        break;

                    case "margintop":
                    case "margin-top":
                        element.style.marginTop = val.ToObject<float>();
                        modifications.Add($"marginTop={val}");
                        break;

                    case "marginbottom":
                    case "margin-bottom":
                        element.style.marginBottom = val.ToObject<float>();
                        modifications.Add($"marginBottom={val}");
                        break;

                    case "paddingleft":
                    case "padding-left":
                        element.style.paddingLeft = val.ToObject<float>();
                        modifications.Add($"paddingLeft={val}");
                        break;

                    case "paddingright":
                    case "padding-right":
                        element.style.paddingRight = val.ToObject<float>();
                        modifications.Add($"paddingRight={val}");
                        break;

                    case "paddingtop":
                    case "padding-top":
                        element.style.paddingTop = val.ToObject<float>();
                        modifications.Add($"paddingTop={val}");
                        break;

                    case "paddingbottom":
                    case "padding-bottom":
                        element.style.paddingBottom = val.ToObject<float>();
                        modifications.Add($"paddingBottom={val}");
                        break;

                    case "borderradius":
                    case "border-radius":
                        float radius = val.ToObject<float>();
                        element.style.borderTopLeftRadius = radius;
                        element.style.borderTopRightRadius = radius;
                        element.style.borderBottomLeftRadius = radius;
                        element.style.borderBottomRightRadius = radius;
                        modifications.Add($"borderRadius={val}");
                        break;

                    default:
                        modifications.Add($"[skipped] {key} (unsupported inline style)");
                        break;
                }
            }
        }

        private static bool? GetNullableBool(this ToolParams p, string key)
        {
            var raw = p.GetRaw(key);
            if (raw == null) return null;
            if (raw.Type == JTokenType.Boolean) return raw.ToObject<bool>();
            string s = raw.ToString();
            if (bool.TryParse(s, out bool result)) return result;
            return null;
        }

        /// <summary>
        /// Finds the index right after the closing '>' of the root UXML element opening tag.
        /// Returns -1 if not found or if the root tag is self-closing.
        /// </summary>
        private static int FindUxmlBodyStart(string content)
        {
            int searchFrom = 0;
            while (true)
            {
                int idx = content.IndexOf("<ui:UXML", searchFrom, StringComparison.OrdinalIgnoreCase);
                if (idx < 0)
                    idx = content.IndexOf("<UXML", searchFrom, StringComparison.OrdinalIgnoreCase);
                if (idx < 0)
                    return -1;

                // Skip matches inside XML comments (<!-- ... -->)
                int commentStart = content.LastIndexOf("<!--", idx, StringComparison.Ordinal);
                if (commentStart >= 0)
                {
                    int commentEnd = content.IndexOf("-->", commentStart + 4, StringComparison.Ordinal);
                    if (commentEnd >= 0 && commentEnd + 3 > idx)
                    {
                        searchFrom = commentEnd + 3;
                        continue;
                    }
                }

                int closeTag = content.IndexOf('>', idx);
                if (closeTag < 0) return -1;
                // Self-closing tag cannot have children
                if (closeTag > 0 && content[closeTag - 1] == '/') return -1;

                return closeTag + 1;
            }
        }

        // ---- Helpers ----

        private static string EnsureUniqueFilePath(string path)
        {
            if (!File.Exists(path)) return path;
            string dir = Path.GetDirectoryName(path) ?? string.Empty;
            string baseName = Path.GetFileNameWithoutExtension(path);
            string ext = Path.GetExtension(path);
            int counter = 1;
            string candidate;
            do
            {
                candidate = Path.Combine(dir, $"{baseName}-{counter}{ext}").Replace('\\', '/');
                counter++;
            } while (File.Exists(candidate));
            return candidate;
        }

        private static string ColorToHex(Color c)
        {
            return $"#{ColorUtility.ToHtmlStringRGBA(c)}";
        }

        private static string GetDecodedContents(ToolParams p)
        {
            bool isEncoded = p.GetBool("contents_encoded") || p.GetBool("contentsEncoded");

            if (isEncoded)
            {
                string encoded = p.Get("encoded_contents") ?? p.Get("encodedContents");
                if (!string.IsNullOrEmpty(encoded))
                {
                    try
                    {
                        return Encoding.UTF8.GetString(Convert.FromBase64String(encoded));
                    }
                    catch (FormatException ex)
                    {
                        throw new ArgumentException(
                            "Parameter 'encodedContents' must be valid base64 when 'contentsEncoded' is true.",
                            ex);
                    }
                }
            }

            return p.Get("contents");
        }

        /// <summary>
        /// Validates UXML content before writing to disk.
        /// Returns null if valid, or an error message if malformed.
        /// Populates warnings list with non-fatal issues.
        /// Uses XmlParserContext to pre-declare common UXML namespace prefixes
        /// (ui, uie, engine, editor) since Unity's parser is more lenient than System.Xml.
        /// </summary>

        /// <summary>
        /// Ensures the root UXML element has editor-extension-mode attribute.
        /// UI Builder requires this to open the file. Injects "False" if missing.
        /// </summary>
        private static string EnsureEditorExtensionMode(string contents)
        {
            if (contents.Contains("editor-extension-mode"))
                return contents;

            int idx = contents.IndexOf("<ui:UXML", StringComparison.Ordinal);
            if (idx < 0)
                idx = contents.IndexOf("<UXML", StringComparison.Ordinal);
            if (idx < 0)
                return contents;

            int closeTag = contents.IndexOf('>', idx);
            if (closeTag < 0)
                return contents;

            bool selfClosing = contents[closeTag - 1] == '/';
            int insertPos = selfClosing ? closeTag - 1 : closeTag;

            return contents.Substring(0, insertPos)
                 + " editor-extension-mode=\"False\""
                 + contents.Substring(insertPos);
        }

        private static string ValidateUxmlContent(string contents, List<string> warnings)
        {
            if (string.IsNullOrWhiteSpace(contents))
                return "UXML content is empty.";

            var nt = new NameTable();
            var nsMgr = new XmlNamespaceManager(nt);
            nsMgr.AddNamespace("ui", "UnityEngine.UIElements");
            nsMgr.AddNamespace("uie", "UnityEditor.UIElements");
            nsMgr.AddNamespace("engine", "UnityEngine.UIElements");
            nsMgr.AddNamespace("editor", "UnityEditor.UIElements");
            var ctx = new XmlParserContext(nt, nsMgr, null, XmlSpace.Default);

            string rootLocalName = null;
            try
            {
                using (var reader = XmlReader.Create(new StringReader(contents), null, ctx))
                {
                    while (reader.Read())
                    {
                        if (reader.NodeType == XmlNodeType.Element && rootLocalName == null)
                            rootLocalName = reader.LocalName;
                    }
                }
            }
            catch (XmlException ex)
            {
                return $"Malformed XML at line {ex.LineNumber}, position {ex.LinePosition}: {ex.Message}";
            }

            if (rootLocalName == null)
                return "UXML content has no root element.";

            if (rootLocalName != "UXML")
                warnings.Add($"Root element is <{rootLocalName}>, expected <UXML> or <ui:UXML>.");

            if (!contents.Contains("UnityEngine.UIElements"))
            {
                warnings.Add("Missing namespace declaration xmlns:ui=\"UnityEngine.UIElements\". " +
                              "UI Builder may fail to open this file.");
            }

            return null;
        }

        /// <summary>
        /// Validates a UXML asset after import by attempting to load it as a VisualTreeAsset.
        /// </summary>
        private static void ValidateUxmlPostImport(string assetPath, List<string> warnings)
        {
            var asset = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(assetPath);
            if (asset == null)
            {
                warnings.Add("Unity failed to parse the UXML file. " +
                              "The file was written but UI Builder will not be able to open it. " +
                              "Check the console for details.");
            }
        }
    }
}
