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

namespace MCPForUnity.Editor.Tools
{
    /// <summary>
    /// Single tool for ScriptableObject workflows:
    /// - action=create: create a ScriptableObject asset (and optionally apply patches)
    /// - action=modify: apply serialized property patches to an existing asset
    ///
    /// Patching is performed via SerializedObject/SerializedProperty paths (Unity-native), not reflection.
    /// </summary>
    [McpForUnityTool("manage_scriptable_object", AutoRegister = false, Group = "scripting_ext")]
    public static class ManageScriptableObject
    {
        private const string CodeCompilingOrReloading = "compiling_or_reloading";
        private const string CodeInvalidParams = "invalid_params";
        private const string CodeTypeNotFound = "type_not_found";
        private const string CodeInvalidFolderPath = "invalid_folder_path";
        private const string CodeTargetNotFound = "target_not_found";
        private const string CodeAssetCreateFailed = "asset_create_failed";

        private static readonly HashSet<string> ValidActions = new(StringComparer.OrdinalIgnoreCase)
        {
            // NOTE: Action strings are normalized by NormalizeAction() (lowercased, '_'/'-' removed),
            // so we only need the canonical normalized forms here.
            "create",
            "createso",
            "modify",
            "modifyso",
        };

        public static object HandleCommand(JObject @params)
        {
            if (@params == null)
            {
                return new ErrorResponse(CodeInvalidParams);
            }

            if (EditorApplication.isCompiling || EditorApplication.isUpdating)
            {
                // Unity is transient; treat as retryable on the client side.
                return new ErrorResponse(CodeCompilingOrReloading, new { hint = "retry" });
            }

            // Allow JSON-string parameters for objects/arrays.
            JsonUtil.CoerceJsonStringParameter(@params, "target");
            CoerceJsonStringArrayParameter(@params, "patches");

            string actionRaw = @params["action"]?.ToString();
            if (string.IsNullOrWhiteSpace(actionRaw))
            {
                return new ErrorResponse(CodeInvalidParams, new { message = "'action' is required.", validActions = ValidActions.ToArray() });
            }

            string action = NormalizeAction(actionRaw);
            if (!ValidActions.Contains(action))
            {
                return new ErrorResponse(CodeInvalidParams, new { message = $"Unknown action: '{actionRaw}'.", validActions = ValidActions.ToArray() });
            }

            if (IsCreateAction(action))
            {
                return HandleCreate(@params);
            }

            return HandleModify(@params);
        }

        private static object HandleCreate(JObject @params)
        {
            string typeName = @params["typeName"]?.ToString() ?? @params["type_name"]?.ToString();
            string folderPath = @params["folderPath"]?.ToString() ?? @params["folder_path"]?.ToString();
            string assetName = @params["assetName"]?.ToString() ?? @params["asset_name"]?.ToString();
            bool overwrite = @params["overwrite"]?.ToObject<bool?>() ?? false;

            if (string.IsNullOrWhiteSpace(typeName))
            {
                return new ErrorResponse(CodeInvalidParams, new { message = "'typeName' is required." });
            }

            if (string.IsNullOrWhiteSpace(folderPath))
            {
                return new ErrorResponse(CodeInvalidParams, new { message = "'folderPath' is required." });
            }

            if (string.IsNullOrWhiteSpace(assetName))
            {
                return new ErrorResponse(CodeInvalidParams, new { message = "'assetName' is required." });
            }

            if (assetName.Contains("/") || assetName.Contains("\\"))
            {
                return new ErrorResponse(CodeInvalidParams, new { message = "'assetName' must not contain path separators." });
            }

            if (!TryNormalizeFolderPath(folderPath, out var normalizedFolder, out var folderNormalizeError))
            {
                return new ErrorResponse(CodeInvalidFolderPath, new { message = folderNormalizeError, folderPath });
            }

            if (!EnsureFolderExists(normalizedFolder, out var folderError))
            {
                return new ErrorResponse(CodeInvalidFolderPath, new { message = folderError, folderPath = normalizedFolder });
            }

            var resolvedType = ResolveType(typeName);
            if (resolvedType == null || !typeof(ScriptableObject).IsAssignableFrom(resolvedType))
            {
                return new ErrorResponse(CodeTypeNotFound, new { message = $"ScriptableObject type not found: '{typeName}'", typeName });
            }

            string fileName = assetName.EndsWith(".asset", StringComparison.OrdinalIgnoreCase)
                ? assetName
                : assetName + ".asset";
            string desiredPath = $"{normalizedFolder.TrimEnd('/')}/{fileName}";
            string finalPath = overwrite ? desiredPath : AssetDatabase.GenerateUniqueAssetPath(desiredPath);

            ScriptableObject instance;
            try
            {
                instance = ScriptableObject.CreateInstance(resolvedType);
                if (instance == null)
                {
                    return new ErrorResponse(CodeAssetCreateFailed, new { message = "CreateInstance returned null.", typeName = resolvedType.FullName });
                }
            }
            catch (Exception ex)
            {
                return new ErrorResponse(CodeAssetCreateFailed, new { message = ex.Message, typeName = resolvedType.FullName });
            }

            // GUID-preserving overwrite logic
            bool isNewAsset = true;
            try
            {
                if (overwrite)
                {
                    var existingAsset = AssetDatabase.LoadAssetAtPath<ScriptableObject>(finalPath);
                    if (existingAsset != null && existingAsset.GetType() == resolvedType)
                    {
                        // Preserve GUID by overwriting existing asset data in-place
                        EditorUtility.CopySerialized(instance, existingAsset);
                        
                        // Fix for "Main Object Name does not match filename" warning:
                        // CopySerialized overwrites the name with the (empty) name of the new instance.
                        // We must restore the correct name to match the filename.
                        existingAsset.name = Path.GetFileNameWithoutExtension(finalPath);

                        UnityEngine.Object.DestroyImmediate(instance); // Destroy temporary instance
                        instance = existingAsset; // Proceed with patching the existing asset
                        isNewAsset = false;
                        
                        // Mark dirty to ensure changes are picked up
                        EditorUtility.SetDirty(instance);
                    }
                    else if (existingAsset != null)
                    {
                        // Type mismatch or not a ScriptableObject - must delete and recreate to change type, losing GUID
                        // (Or we could warn, but overwrite usually implies replacing)
                        AssetDatabase.DeleteAsset(finalPath);
                    }
                }

                if (isNewAsset)
                {
                    // Ensure the new instance has the correct name before creating asset to avoid warnings
                    instance.name = Path.GetFileNameWithoutExtension(finalPath);
                    AssetDatabase.CreateAsset(instance, finalPath);
                }
            }
            catch (Exception ex)
            {
                return new ErrorResponse(CodeAssetCreateFailed, new { message = ex.Message, path = finalPath });
            }

            string guid = AssetDatabase.AssetPathToGUID(finalPath);
            var patchesToken = @params["patches"];
            object patchResults = null;
            var warnings = new List<string>();

            if (patchesToken is JArray patches && patches.Count > 0)
            {
                var patchApply = ApplyPatches(instance, patches);
                patchResults = patchApply.results;
                warnings.AddRange(patchApply.warnings);
            }

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

            return new SuccessResponse(
                "ScriptableObject created.",
                new
                {
                    guid,
                    path = finalPath,
                    typeNameResolved = resolvedType.FullName,
                    patchResults,
                    warnings = warnings.Count > 0 ? warnings : null
                }
            );
        }

        private static object HandleModify(JObject @params)
        {
            if (!TryResolveTarget(@params["target"], out var target, out var targetPath, out var targetGuid, out var err))
            {
                return err;
            }

            var patchesToken = @params["patches"];
            if (patchesToken == null || patchesToken.Type == JTokenType.Null)
            {
                return new ErrorResponse(CodeInvalidParams, new { message = "'patches' is required.", targetPath, targetGuid });
            }

            if (patchesToken is not JArray patches)
            {
                return new ErrorResponse(CodeInvalidParams, new { message = "'patches' must be an array.", targetPath, targetGuid });
            }

            // Phase 5: Dry-run mode - validate patches without applying
            bool dryRun = @params["dryRun"]?.ToObject<bool?>() ?? @params["dry_run"]?.ToObject<bool?>() ?? false;
            
            if (dryRun)
            {
                var validationResults = ValidatePatches(target, patches);
                return new SuccessResponse(
                    "Dry-run validation complete.",
                    new
                    {
                        targetGuid,
                        targetPath,
                        targetTypeName = target.GetType().FullName,
                        dryRun = true,
                        valid = validationResults.All(r => (bool)r.GetType().GetProperty("ok")?.GetValue(r)),
                        validationResults
                    }
                );
            }

            var (results, warnings) = ApplyPatches(target, patches);

            return new SuccessResponse(
                "Serialized properties patched.",
                new
                {
                    targetGuid,
                    targetPath,
                    targetTypeName = target.GetType().FullName,
                    results,
                    warnings = warnings.Count > 0 ? warnings : null
                }
            );
        }

        /// <summary>
        /// Validates patches without applying them (for dry-run mode).
        /// Checks that property paths exist and that value types are compatible.
        /// </summary>
        private static List<object> ValidatePatches(UnityEngine.Object target, JArray patches)
        {
            var results = new List<object>(patches.Count);
            var so = new SerializedObject(target);
            so.Update();

            for (int i = 0; i < patches.Count; i++)
            {
                if (patches[i] is not JObject patchObj)
                {
                    results.Add(new { index = i, propertyPath = "", op = "", ok = false, message = $"Patch at index {i} must be an object." });
                    continue;
                }

                string propertyPath = patchObj["propertyPath"]?.ToString()
                    ?? patchObj["property_path"]?.ToString()
                    ?? patchObj["path"]?.ToString();
                string op = (patchObj["op"]?.ToString() ?? "set").Trim();

                if (string.IsNullOrWhiteSpace(propertyPath))
                {
                    results.Add(new { index = i, propertyPath = propertyPath ?? "", op, ok = false, message = "Missing required field: propertyPath" });
                    continue;
                }

                // Normalize the path
                string normalizedPath = NormalizePropertyPath(propertyPath);
                string normalizedOp = op.ToLowerInvariant();

                // For array_resize, check if the array exists
                if (normalizedOp == "array_resize")
                {
                    var valueToken = patchObj["value"];
                    if (valueToken == null || valueToken.Type == JTokenType.Null)
                    {
                        results.Add(new { index = i, propertyPath = normalizedPath, op, ok = false, message = "array_resize requires integer 'value'." });
                        continue;
                    }

                    int size = ParamCoercion.CoerceInt(valueToken, -1);
                    if (size < 0)
                    {
                        results.Add(new { index = i, propertyPath = normalizedPath, op, ok = false, message = "array_resize requires non-negative integer 'value'." });
                        continue;
                    }

                    // Check if the array path exists
                    string arrayPath = normalizedPath;
                    if (arrayPath.EndsWith(".Array.size", StringComparison.Ordinal))
                    {
                        arrayPath = arrayPath.Substring(0, arrayPath.Length - ".Array.size".Length);
                    }

                    var arrayProp = so.FindProperty(arrayPath);
                    if (arrayProp == null)
                    {
                        results.Add(new { index = i, propertyPath = normalizedPath, op, ok = false, message = $"Array not found: {arrayPath}" });
                        continue;
                    }

                    if (!arrayProp.isArray)
                    {
                        results.Add(new { index = i, propertyPath = normalizedPath, op, ok = false, message = $"Property is not an array: {arrayPath}" });
                        continue;
                    }

                    results.Add(new { index = i, propertyPath = normalizedPath, op, ok = true, message = $"Will resize to {size}.", currentSize = arrayProp.arraySize });
                    continue;
                }

                // For set operations, check if the property exists (or can be auto-grown)
                var prop = so.FindProperty(normalizedPath);
                
                // Check if it's an auto-growable array element path
                bool isAutoGrowable = false;
                if (prop == null)
                {
                    var match = Regex.Match(normalizedPath, @"^(.+?)\.Array\.data\[(\d+)\]");
                    if (match.Success)
                    {
                        string arrayPath = match.Groups[1].Value;
                        var arrayProp = so.FindProperty(arrayPath);
                        if (arrayProp != null && arrayProp.isArray)
                        {
                            isAutoGrowable = true;
                            // Get the element type info from existing elements or report as growable
                            int targetIndex = int.Parse(match.Groups[2].Value);
                            if (arrayProp.arraySize > 0)
                            {
                                var sampleElement = arrayProp.GetArrayElementAtIndex(0);
                                results.Add(new { 
                                    index = i, 
                                    propertyPath = normalizedPath, 
                                    op, 
                                    ok = true, 
                                    message = $"Will auto-grow array from {arrayProp.arraySize} to {targetIndex + 1}.",
                                    elementType = sampleElement?.propertyType.ToString() ?? "unknown"
                                });
                            }
                            else
                            {
                                results.Add(new { 
                                    index = i, 
                                    propertyPath = normalizedPath, 
                                    op, 
                                    ok = true, 
                                    message = $"Will auto-grow empty array to size {targetIndex + 1}."
                                });
                            }
                            continue;
                        }
                    }
                }

                if (prop == null && !isAutoGrowable)
                {
                    results.Add(new { index = i, propertyPath = normalizedPath, op, ok = false, message = $"Property not found: {normalizedPath}" });
                    continue;
                }

                if (prop != null)
                {
                    // Property exists - validate value format for supported complex types
                    var valueToken = patchObj["value"];
                    string valueValidationMsg = null;
                    bool valueFormatOk = true;
                    
                    // Enhanced dry-run: validate value format for AnimationCurve and Quaternion
                    // Uses shared validators from VectorParsing
                    if (valueToken != null && valueToken.Type != JTokenType.Null)
                    {
                        switch (prop.propertyType)
                        {
                            case SerializedPropertyType.AnimationCurve:
                                valueFormatOk = VectorParsing.ValidateAnimationCurveFormat(valueToken, out valueValidationMsg);
                                break;
                            case SerializedPropertyType.Quaternion:
                                valueFormatOk = VectorParsing.ValidateQuaternionFormat(valueToken, out valueValidationMsg);
                                break;
                        }
                    }
                    
                    if (valueFormatOk)
                    {
                        results.Add(new { 
                            index = i, 
                            propertyPath = normalizedPath, 
                            op, 
                            ok = true, 
                            message = valueValidationMsg ?? "Property found.",
                            propertyType = prop.propertyType.ToString(),
                            isArray = prop.isArray
                        });
                    }
                    else
                    {
                        results.Add(new { 
                            index = i, 
                            propertyPath = normalizedPath, 
                            op, 
                            ok = false, 
                            message = valueValidationMsg,
                            propertyType = prop.propertyType.ToString(),
                            isArray = prop.isArray
                        });
                    }
                }
            }

            return results;
        }

        private static (List<object> results, List<string> warnings) ApplyPatches(UnityEngine.Object target, JArray patches)
        {
            var warnings = new List<string>();
            var results = new List<object>(patches.Count);
            bool anyChanged = false;

            var so = new SerializedObject(target);
            so.Update();

            for (int i = 0; i < patches.Count; i++)
            {
                if (patches[i] is not JObject patchObj)
                {
                    results.Add(new { propertyPath = "", op = "", ok = false, message = $"Patch at index {i} must be an object." });
                    continue;
                }

                string propertyPath = patchObj["propertyPath"]?.ToString()
                    ?? patchObj["property_path"]?.ToString()
                    ?? patchObj["path"]?.ToString();
                string op = (patchObj["op"]?.ToString() ?? "set").Trim();
                if (string.IsNullOrWhiteSpace(propertyPath))
                {
                    results.Add(new { propertyPath = propertyPath ?? "", op, ok = false, message = "Missing required field: propertyPath" });
                    continue;
                }

                if (string.IsNullOrWhiteSpace(op))
                {
                    op = "set";
                }

                var patchResult = ApplyPatch(so, propertyPath, op, patchObj, out bool changed);
                anyChanged |= changed;
                results.Add(patchResult);

                // Array resize should be applied immediately so later paths resolve.
                if (string.Equals(op, "array_resize", StringComparison.OrdinalIgnoreCase) && changed)
                {
                    so.ApplyModifiedProperties();
                    so.Update();
                }
            }

            if (anyChanged)
            {
                so.ApplyModifiedProperties();
                EditorUtility.SetDirty(target);
                AssetDatabase.SaveAssets();
            }

            return (results, warnings);
        }

        private static object ApplyPatch(SerializedObject so, string propertyPath, string op, JObject patchObj, out bool changed)
        {
            changed = false;
            try
            {
                // Phase 1.1: Normalize friendly path syntax (e.g., myList[5] → myList.Array.data[5])
                string normalizedPath = NormalizePropertyPath(propertyPath);
                string normalizedOp = op.Trim().ToLowerInvariant();

                switch (normalizedOp)
                {
                    case "array_resize":
                        return ApplyArrayResize(so, normalizedPath, patchObj, out changed);
                    case "set":
                    default:
                        return ApplySet(so, normalizedPath, patchObj, out changed);
                }
            }
            catch (Exception ex)
            {
                return new { propertyPath, op, ok = false, message = ex.Message };
            }
        }

        /// <summary>
        /// Normalizes friendly property path syntax to Unity's internal format.
        /// Converts bracket notation (e.g., myList[5]) to Unity's Array.data format (myList.Array.data[5]).
        /// </summary>
        private static string NormalizePropertyPath(string path)
        {
            if (string.IsNullOrEmpty(path))
                return path;

            // Pattern: word[number] where it's not already in .Array.data[number] format
            // We need to handle cases like: myList[5], nested.list[0].field, etc.
            // But NOT: myList.Array.data[5] (already in Unity format)
            
            // Replace fieldName[index] with fieldName.Array.data[index]
            // But only if it's not already in Array.data format
            return Regex.Replace(path, @"(\w+)\[(\d+)\]", m =>
            {
                string fieldName = m.Groups[1].Value;
                string index = m.Groups[2].Value;
                
                // Check if this match is already part of .Array.data[index] pattern
                // by checking if the text immediately before the field name is ".Array."
                // and the field name is "data"
                int matchStart = m.Index;
                if (fieldName == "data" && matchStart >= 7) // Length of ".Array."
                {
                    string preceding = path.Substring(matchStart - 7, 7);
                    if (preceding == ".Array.")
                    {
                        // Already in Unity format (e.g., myList.Array.data[0]), return as-is
                        return m.Value;
                    }
                }
                
                return $"{fieldName}.Array.data[{index}]";
            });
        }

        /// <summary>
        /// Ensures an array has sufficient capacity for the given index.
        /// Automatically resizes the array if the target index is beyond current bounds.
        /// </summary>
        /// <param name="so">The SerializedObject containing the array</param>
        /// <param name="path">The normalized property path (must be in Array.data format)</param>
        /// <param name="resized">True if the array was resized</param>
        /// <returns>True if the path is valid for setting, false if it cannot be resolved</returns>
        private static bool EnsureArrayCapacity(SerializedObject so, string path, out bool resized)
        {
            resized = false;
            
            // Match pattern: something.Array.data[N]
            var match = Regex.Match(path, @"^(.+?)\.Array\.data\[(\d+)\]");
            if (!match.Success)
            {
                // Not an array element path, nothing to do
                return true;
            }

            string arrayPath = match.Groups[1].Value;
            if (!int.TryParse(match.Groups[2].Value, out int targetIndex))
            {
                return false;
            }

            var arrayProp = so.FindProperty(arrayPath);
            if (arrayProp == null || !arrayProp.isArray)
            {
                // Array property not found or not an array
                return false;
            }

            if (arrayProp.arraySize <= targetIndex)
            {
                // Need to grow the array
                arrayProp.arraySize = targetIndex + 1;
                so.ApplyModifiedProperties();
                so.Update();
                resized = true;
            }

            return true;
        }

        private static object ApplyArrayResize(SerializedObject so, string propertyPath, JObject patchObj, out bool changed)
        {
            changed = false;
            
            // Use ParamCoercion for robust int parsing
            var valueToken = patchObj["value"];
            if (valueToken == null || valueToken.Type == JTokenType.Null)
            {
                return new { propertyPath, op = "array_resize", ok = false, message = "array_resize requires integer 'value'." };
            }
            
            int newSize = ParamCoercion.CoerceInt(valueToken, -1);
            if (newSize < 0)
            {
                return new { propertyPath, op = "array_resize", ok = false, message = "array_resize requires integer 'value'." };
            }

            newSize = Math.Max(0, newSize);

            // Unity supports resizing either:
            // - the array/list property itself (prop.isArray -> prop.arraySize)
            // - the synthetic leaf property "<array>.Array.size" (prop.intValue)
            //
            // Different Unity versions/serialization edge cases can fail to resolve the synthetic leaf via FindProperty
            // (or can return different property types), so we keep a "best-effort" fallback:
            // - Prefer acting on the requested path if it resolves.
            // - If the requested path doesn't resolve, try to resolve the *array property* and set arraySize directly.
            SerializedProperty prop = so.FindProperty(propertyPath);
            SerializedProperty arrayProp = null;
            if (propertyPath.EndsWith(".Array.size", StringComparison.Ordinal))
            {
                // Caller explicitly targeted the synthetic leaf. Resolve the parent array property as a fallback
                // (Unity sometimes fails to resolve the synthetic leaf in certain serialization contexts).
                var arrayPath = propertyPath.Substring(0, propertyPath.Length - ".Array.size".Length);
                arrayProp = so.FindProperty(arrayPath);
            }
            else
            {
                // Caller targeted either the array property itself (e.g., "items") or some other property.
                // If it's already an array, we can resize it directly. Otherwise, we attempt to resolve
                // a synthetic ".Array.size" leaf as a convenience, which some clients may pass.
                arrayProp = prop != null && prop.isArray ? prop : so.FindProperty(propertyPath + ".Array.size");
            }

            if (prop == null)
            {
                // If we failed to find the direct property but we *can* find the array property, use that.
                if (arrayProp != null && arrayProp.isArray)
                {
                    if (arrayProp.arraySize != newSize)
                    {
                        arrayProp.arraySize = newSize;
                        changed = true;
                    }
                    return new
                    {
                        propertyPath,
                        op = "array_resize",
                        ok = true,
                        resolvedPropertyType = "Array",
                        message = $"Set array size to {newSize}."
                    };
                }

                return new { propertyPath, op = "array_resize", ok = false, message = $"Property not found: {propertyPath}" };
            }

            // Unity may represent ".Array.size" as either Integer or ArraySize depending on version.
            if ((prop.propertyType == SerializedPropertyType.Integer || prop.propertyType == SerializedPropertyType.ArraySize)
                && propertyPath.EndsWith(".Array.size", StringComparison.Ordinal))
            {
                // We successfully resolved the synthetic leaf; write the size through its intValue.
                if (prop.intValue != newSize)
                {
                    prop.intValue = newSize;
                    changed = true;
                }
                return new { propertyPath, op = "array_resize", ok = true, resolvedPropertyType = prop.propertyType.ToString(), message = $"Set array size to {newSize}." };
            }

            if (prop.isArray)
            {
                // We resolved the array property itself; write through arraySize.
                if (prop.arraySize != newSize)
                {
                    prop.arraySize = newSize;
                    changed = true;
                }
                return new { propertyPath, op = "array_resize", ok = true, resolvedPropertyType = "Array", message = $"Set array size to {newSize}." };
            }

            return new { propertyPath, op = "array_resize", ok = false, resolvedPropertyType = prop.propertyType.ToString(), message = $"Property is not an array or array-size field: {propertyPath}" };
        }

        private static object ApplySet(SerializedObject so, string propertyPath, JObject patchObj, out bool changed)
        {
            changed = false;
            
            // Phase 1.2: Auto-resize arrays if targeting an index beyond current bounds
            if (!EnsureArrayCapacity(so, propertyPath, out bool arrayResized))
            {
                // Could not resolve the array path - try to find the property anyway for a better error message
                var checkProp = so.FindProperty(propertyPath);
                if (checkProp == null)
                {
                    // Try to provide helpful context about what went wrong
                    var arrayMatch = Regex.Match(propertyPath, @"^(.+?)\.Array\.data\[(\d+)\]");
                    if (arrayMatch.Success)
                    {
                        string arrayPath = arrayMatch.Groups[1].Value;
                        var arrayProp = so.FindProperty(arrayPath);
                        if (arrayProp == null)
                        {
                            return new { propertyPath, op = "set", ok = false, message = $"Array property not found: {arrayPath}" };
                        }
                        if (!arrayProp.isArray)
                        {
                            return new { propertyPath, op = "set", ok = false, message = $"Property is not an array: {arrayPath}" };
                        }
                    }
                    return new { propertyPath, op = "set", ok = false, message = $"Property not found: {propertyPath}" };
                }
            }
            
            var prop = so.FindProperty(propertyPath);
            if (prop == null)
            {
                return new { propertyPath, op = "set", ok = false, message = $"Property not found: {propertyPath}" };
            }
            
            // Track if we resized - this counts as a change
            if (arrayResized)
            {
                changed = true;
            }

            if (prop.propertyType == SerializedPropertyType.ObjectReference)
            {
                // Legacy "ref" key takes precedence for backward compatibility.
                // Use TryGetValue to preserve non-JObject ref tokens (e.g. string GUID).
                patchObj.TryGetValue("ref", out JToken refToken);
                var objRefValue = patchObj["value"];
                JToken resolveToken = refToken ?? objRefValue;

                if (resolveToken == null)
                {
                    return new { propertyPath, op = "set", ok = false, resolvedPropertyType = prop.propertyType.ToString(),
                        message = "ObjectReference patch requires a 'ref' or 'value' key." };
                }

                if (!ComponentOps.SetObjectReference(prop, resolveToken, out string refError))
                {
                    return new { propertyPath, op = "set", ok = false, resolvedPropertyType = prop.propertyType.ToString(), message = refError };
                }

                changed = true;
                string refMessage = prop.objectReferenceValue == null
                    ? "Cleared reference."
                    : $"Set reference to '{prop.objectReferenceValue.name}'.";
                return new { propertyPath, op = "set", ok = true, resolvedPropertyType = prop.propertyType.ToString(), message = refMessage };
            }

            var valueToken = patchObj["value"];
            if (valueToken == null)
            {
                return new { propertyPath, op = "set", ok = false, resolvedPropertyType = prop.propertyType.ToString(), message = "Missing required field: value" };
            }

            bool ok = TrySetValue(prop, valueToken, out string message);
            changed = ok;
            return new { propertyPath, op = "set", ok, resolvedPropertyType = prop.propertyType.ToString(), message };
        }

        private static bool TrySetValue(SerializedProperty prop, JToken valueToken, out string message)
        {
            return TrySetValueRecursive(prop, valueToken, out message, 0);
        }

        /// <summary>
        /// Recursively sets values on SerializedProperties, supporting bulk array and object mapping.
        /// </summary>
        /// <param name="prop">The property to set</param>
        /// <param name="valueToken">The JSON value</param>
        /// <param name="message">Output message describing the result</param>
        /// <param name="depth">Current recursion depth (for safety limits)</param>
        private static bool TrySetValueRecursive(SerializedProperty prop, JToken valueToken, out string message, int depth)
        {
            message = null;
            const int MaxRecursionDepth = 20;

            if (depth > MaxRecursionDepth)
            {
                message = $"Maximum recursion depth ({MaxRecursionDepth}) exceeded. Check for circular references.";
                return false;
            }

            try
            {
                // Phase 3.1: Handle bulk array mapping - JArray value for array/list properties
                if (prop.isArray && prop.propertyType != SerializedPropertyType.String && valueToken is JArray jArray)
                {
                    // Resize the array to match the JSON array
                    prop.arraySize = jArray.Count;
                    
                    // Get the SerializedObject and apply so we can access elements
                    var so = prop.serializedObject;
                    so.ApplyModifiedProperties();
                    so.Update();

                    int successCount = 0;
                    var errors = new List<string>();

                    for (int i = 0; i < jArray.Count; i++)
                    {
                        var elementProp = prop.GetArrayElementAtIndex(i);
                        if (elementProp == null)
                        {
                            errors.Add($"Could not get element at index {i}");
                            continue;
                        }

                        if (TrySetValueRecursive(elementProp, jArray[i], out string elemMessage, depth + 1))
                        {
                            successCount++;
                        }
                        else
                        {
                            errors.Add($"[{i}]: {elemMessage}");
                        }
                    }

                    so.ApplyModifiedProperties();

                    if (errors.Count > 0)
                    {
                        message = $"Set {successCount}/{jArray.Count} elements. Errors: {string.Join("; ", errors)}";
                        return successCount > 0; // Partial success
                    }

                    message = $"Set array with {jArray.Count} elements.";
                    return true;
                }

                // Phase 3.2: Handle bulk object mapping - JObject value for Generic (struct/class) properties
                if (prop.propertyType == SerializedPropertyType.Generic && !prop.isArray && valueToken is JObject jObj)
                {
                    int successCount = 0;
                    var errors = new List<string>();
                    var so = prop.serializedObject;

                    foreach (var kvp in jObj)
                    {
                        string childPath = prop.propertyPath + "." + kvp.Key;
                        var childProp = so.FindProperty(childPath);

                        if (childProp == null)
                        {
                            errors.Add($"Property not found: {kvp.Key}");
                            continue;
                        }

                        if (TrySetValueRecursive(childProp, kvp.Value, out string childMessage, depth + 1))
                        {
                            successCount++;
                        }
                        else
                        {
                            errors.Add($"{kvp.Key}: {childMessage}");
                        }
                    }

                    so.ApplyModifiedProperties();

                    if (errors.Count > 0)
                    {
                        message = $"Set {successCount}/{jObj.Count} fields. Errors: {string.Join("; ", errors)}";
                        return successCount > 0; // Partial success
                    }

                    message = $"Set struct/class with {jObj.Count} fields.";
                    return true;
                }

                // ObjectReference - delegate to shared handler
                if (prop.propertyType == SerializedPropertyType.ObjectReference)
                {
                    if (!ComponentOps.SetObjectReference(prop, valueToken, out string refError))
                    {
                        message = refError;
                        return false;
                    }
                    message = prop.objectReferenceValue == null
                        ? "Cleared reference."
                        : $"Set reference to '{prop.objectReferenceValue.name}'.";
                    return true;
                }

                // Supported Types: Integer, Boolean, Float, String, Enum, Vector2, Vector3, Vector4, Color
                // Using shared helpers from ParamCoercion and VectorParsing
                switch (prop.propertyType)
                {
                    case SerializedPropertyType.Integer:
                        if (valueToken == null || valueToken.Type == JTokenType.Null)
                        {
                            message = "Expected integer value.";
                            return false;
                        }
                        if (valueToken.Type != JTokenType.Integer && valueToken.Type != JTokenType.Float
                            && !long.TryParse(valueToken.ToString(), out _))
                        {
                            message = "Expected integer value.";
                            return false;
                        }
                        if (prop.type == "long")
                            prop.longValue = ParamCoercion.CoerceLong(valueToken, 0);
                        else
                            prop.intValue = ParamCoercion.CoerceInt(valueToken, 0);
                        message = prop.type == "long" ? "Set long." : "Set int.";
                        return true;

                    case SerializedPropertyType.Boolean:
                        // Use ParamCoercion for robust bool parsing (handles "true", "1", "yes", etc.)
                        if (valueToken == null || valueToken.Type == JTokenType.Null)
                        {
                            message = "Expected boolean value.";
                            return false;
                        }
                        bool boolVal = ParamCoercion.CoerceBool(valueToken, false);
                        // Verify it actually looked like a bool
                        if (valueToken.Type != JTokenType.Boolean)
                        {
                            string strVal = valueToken.ToString().Trim().ToLowerInvariant();
                            if (strVal != "true" && strVal != "false" && strVal != "1" && strVal != "0" &&
                                strVal != "yes" && strVal != "no" && strVal != "on" && strVal != "off")
                            {
                                message = "Expected boolean value.";
                                return false;
                            }
                        }
                        prop.boolValue = boolVal;
                        message = "Set bool.";
                        return true;

                    case SerializedPropertyType.Float:
                        // Use ParamCoercion for robust float parsing
                        float floatVal = ParamCoercion.CoerceFloat(valueToken, float.NaN);
                        if (float.IsNaN(floatVal))
                        {
                            message = "Expected float value.";
                            return false;
                        }
                        prop.floatValue = floatVal;
                        message = "Set float.";
                        return true;

                    case SerializedPropertyType.String:
                        prop.stringValue = valueToken.Type == JTokenType.Null ? null : valueToken.ToString();
                        message = "Set string.";
                        return true;

                    case SerializedPropertyType.Enum:
                        return TrySetEnum(prop, valueToken, out message);

                    case SerializedPropertyType.Vector2:
                        // Use VectorParsing for Vector2
                        var v2 = VectorParsing.ParseVector2(valueToken);
                        if (v2 == null)
                        {
                            message = "Expected Vector2 (array or object).";
                            return false;
                        }
                        prop.vector2Value = v2.Value;
                        message = "Set Vector2.";
                        return true;

                    case SerializedPropertyType.Vector3:
                        // Use VectorParsing for Vector3
                        var v3 = VectorParsing.ParseVector3(valueToken);
                        if (v3 == null)
                        {
                            message = "Expected Vector3 (array or object).";
                            return false;
                        }
                        prop.vector3Value = v3.Value;
                        message = "Set Vector3.";
                        return true;

                    case SerializedPropertyType.Vector4:
                        // Use VectorParsing for Vector4
                        var v4 = VectorParsing.ParseVector4(valueToken);
                        if (v4 == null)
                        {
                            message = "Expected Vector4 (array or object).";
                            return false;
                        }
                        prop.vector4Value = v4.Value;
                        message = "Set Vector4.";
                        return true;

                    case SerializedPropertyType.Color:
                        // Use VectorParsing for Color
                        var col = VectorParsing.ParseColor(valueToken);
                        if (col == null)
                        {
                            message = "Expected Color (array or object).";
                            return false;
                        }
                        prop.colorValue = col.Value;
                        message = "Set Color.";
                        return true;

                    case SerializedPropertyType.AnimationCurve:
                        return TrySetAnimationCurve(prop, valueToken, out message);

                    case SerializedPropertyType.Quaternion:
                        return TrySetQuaternion(prop, valueToken, out message);

                    case SerializedPropertyType.Generic:
                        // Generic properties (structs/classes) should be handled above with JObject mapping
                        // If we get here, the value wasn't a JObject
                        if (prop.isArray)
                        {
                            message = $"Expected array (JArray) for array property, got {valueToken?.Type.ToString() ?? "null"}.";
                        }
                        else
                        {
                            message = $"Expected object (JObject) for struct/class property, got {valueToken?.Type.ToString() ?? "null"}.";
                        }
                        return false;

                    default:
                        message = $"Unsupported SerializedPropertyType: {prop.propertyType}. " +
                                  "This type cannot be set via MCP patches. Consider editing the .asset file directly " +
                                  "or using Unity's Inspector. For complex types, check if there's a supported alternative format.";
                        return false;
                }
            }
            catch (Exception ex)
            {
                message = ex.Message;
                return false;
            }
        }

        private static bool TrySetEnum(SerializedProperty prop, JToken valueToken, out string message)
        {
            message = null;
            var names = prop.enumNames;
            if (names == null || names.Length == 0) { message = "Enum has no names."; return false; }

            if (valueToken.Type == JTokenType.Integer)
            {
                int idx = valueToken.Value<int>();
                if (idx < 0 || idx >= names.Length) { message = $"Enum index out of range: {idx}"; return false; }
                prop.enumValueIndex = idx; message = "Set enum."; return true;
            }

            string s = valueToken.ToString();
            for (int i = 0; i < names.Length; i++)
            {
                if (string.Equals(names[i], s, StringComparison.OrdinalIgnoreCase))
                {
                    prop.enumValueIndex = i; message = "Set enum."; return true;
                }
            }
            message = $"Unknown enum name '{s}'.";
            return false;
        }

        /// <summary>
        /// Sets an AnimationCurve property from a JSON structure.
        /// 
        /// <para><b>Supported formats:</b></para>
        /// <list type="bullet">
        ///   <item>Wrapped: <c>{ "keys": [ { "time": 0, "value": 1.0 }, ... ] }</c></item>
        ///   <item>Direct array: <c>[ { "time": 0, "value": 1.0 }, ... ]</c></item>
        ///   <item>Null/empty: Sets an empty AnimationCurve</item>
        /// </list>
        /// 
        /// <para><b>Keyframe fields:</b></para>
        /// <list type="bullet">
        ///   <item><c>time</c> (float): Keyframe time position. <b>Default: 0</b></item>
        ///   <item><c>value</c> (float): Keyframe value. <b>Default: 0</b></item>
        ///   <item><c>inSlope</c> or <c>inTangent</c> (float): Incoming tangent slope. <b>Default: 0</b></item>
        ///   <item><c>outSlope</c> or <c>outTangent</c> (float): Outgoing tangent slope. <b>Default: 0</b></item>
        ///   <item><c>weightedMode</c> (int): Weighted mode enum (0=None, 1=In, 2=Out, 3=Both). <b>Default: 0 (None)</b></item>
        ///   <item><c>inWeight</c> (float): Incoming tangent weight. <b>Default: 0</b></item>
        ///   <item><c>outWeight</c> (float): Outgoing tangent weight. <b>Default: 0</b></item>
        /// </list>
        /// 
        /// <para><b>Note:</b> All keyframe fields are optional. Missing fields gracefully default to 0,
        /// which produces linear interpolation when both tangents are 0.</para>
        /// </summary>
        /// <param name="prop">The SerializedProperty of type AnimationCurve to set</param>
        /// <param name="valueToken">JSON token containing the curve data</param>
        /// <param name="message">Output message describing the result</param>
        /// <returns>True if successful, false if the format is invalid</returns>
        private static bool TrySetAnimationCurve(SerializedProperty prop, JToken valueToken, out string message)
        {
            message = null;

            if (valueToken == null || valueToken.Type == JTokenType.Null)
            {
                // Set to empty curve
                prop.animationCurveValue = new AnimationCurve();
                message = "Set AnimationCurve to empty.";
                return true;
            }

            JArray keysArray = null;

            // Accept either { "keys": [...] } or just [...]
            if (valueToken is JObject curveObj)
            {
                keysArray = curveObj["keys"] as JArray;
                if (keysArray == null)
                {
                    message = "AnimationCurve object requires 'keys' array. Expected: { \"keys\": [ { \"time\": 0, \"value\": 0 }, ... ] }";
                    return false;
                }
            }
            else if (valueToken is JArray directArray)
            {
                keysArray = directArray;
            }
            else
            {
                message = "AnimationCurve requires object with 'keys' or array of keyframes. " +
                          "Expected: { \"keys\": [ { \"time\": 0, \"value\": 0, \"inSlope\": 0, \"outSlope\": 0 }, ... ] }";
                return false;
            }

            try
            {
                var curve = new AnimationCurve();
                foreach (var keyToken in keysArray)
                {
                    if (keyToken is not JObject keyObj)
                    {
                        message = "Each keyframe must be an object with 'time' and 'value'.";
                        return false;
                    }

                    float time = keyObj["time"]?.Value<float>() ?? 0f;
                    float value = keyObj["value"]?.Value<float>() ?? 0f;
                    float inSlope = keyObj["inSlope"]?.Value<float>() ?? keyObj["inTangent"]?.Value<float>() ?? 0f;
                    float outSlope = keyObj["outSlope"]?.Value<float>() ?? keyObj["outTangent"]?.Value<float>() ?? 0f;

                    var keyframe = new Keyframe(time, value, inSlope, outSlope);

                    // Optional: weighted tangent mode (Unity 2018.1+)
                    if (keyObj["weightedMode"] != null)
                    {
                        int weightedMode = keyObj["weightedMode"].Value<int>();
                        keyframe.weightedMode = (WeightedMode)weightedMode;
                    }
                    if (keyObj["inWeight"] != null)
                    {
                        keyframe.inWeight = keyObj["inWeight"].Value<float>();
                    }
                    if (keyObj["outWeight"] != null)
                    {
                        keyframe.outWeight = keyObj["outWeight"].Value<float>();
                    }

                    curve.AddKey(keyframe);
                }

                prop.animationCurveValue = curve;
                message = $"Set AnimationCurve with {keysArray.Count} keyframes.";
                return true;
            }
            catch (Exception ex)
            {
                message = $"Failed to parse AnimationCurve: {ex.Message}";
                return false;
            }
        }

        /// <summary>
        /// Sets a Quaternion property from JSON.
        /// 
        /// <para><b>Supported formats:</b></para>
        /// <list type="bullet">
        ///   <item>Euler array: <c>[x, y, z]</c> - Euler angles in degrees</item>
        ///   <item>Raw quaternion array: <c>[x, y, z, w]</c> - Direct quaternion components</item>
        ///   <item>Object format: <c>{ "x": 0, "y": 0, "z": 0, "w": 1 }</c> - Direct components</item>
        ///   <item>Explicit euler: <c>{ "euler": [x, y, z] }</c> - Euler angles in degrees</item>
        ///   <item>Null/empty: Sets Quaternion.identity (no rotation)</item>
        /// </list>
        /// 
        /// <para><b>Format detection:</b></para>
        /// <list type="bullet">
        ///   <item>3-element array → Interpreted as Euler angles (degrees)</item>
        ///   <item>4-element array → Interpreted as raw quaternion [x, y, z, w]</item>
        ///   <item>Object with euler → Uses euler array for rotation</item>
        ///   <item>Object with x, y, z, w → Uses raw quaternion components</item>
        /// </list>
        /// </summary>
        /// <param name="prop">The SerializedProperty of type Quaternion to set</param>
        /// <param name="valueToken">JSON token containing the quaternion data</param>
        /// <param name="message">Output message describing the result</param>
        /// <returns>True if successful, false if the format is invalid</returns>
        private static bool TrySetQuaternion(SerializedProperty prop, JToken valueToken, out string message)
        {
            message = null;

            if (valueToken == null || valueToken.Type == JTokenType.Null)
            {
                prop.quaternionValue = Quaternion.identity;
                message = "Set Quaternion to identity.";
                return true;
            }

            try
            {
                if (valueToken is JArray arr)
                {
                    if (arr.Count == 3)
                    {
                        // Euler angles [x, y, z]
                        var euler = new Vector3(
                            arr[0].Value<float>(),
                            arr[1].Value<float>(),
                            arr[2].Value<float>()
                        );
                        prop.quaternionValue = Quaternion.Euler(euler);
                        message = $"Set Quaternion from Euler({euler.x}, {euler.y}, {euler.z}).";
                        return true;
                    }
                    else if (arr.Count == 4)
                    {
                        // Raw quaternion [x, y, z, w]
                        prop.quaternionValue = new Quaternion(
                            arr[0].Value<float>(),
                            arr[1].Value<float>(),
                            arr[2].Value<float>(),
                            arr[3].Value<float>()
                        );
                        message = "Set Quaternion from [x, y, z, w].";
                        return true;
                    }
                    else
                    {
                        message = "Quaternion array must have 3 elements (Euler) or 4 elements (x, y, z, w).";
                        return false;
                    }
                }
                else if (valueToken is JObject obj)
                {
                    // Check for explicit euler property
                    if (obj["euler"] is JArray eulerArr && eulerArr.Count == 3)
                    {
                        var euler = new Vector3(
                            eulerArr[0].Value<float>(),
                            eulerArr[1].Value<float>(),
                            eulerArr[2].Value<float>()
                        );
                        prop.quaternionValue = Quaternion.Euler(euler);
                        message = $"Set Quaternion from euler: ({euler.x}, {euler.y}, {euler.z}).";
                        return true;
                    }

                    // Object format { x, y, z, w }
                    if (obj["x"] != null && obj["y"] != null && obj["z"] != null && obj["w"] != null)
                    {
                        prop.quaternionValue = new Quaternion(
                            obj["x"].Value<float>(),
                            obj["y"].Value<float>(),
                            obj["z"].Value<float>(),
                            obj["w"].Value<float>()
                        );
                        message = "Set Quaternion from { x, y, z, w }.";
                        return true;
                    }

                    message = "Quaternion object must have { x, y, z, w } or { euler: [x, y, z] }.";
                    return false;
                }
                else
                {
                    message = "Quaternion requires array [x,y,z] (Euler), [x,y,z,w] (raw), or object { x, y, z, w }.";
                    return false;
                }
            }
            catch (Exception ex)
            {
                message = $"Failed to parse Quaternion: {ex.Message}";
                return false;
            }
        }

        private static bool TryResolveTarget(JToken targetToken, out UnityEngine.Object target, out string targetPath, out string targetGuid, out object error)
        {
            target = null;
            targetPath = null;
            targetGuid = null;
            error = null;

            if (targetToken is not JObject targetObj)
            {
                error = new ErrorResponse(CodeInvalidParams, new { message = "'target' must be an object with {guid|path}." });
                return false;
            }

            string guid = targetObj["guid"]?.ToString();
            string path = targetObj["path"]?.ToString();

            if (string.IsNullOrWhiteSpace(guid) && string.IsNullOrWhiteSpace(path))
            {
                error = new ErrorResponse(CodeInvalidParams, new { message = "'target' must include 'guid' or 'path'." });
                return false;
            }

            string resolvedPath = !string.IsNullOrWhiteSpace(guid)
                ? AssetDatabase.GUIDToAssetPath(guid)
                : AssetPathUtility.SanitizeAssetPath(path);

            if (string.IsNullOrWhiteSpace(resolvedPath))
            {
                error = new ErrorResponse(CodeTargetNotFound, new { message = "Could not resolve target path.", guid, path });
                return false;
            }

            var obj = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(resolvedPath);
            if (obj == null)
            {
                error = new ErrorResponse(CodeTargetNotFound, new { message = "Target asset not found.", targetPath = resolvedPath, targetGuid = guid });
                return false;
            }

            target = obj;
            targetPath = resolvedPath;
            targetGuid = string.IsNullOrWhiteSpace(guid) ? AssetDatabase.AssetPathToGUID(resolvedPath) : guid;
            return true;
        }

        private static void CoerceJsonStringArrayParameter(JObject @params, string paramName)
        {
            var token = @params?[paramName];
            if (token != null && token.Type == JTokenType.String)
            {
                try
                {
                    var parsed = JToken.Parse(token.ToString());
                    if (parsed is JArray arr)
                    {
                        @params[paramName] = arr;
                    }
                }
                catch (Exception e)
                {
                    McpLog.Warn($"[MCP] Could not parse '{paramName}' JSON string: {e.Message}");
                }
            }
        }

        private static bool EnsureFolderExists(string folderPath, out string error)
        {
            error = null;
            if (string.IsNullOrWhiteSpace(folderPath))
            {
                error = "Folder path is empty.";
                return false;
            }

            // Expect normalized input here (Assets/... or Assets).
            string sanitized = SanitizeSlashes(folderPath);

            if (!sanitized.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)
                && !string.Equals(sanitized, "Assets", StringComparison.OrdinalIgnoreCase))
            {
                error = "Folder path must be under Assets/.";
                return false;
            }

            if (string.Equals(sanitized, "Assets", StringComparison.OrdinalIgnoreCase))
            {
                return true;
            }

            sanitized = sanitized.TrimEnd('/');
            if (AssetDatabase.IsValidFolder(sanitized))
            {
                return true;
            }

            // Create recursively from Assets/
            var parts = sanitized.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
            if (parts.Length == 0 || !string.Equals(parts[0], "Assets", StringComparison.OrdinalIgnoreCase))
            {
                error = "Folder path must start with Assets/";
                return false;
            }

            string current = "Assets";
            for (int i = 1; i < parts.Length; i++)
            {
                string next = current + "/" + parts[i];
                if (!AssetDatabase.IsValidFolder(next))
                {
                    string guid = AssetDatabase.CreateFolder(current, parts[i]);
                    if (string.IsNullOrEmpty(guid))
                    {
                        error = $"Failed to create folder: {next}";
                        return false;
                    }
                }
                current = next;
            }

            return AssetDatabase.IsValidFolder(sanitized);
        }

        private static string SanitizeSlashes(string path)
        {
            if (string.IsNullOrWhiteSpace(path))
            {
                return path;
            }

            var s = AssetPathUtility.NormalizeSeparators(path);
            while (s.IndexOf("//", StringComparison.Ordinal) >= 0)
            {
                s = s.Replace("//", "/", StringComparison.Ordinal);
            }
            return s;
        }

        private static bool TryNormalizeFolderPath(string folderPath, out string normalized, out string error)
        {
            normalized = null;
            error = null;

            if (string.IsNullOrWhiteSpace(folderPath))
            {
                error = "Folder path is empty.";
                return false;
            }

            var s = SanitizeSlashes(folderPath.Trim());

            // Reject obvious non-project/invalid roots. We only support Assets/ (and relative paths that will be rooted under Assets/).
            if (s.StartsWith("/", StringComparison.Ordinal) 
                || s.StartsWith("file:", StringComparison.OrdinalIgnoreCase)
                || Regex.IsMatch(s, @"^[a-zA-Z]:"))
            {
                error = "Folder path must be a project-relative path under Assets/.";
                return false;
            }

            if (s.StartsWith("Packages/", StringComparison.OrdinalIgnoreCase)
                || s.StartsWith("ProjectSettings/", StringComparison.OrdinalIgnoreCase)
                || s.StartsWith("Library/", StringComparison.OrdinalIgnoreCase))
            {
                error = "Folder path must be under Assets/.";
                return false;
            }

            if (string.Equals(s, "Assets", StringComparison.OrdinalIgnoreCase))
            {
                normalized = "Assets";
                return true;
            }

            if (s.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
            {
                normalized = s.TrimEnd('/');
                return true;
            }

            // Allow relative paths like "Temp/MyFolder" and root them under Assets/.
            normalized = ("Assets/" + s.TrimStart('/')).TrimEnd('/');
            return true;
        }

        // NOTE: Local TryGet* helpers have been removed. 
        // Using shared helpers instead: ParamCoercion (for int/float/bool) and VectorParsing (for Vector2/3/4, Color)

        private static string NormalizeAction(string raw)
        {
            var s = raw.Trim();
            s = s.Replace("-", "").Replace("_", "");
            return s.ToLowerInvariant();
        }

        private static bool IsCreateAction(string normalized)
        {
            return normalized == "create" || normalized == "createso";
        }

        /// <summary>
        /// Resolves a type by name. Delegates to UnityTypeResolver.ResolveAny().
        /// </summary>
        private static Type ResolveType(string typeName)
        {
            return Helpers.UnityTypeResolver.ResolveAny(typeName);
        }
    }
}
