using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;

using AIRuntime.Runtime.Capabilities;
using AIRuntime.Runtime.Capabilities.Attributes;
using AIRuntime.Runtime.Capabilities.Permissions;

using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;


namespace AIRuntime.Editor
{
    [Serializable]
    public sealed class EditorVector3Arguments
    {
        public float X { get; set; }

        public float Y { get; set; }

        public float Z { get; set; }


        internal Vector3 ToVector3()
        {
            return new Vector3(
                X,
                Y,
                Z);
        }
    }


    [Serializable]
    public sealed class CreateEditorSceneArguments
    {
        [Required]
        [Description("Unity asset path ending in .unity, for example Assets/MyGame/Scenes/Main.unity.")]
        public string ScenePath { get; set; }

        [DefaultValue(true)]
        [Description("Open and reuse the scene when the asset already exists.")]
        public bool OpenIfExists { get; set; } =
            true;

        [DefaultValue(true)]
        [Description("Make the created or opened scene the active Editor scene.")]
        public bool SetActive { get; set; } =
            true;
    }


    [Serializable]
    internal sealed class CreateEditorSceneResult
    {
        public bool Succeeded;

        public bool Created;

        public string ScenePath;

        public string SceneName;

        public bool Loaded;

        public bool Active;

        public bool ActiveRequested;

        public string Warning;

        public int RootCount;

        public string ResponseInstruction =
            "Use create_game_object to author objects in this scene, then save_editor_scene. Never write Unity scene YAML manually.";
    }


    public sealed class CreateEditorSceneCapability :
        CapabilityBase<CreateEditorSceneArguments>
    {
        public override string Name =>
            "create_editor_scene";


        public override string Description =>
            "Creates and saves a real Unity scene asset, or opens an existing scene asset, without generating scene YAML manually.";


        public override CapabilityPermission Permission =>
            CapabilityPermission.Write;


        protected override Task<object> ExecuteAsync(
            CreateEditorSceneArguments arguments,
            CapabilityContext context)
        {
            context.CancellationToken.ThrowIfCancellationRequested();

            if (arguments == null)
                throw new ArgumentNullException(nameof(arguments));

            string scenePath =
                EditorSceneAuthoringUtility.ValidateScenePath(
                    arguments.ScenePath);

            EditorSceneAuthoringUtility.EnsureAssetFolder(
                EditorSceneAuthoringUtility.GetParentAssetPath(
                    scenePath));

            Scene scene =
                EditorSceneAuthoringUtility.FindLoadedScene(
                    scenePath);

            bool created =
                false;

            if (!scene.IsValid())
            {
                SceneAsset existing =
                    AssetDatabase.LoadAssetAtPath<SceneAsset>(
                        scenePath);

                if (existing != null)
                {
                    if (!arguments.OpenIfExists)
                    {
                        throw new InvalidOperationException(
                            $"Scene already exists: {scenePath}");
                    }

                    scene =
                        EditorSceneManager.OpenScene(
                            scenePath,
                            OpenSceneMode.Additive);
                }
                else
                {
                    NewSceneMode creationMode =
                        PrepareForSceneCreation();

                    try
                    {
                        scene =
                            EditorSceneManager.NewScene(
                                NewSceneSetup.EmptyScene,
                                creationMode);
                    }
                    catch (InvalidOperationException exception)
                    {
                        throw new InvalidOperationException(
                            "Unity could not create an additive scene while an unsaved untitled scene is open. Save or close the untitled scene, then retry.",
                            exception);
                    }

                    if (!EditorSceneManager.SaveScene(
                            scene,
                            scenePath,
                            false))
                    {
                        throw new InvalidOperationException(
                            $"Unity could not save the new scene at '{scenePath}'.");
                    }

                    created =
                        true;
                }
            }

            bool active =
                scene == SceneManager.GetActiveScene();

            if (arguments.SetActive &&
                !active)
            {
                // Unity can transiently reject activation while an additive
                // scene is being imported or restored. The scene asset is
                // nevertheless valid, loaded, and addressable by its exact
                // path, so activation must not turn successful creation into
                // a failed authoring task.
                active =
                    SceneManager.SetActiveScene(scene);
            }

            AssetDatabase.SaveAssets();

            return Task.FromResult<object>(
                new CreateEditorSceneResult
                {
                    Succeeded = true,
                    Created = created,
                    ScenePath = scene.path.Replace('\\', '/'),
                    SceneName = scene.name,
                    Loaded = scene.isLoaded,
                    Active = active,
                    ActiveRequested = arguments.SetActive,
                    Warning = arguments.SetActive && !active
                        ? "Unity created and loaded the scene but did not make it active. Continue by passing ScenePath explicitly to scene tools."
                        : string.Empty,
                    RootCount = scene.GetRootGameObjects().Length
                });
        }


        private static NewSceneMode PrepareForSceneCreation()
        {
            List<Scene> untitledScenes =
                new List<Scene>();

            for (int index = 0;
                 index < SceneManager.sceneCount;
                 index++)
            {
                Scene loadedScene =
                    SceneManager.GetSceneAt(
                        index);

                if (loadedScene.IsValid() &&
                    loadedScene.isLoaded &&
                    string.IsNullOrWhiteSpace(
                        loadedScene.path) &&
                    !EditorSceneManager.IsPreviewScene(
                        loadedScene))
                {
                    untitledScenes.Add(
                        loadedScene);
                }
            }


            Scene unsafeUntitledScene =
                untitledScenes.FirstOrDefault(
                    candidate =>
                        candidate.GetRootGameObjects().Length >
                            0 &&
                        !ContainsOnlyDefaultSceneObjects(
                            candidate));

            if (unsafeUntitledScene.IsValid())
            {
                string rootSummary =
                    string.Join(
                        ", ",
                        unsafeUntitledScene
                            .GetRootGameObjects()
                            .Select(
                                root =>
                                    $"'{root.name}' [{string.Join(", ", root.GetComponents<Component>().Select(component => component == null ? "Missing Script" : component.GetType().Name))}]"));

                throw new InvalidOperationException(
                    "An unsaved untitled scene contains GameObjects that are not the standard Main Camera and Directional Light startup pair. " +
                    $"Roots: {rootSummary}. AIRuntime will not close it automatically because that could discard user work. " +
                    "Save or close that scene, then retry.");
            }


            if (untitledScenes.Count ==
                    1 &&
                SceneManager.sceneCount ==
                    1)
            {
                // Replacing Unity's empty startup scene is safe and avoids
                // the additive-scene restriction without prompting or data
                // loss.
                return NewSceneMode.Single;
            }


            foreach (Scene untitledScene in untitledScenes)
            {
                if (!EditorSceneManager.CloseScene(
                        untitledScene,
                        true))
                {
                    throw new InvalidOperationException(
                        "Unity could not close an empty untitled scene before creating the requested scene.");
                }
            }


            return NewSceneMode.Additive;
        }


        private static bool ContainsOnlyDefaultSceneObjects(
            Scene scene)
        {
            GameObject[] roots =
                scene.GetRootGameObjects();

            if (roots.Length !=
                2)
            {
                return false;
            }


            GameObject cameraObject =
                roots.FirstOrDefault(
                    root =>
                        string.Equals(
                            root.name,
                            "Main Camera",
                            StringComparison.Ordinal));

            GameObject lightObject =
                roots.FirstOrDefault(
                    root =>
                        string.Equals(
                            root.name,
                            "Directional Light",
                            StringComparison.Ordinal));

            if (cameraObject ==
                    null ||
                lightObject ==
                    null ||
                cameraObject.transform.childCount !=
                    0 ||
                lightObject.transform.childCount !=
                    0)
            {
                return false;
            }


            Component[] cameraComponents =
                cameraObject.GetComponents<Component>();

            Component[] lightComponents =
                lightObject.GetComponents<Component>();

            bool defaultCameraComponents =
                cameraObject.GetComponent<Camera>() !=
                    null &&
                cameraObject.GetComponent<AudioListener>() !=
                    null &&
                cameraComponents.All(
                    component =>
                        IsAllowedDefaultSceneComponent(
                            component,
                            typeof(Transform),
                            typeof(Camera),
                            typeof(AudioListener)));

            Light directionalLight =
                lightObject.GetComponent<Light>();

            bool defaultLightComponents =
                directionalLight !=
                    null &&
                directionalLight.type ==
                    LightType.Directional &&
                lightComponents.All(
                    component =>
                        IsAllowedDefaultSceneComponent(
                            component,
                            typeof(Transform),
                            typeof(Light)));

            // Unity templates and editor versions use different startup
            // transforms for this pair. Object names, hierarchy, and the exact
            // built-in component sets are the stable definition; transform
            // values are deliberately not used to identify the disposable
            // startup scene.
            return defaultCameraComponents &&
                   defaultLightComponents;
        }


        private static bool IsAllowedDefaultSceneComponent(
            Component component,
            params Type[] requiredBuiltInTypes)
        {
            if (component ==
                null)
            {
                return false;
            }


            Type componentType =
                component.GetType();

            if (requiredBuiltInTypes.Contains(
                    componentType))
            {
                return true;
            }


            // URP and HDRP add their own camera/light data components to the
            // standard startup objects. They live in UnityEngine.Rendering
            // namespaces and are template state rather than user gameplay.
            return componentType.Namespace !=
                       null &&
                   componentType.Namespace.StartsWith(
                       "UnityEngine.Rendering",
                       StringComparison.Ordinal);
        }
    }


    [Serializable]
    public sealed class CreateGameObjectArguments
    {
        [Required]
        [Description("Name of the new GameObject.")]
        public string Name { get; set; }

        [Description("Optional loaded scene path or exact loaded scene name. When omitted, the active scene is used.")]
        public string ScenePath { get; set; }

        [DefaultValue("Empty")]
        [Description("Object kind: Empty, Cube, Sphere, Capsule, Cylinder, Plane, Quad, Camera, or DirectionalLight.")]
        public string ObjectType { get; set; } =
            "Empty";

        [Description("Optional component types to install at GameObject construction time. This supports Transform subclasses such as UnityEngine.RectTransform for UI objects. Prefer full type names.")]
        public List<string> ComponentTypes { get; set; } =
            new List<string>();

        [Description("Optional exact parent GameObject instance ID.")]
        public int ParentGameObjectInstanceId { get; set; }

        [Description("World position, or local position when a parent is supplied.")]
        public EditorVector3Arguments Position { get; set; }

        [Description("World Euler rotation, or local Euler rotation when a parent is supplied.")]
        public EditorVector3Arguments Rotation { get; set; }

        [Description("Local scale. Defaults to one.")]
        public EditorVector3Arguments Scale { get; set; }

        [DefaultValue(true)]
        [Description("Select the created GameObject in the Unity Editor.")]
        public bool SelectCreatedObject { get; set; } =
            true;

        [DefaultValue(true)]
        [Description("Save the target scene after creating the object.")]
        public bool SaveScene { get; set; } =
            true;
    }


    [Serializable]
    internal sealed class CreateGameObjectResult
    {
        public bool Succeeded;

        public int GameObjectInstanceId;

        public string GlobalObjectId;

        public string Name;

        public string ObjectType;

        public string ScenePath;

        public string HierarchyPath;

        public List<string> Components =
            new List<string>();

        public bool SceneSaved;

        public string ResponseInstruction =
            "Use the returned GameObjectInstanceId for add_editor_component and serialized-property tools. Save and inspect the scene before claiming completion.";
    }


    public sealed class CreateGameObjectCapability :
        CapabilityBase<CreateGameObjectArguments>
    {
        public override string Name =>
            "create_game_object";


        public override string Description =>
            "Creates an Empty, primitive, Camera, or DirectionalLight GameObject in an existing loaded Unity scene and returns its exact identity.";


        public override CapabilityPermission Permission =>
            CapabilityPermission.Write;


        protected override Task<object> ExecuteAsync(
            CreateGameObjectArguments arguments,
            CapabilityContext context)
        {
            context.CancellationToken.ThrowIfCancellationRequested();

            if (arguments == null ||
                string.IsNullOrWhiteSpace(arguments.Name))
            {
                throw new ArgumentException("Name is required.");
            }

            Scene scene =
                EditorSceneAuthoringUtility.ResolveLoadedScene(
                    arguments.ScenePath);

            if (arguments.SaveScene)
            {
                EditorSceneAuthoringUtility
                    .EnsureSceneCanBeSavedNonInteractively(
                        scene);
            }

            GameObject gameObject =
                CreateObject(
                    arguments.ObjectType,
                    arguments.ComponentTypes);

            gameObject.name =
                arguments.Name.Trim();

            Undo.RegisterCreatedObjectUndo(
                gameObject,
                "Create GameObject");

            SceneManager.MoveGameObjectToScene(
                gameObject,
                scene);

            Transform parent =
                null;

            if (arguments.ParentGameObjectInstanceId != 0)
            {
                GameObject parentObject =
                    EditorCapabilityUtility.ResolveGameObject(
                        arguments.ParentGameObjectInstanceId,
                        null,
                        false);

                if (parentObject.scene != scene)
                {
                    throw new InvalidOperationException(
                        "The requested parent belongs to a different scene.");
                }

                parent =
                    parentObject.transform;

                gameObject.transform.SetParent(
                    parent,
                    false);
            }

            gameObject.transform.localPosition =
                arguments.Position?.ToVector3() ??
                Vector3.zero;

            gameObject.transform.localEulerAngles =
                arguments.Rotation?.ToVector3() ??
                Vector3.zero;

            gameObject.transform.localScale =
                arguments.Scale?.ToVector3() ??
                Vector3.one;

            EditorSceneManager.MarkSceneDirty(scene);

            bool saved =
                !arguments.SaveScene ||
                EditorSceneManager.SaveScene(scene);

            if (!saved)
            {
                throw new InvalidOperationException(
                    $"GameObject was created, but Unity could not save scene '{scene.path}'.");
            }

            if (arguments.SelectCreatedObject)
            {
                Selection.activeGameObject =
                    gameObject;
            }

            CreateGameObjectResult result =
                new CreateGameObjectResult
                {
                    Succeeded = true,
                    GameObjectInstanceId = gameObject.GetInstanceID(),
                    GlobalObjectId = UnityEditor.GlobalObjectId
                        .GetGlobalObjectIdSlow(gameObject)
                        .ToString(),
                    Name = gameObject.name,
                    ObjectType = NormalizeObjectType(arguments.ObjectType),
                    ScenePath = scene.path.Replace('\\', '/'),
                    HierarchyPath = GetHierarchyPath(gameObject.transform),
                    SceneSaved = arguments.SaveScene
                };

            foreach (Component component in gameObject.GetComponents<Component>())
            {
                result.Components.Add(
                    component == null
                        ? "[Missing Script]"
                        : component.GetType().FullName);
            }

            return Task.FromResult<object>(result);
        }


        private static GameObject CreateObject(
            string requestedType,
            IEnumerable<string> requestedComponentTypes)
        {
            string objectType = NormalizeObjectType(requestedType);
            Type[] componentTypes =
                (requestedComponentTypes ?? Enumerable.Empty<string>())
                .Where(value => !string.IsNullOrWhiteSpace(value))
                .Select(ResolveComponentType)
                .Distinct()
                .ToArray();

            GameObject result;

            switch (objectType)
            {
                case "Empty":
                    result = componentTypes.Length == 0
                        ? new GameObject()
                        : new GameObject("New GameObject", componentTypes);
                    break;
                case "Cube":
                    result = GameObject.CreatePrimitive(PrimitiveType.Cube);
                    break;
                case "Sphere":
                    result = GameObject.CreatePrimitive(PrimitiveType.Sphere);
                    break;
                case "Capsule":
                    result = GameObject.CreatePrimitive(PrimitiveType.Capsule);
                    break;
                case "Cylinder":
                    result = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
                    break;
                case "Plane":
                    result = GameObject.CreatePrimitive(PrimitiveType.Plane);
                    break;
                case "Quad":
                    result = GameObject.CreatePrimitive(PrimitiveType.Quad);
                    break;
                case "Camera":
                {
                    result = new GameObject();
                    result.AddComponent<Camera>();
                    break;
                }
                case "DirectionalLight":
                {
                    result = new GameObject();
                    Light light = result.AddComponent<Light>();
                    light.type = LightType.Directional;
                    break;
                }
                default:
                    throw new ArgumentException(
                        $"Unsupported ObjectType '{requestedType}'. Use Empty, Cube, Sphere, Capsule, Cylinder, Plane, Quad, Camera, or DirectionalLight.");
            }

            if (objectType != "Empty")
            {
                foreach (Type componentType in componentTypes)
                {
                    if (typeof(Transform).IsAssignableFrom(componentType))
                    {
                        throw new InvalidOperationException(
                            $"Component type '{componentType.FullName}' must be supplied while constructing an Empty GameObject because Unity cannot replace a primitive's Transform.");
                    }

                    if (result.GetComponent(componentType) == null)
                        result.AddComponent(componentType);
                }
            }

            return result;
        }


        private static Type ResolveComponentType(
            string requestedType)
        {
            string normalized = requestedType?.Trim();
            Type type = Type.GetType(normalized ?? string.Empty, false);

            if (type == null)
            {
                type = AppDomain.CurrentDomain.GetAssemblies()
                    .Select(assembly => assembly.GetType(normalized ?? string.Empty, false))
                    .FirstOrDefault(candidate => candidate != null);
            }

            if (type == null && !string.IsNullOrWhiteSpace(normalized))
            {
                type = AppDomain.CurrentDomain.GetAssemblies()
                    .SelectMany(assembly =>
                    {
                        try { return assembly.GetTypes(); }
                        catch (ReflectionTypeLoadException exception)
                        {
                            return exception.Types.Where(candidate => candidate != null);
                        }
                    })
                    .FirstOrDefault(candidate =>
                        string.Equals(candidate.Name, normalized, StringComparison.Ordinal));
            }

            if (type == null || !typeof(Component).IsAssignableFrom(type) ||
                type.IsAbstract || type.ContainsGenericParameters)
            {
                throw new ArgumentException(
                    $"Component type '{requestedType}' is not a concrete Unity Component type.");
            }

            return type;
        }


        private static string NormalizeObjectType(
            string value)
        {
            string compact =
                (value ?? "Empty")
                .Replace(" ", string.Empty)
                .Trim();

            if (string.Equals(compact, "directionallight", StringComparison.OrdinalIgnoreCase))
                return "DirectionalLight";

            foreach (string known in new[] { "Empty", "Cube", "Sphere", "Capsule", "Cylinder", "Plane", "Quad", "Camera" })
            {
                if (string.Equals(compact, known, StringComparison.OrdinalIgnoreCase))
                    return known;
            }

            return compact;
        }


        private static string GetHierarchyPath(
            Transform transform)
        {
            string path =
                transform.name;

            while (transform.parent != null)
            {
                transform = transform.parent;
                path = transform.name + "/" + path;
            }

            return path;
        }
    }


    [Serializable]
    public sealed class SaveEditorSceneArguments
    {
        [Description("Optional loaded scene path. When omitted, the active scene is saved.")]
        public string ScenePath { get; set; }

        [Description("Required exact Assets/.../.unity destination only when the selected loaded scene is untitled. This persists that scene directly without opening Unity's Save As dialog. Existing scene assets are never overwritten through this field.")]
        public string DestinationScenePath { get; set; }
    }


    [Serializable]
    internal sealed class SaveEditorSceneResult
    {
        public bool Succeeded;

        public string ScenePath;

        public bool Saved;

        public bool DirtyAfterSave;

        public int RootCount;

        public bool PersistenceVerified;

        public bool PersistedUntitledScene;

        public List<string> PersistedGameObjectPaths =
            new List<string>();
    }


    public sealed class SaveEditorSceneCapability :
        CapabilityBase<SaveEditorSceneArguments>
    {
        public override string Name =>
            "save_editor_scene";


        public override string Description =>
            "Saves a loaded Unity scene and reports its persisted state. An untitled active scene can be persisted noninteractively by passing its exact unused Assets/.../.unity destinationScenePath.";


        public override CapabilityPermission Permission =>
            CapabilityPermission.Write;


        protected override Task<object> ExecuteAsync(
            SaveEditorSceneArguments arguments,
            CapabilityContext context)
        {
            context.CancellationToken.ThrowIfCancellationRequested();

            Scene scene =
                EditorSceneAuthoringUtility.ResolveLoadedScene(
                    arguments?.ScenePath);

            bool persistedUntitledScene =
                string.IsNullOrWhiteSpace(
                    scene.path);

            string destinationScenePath =
                string.IsNullOrWhiteSpace(
                    arguments?.DestinationScenePath)
                    ? string.Empty
                    : EditorSceneAuthoringUtility.ValidateScenePath(
                        arguments.DestinationScenePath);

            if (string.IsNullOrWhiteSpace(scene.path))
            {
                if (string.IsNullOrWhiteSpace(
                        destinationScenePath))
                {
                    throw new InvalidOperationException(
                        "The target scene has no asset path. Pass the exact unused Assets/.../.unity path as destinationScenePath to persist this loaded untitled scene without opening Unity's Save As dialog.");
                }

                if (EditorSceneAuthoringUtility.SceneAssetExists(
                        destinationScenePath))
                {
                    throw new InvalidOperationException(
                        $"Destination scene already exists: '{destinationScenePath}'. AIRuntime will not overwrite it while persisting an untitled scene. Open that scene explicitly or choose the card's unused declared scene path.");
                }

                EditorSceneAuthoringUtility.EnsureAssetFolder(
                    EditorSceneAuthoringUtility.GetParentAssetPath(
                        destinationScenePath));
            }
            else if (!string.IsNullOrWhiteSpace(
                         destinationScenePath) &&
                     !string.Equals(
                         scene.path.Replace('\\', '/'),
                         destinationScenePath,
                         StringComparison.OrdinalIgnoreCase))
            {
                throw new InvalidOperationException(
                    "destinationScenePath may only persist an untitled scene. The selected scene already has an asset path; save it in place by omitting destinationScenePath.");
            }

            bool saved =
                persistedUntitledScene
                    ? EditorSceneManager.SaveScene(
                        scene,
                        destinationScenePath,
                        false)
                    : EditorSceneManager.SaveScene(scene);

            if (!saved)
            {
                throw new InvalidOperationException(
                    $"Unity could not save scene '{scene.path}'.");
            }

            AssetDatabase.SaveAssets();

            string normalizedScenePath =
                scene.path.Replace('\\', '/');

            List<string> persistedGameObjectPaths =
                GetGameObjectPaths(
                    scene);

            return Task.FromResult<object>(
                new SaveEditorSceneResult
                {
                    Succeeded = true,
                    ScenePath = normalizedScenePath,
                    Saved = true,
                    DirtyAfterSave = scene.isDirty,
                    RootCount = scene.GetRootGameObjects().Length,
                    PersistenceVerified =
                        !scene.isDirty &&
                        AssetDatabase.LoadAssetAtPath<SceneAsset>(
                            normalizedScenePath) != null,
                    PersistedUntitledScene =
                        persistedUntitledScene,
                    PersistedGameObjectPaths =
                        persistedGameObjectPaths
                });
        }


        private static List<string> GetGameObjectPaths(
            Scene scene)
        {
            List<string> paths =
                new List<string>();

            foreach (GameObject root in scene.GetRootGameObjects())
            {
                AddGameObjectPaths(
                    root.transform,
                    root.name,
                    paths);
            }

            return paths;
        }


        private static void AddGameObjectPaths(
            Transform transform,
            string path,
            ICollection<string> paths)
        {
            paths.Add(
                path);

            for (int index = 0;
                 index < transform.childCount;
                 index++)
            {
                Transform child =
                    transform.GetChild(
                        index);

                AddGameObjectPaths(
                    child,
                    path + "/" + child.name,
                    paths);
            }
        }
    }


    [Serializable]
    public sealed class CreatePrefabArguments
    {
        [Description("Exact live source GameObject instance ID. When omitted, the active GameObject selection is used.")]
        public int SourceGameObjectInstanceId { get; set; }

        [Description("Optional persistent GlobalObjectId for the source GameObject. Use this with the instance ID when available so a reload can recover the source safely.")]
        public string SourceGlobalObjectId { get; set; }

        [Required]
        [Description("Contained Unity asset path under Assets/ ending in .prefab.")]
        public string AssetPath { get; set; }

        [DefaultValue(true)]
        [Description("Replace an existing prefab asset at the exact path. When false, an existing asset is rejected.")]
        public bool ReplaceExisting { get; set; } = true;

        [DefaultValue(false)]
        [Description("Connect the source scene GameObject to the saved prefab. Leave false to create or update the prefab without changing the source object's prefab relationship.")]
        public bool ConnectSource { get; set; }

        [DefaultValue(true)]
        [Description("Save the source scene after connecting it to the prefab.")]
        public bool SaveSourceScene { get; set; } = true;
    }


    [Serializable]
    internal sealed class CreatePrefabResult
    {
        public bool Succeeded;
        public bool Created;
        public bool Updated;
        public bool SourceConnected;
        public bool SourceSceneSaved;
        public string AssetPath;
        public string AssetGuid;
        public string RootName;
        public int PrefabAssetInstanceId;
        public string PrefabAssetType;
        public List<string> Components = new List<string>();
        public string ResponseInstruction =
            "The prefab was persisted through Unity's PrefabUtility. Use inspect_prefab with AssetPath to verify its source, type, and structure before completing the card.";
    }


    public sealed class CreatePrefabCapability :
        CapabilityBase<CreatePrefabArguments>
    {
        public override string Name => "create_prefab";

        public override string Description =>
            "Creates or updates a real Unity prefab asset from a live scene GameObject through PrefabUtility, optionally connecting the source instance.";

        public override CapabilityPermission Permission =>
            CapabilityPermission.Write;


        protected override Task<object> ExecuteAsync(
            CreatePrefabArguments arguments,
            CapabilityContext context)
        {
            context.CancellationToken.ThrowIfCancellationRequested();

            if (arguments == null)
                throw new ArgumentNullException(nameof(arguments));

            string assetPath = ValidatePrefabPath(arguments.AssetPath);
            GameObject source = EditorCapabilityUtility.ResolveGameObject(
                arguments.SourceGameObjectInstanceId,
                arguments.SourceGlobalObjectId,
                useActiveSelection: true);

            if (EditorUtility.IsPersistent(source))
            {
                throw new InvalidOperationException(
                    "The source must be a live scene GameObject, not an existing prefab asset. Instantiate or select a scene object first.");
            }

            UnityEngine.Object existing =
                AssetDatabase.LoadMainAssetAtPath(assetPath);
            bool created = existing == null;

            if (!created &&
                (!(existing is GameObject existingGameObject) ||
                 !PrefabUtility.IsPartOfPrefabAsset(existingGameObject)))
            {
                throw new InvalidOperationException(
                    $"An asset exists at '{assetPath}', but it is not a prefab and will not be overwritten.");
            }

            if (!created && !arguments.ReplaceExisting)
            {
                throw new InvalidOperationException(
                    $"A prefab already exists at '{assetPath}'. Set ReplaceExisting=true to update it.");
            }

            EditorSceneAuthoringUtility.EnsureAssetFolder(
                EditorSceneAuthoringUtility.GetParentAssetPath(assetPath));

            GameObject persisted;
            bool sourceSceneSaved = false;

            if (arguments.ConnectSource)
            {
                persisted = PrefabUtility.SaveAsPrefabAssetAndConnect(
                    source,
                    assetPath,
                    InteractionMode.AutomatedAction);

                if (persisted != null &&
                    arguments.SaveSourceScene &&
                    source.scene.IsValid() &&
                    !string.IsNullOrWhiteSpace(source.scene.path))
                {
                    sourceSceneSaved =
                        EditorSceneManager.SaveScene(source.scene);
                }
            }
            else
            {
                persisted = PrefabUtility.SaveAsPrefabAsset(
                    source,
                    assetPath,
                    out bool savedSuccessfully);

                if (!savedSuccessfully)
                    persisted = null;
            }

            AssetDatabase.SaveAssets();
            AssetDatabase.ImportAsset(
                assetPath,
                ImportAssetOptions.ForceSynchronousImport |
                ImportAssetOptions.ForceUpdate);

            GameObject verified =
                AssetDatabase.LoadAssetAtPath<GameObject>(assetPath);

            if (persisted == null ||
                verified == null ||
                !PrefabUtility.IsPartOfPrefabAsset(verified))
            {
                throw new InvalidOperationException(
                    $"Unity did not persist a valid prefab at '{assetPath}'.");
            }

            return Task.FromResult<object>(
                new CreatePrefabResult
                {
                    Succeeded = true,
                    Created = created,
                    Updated = !created,
                    SourceConnected =
                        PrefabUtility.IsPartOfPrefabInstance(source),
                    SourceSceneSaved = sourceSceneSaved,
                    AssetPath = assetPath,
                    AssetGuid = AssetDatabase.AssetPathToGUID(assetPath),
                    RootName = verified.name,
                    PrefabAssetInstanceId = verified.GetInstanceID(),
                    PrefabAssetType =
                        PrefabUtility.GetPrefabAssetType(verified).ToString(),
                    Components = verified
                        .GetComponents<Component>()
                        .Select(component =>
                            component == null
                                ? "[Missing Script]"
                                : component.GetType().FullName)
                        .ToList()
                });
        }


        private static string ValidatePrefabPath(
            string path)
        {
            string normalized =
                (path ?? string.Empty)
                .Trim()
                .Replace('\\', '/');

            if (!normalized.StartsWith("Assets/", StringComparison.Ordinal) ||
                !normalized.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase) ||
                normalized.Contains("../") ||
                normalized.Contains("/../"))
            {
                throw new ArgumentException(
                    "AssetPath must be a contained Unity asset path under Assets/ and end in .prefab.");
            }

            return normalized;
        }
    }


    [Serializable]
    public sealed class EditorColorArguments
    {
        public float R { get; set; }
        public float G { get; set; }
        public float B { get; set; }

        [DefaultValue(1f)]
        public float A { get; set; } = 1f;

        internal Color ToColor() => new Color(R, G, B, A);
    }


    [Serializable]
    public sealed class CreateMaterialArguments
    {
        [Required]
        [Description("Contained Unity asset path under Assets/ ending in .mat.")]
        public string AssetPath { get; set; }

        [DefaultValue("Standard")]
        [Description("Exact Unity shader name. Defaults to Standard.")]
        public string ShaderName { get; set; } = "Standard";

        [Description("Optional material color. The shader must expose _BaseColor or _Color.")]
        public EditorColorArguments Color { get; set; }

        [DefaultValue(true)]
        [Description("Reuse and update an existing Material at the same path.")]
        public bool ReuseIfExists { get; set; } = true;
    }


    [Serializable]
    internal sealed class CreateMaterialResult
    {
        public bool Succeeded;
        public bool Created;
        public string AssetPath;
        public string Name;
        public string ShaderName;
        public bool ColorApplied;
        public EditorColorArguments Color;
        public int InstanceId;

        public string ResponseInstruction =
            "The material now exists at AssetPath. Use that exact AssetPath as an object-reference value only after this result reports Succeeded=true.";
    }


    public sealed class CreateMaterialCapability :
        CapabilityBase<CreateMaterialArguments>
    {
        public override string Name => "create_material";

        public override string Description =>
            "Creates or reuses a Unity Material asset with an exact shader and optional color, making it safe to assign through set_serialized_property.";

        public override CapabilityPermission Permission =>
            CapabilityPermission.Write;


        protected override Task<object> ExecuteAsync(
            CreateMaterialArguments arguments,
            CapabilityContext context)
        {
            context.CancellationToken.ThrowIfCancellationRequested();

            if (arguments == null)
                throw new ArgumentNullException(nameof(arguments));

            string assetPath = ValidateMaterialPath(arguments.AssetPath);
            string shaderName = string.IsNullOrWhiteSpace(arguments.ShaderName)
                ? "Standard"
                : arguments.ShaderName.Trim();

            Material material = AssetDatabase.LoadAssetAtPath<Material>(assetPath);
            bool created = material == null;

            if (!created && !arguments.ReuseIfExists)
                throw new InvalidOperationException($"A material already exists at '{assetPath}'.");

            Shader shader = Shader.Find(shaderName);
            if (shader == null)
                throw new InvalidOperationException($"Unity shader '{shaderName}' was not found. Use an exact installed shader name.");

            if (created)
            {
                EditorSceneAuthoringUtility.EnsureAssetFolder(
                    EditorSceneAuthoringUtility.GetParentAssetPath(assetPath));

                material = new Material(shader)
                {
                    name = System.IO.Path.GetFileNameWithoutExtension(assetPath)
                };

                AssetDatabase.CreateAsset(material, assetPath);
            }
            else if (material.shader != shader)
            {
                Undo.RecordObject(material, "Change material shader");
                material.shader = shader;
            }

            bool colorApplied = false;
            if (arguments.Color != null)
            {
                string colorProperty = material.HasProperty("_BaseColor")
                    ? "_BaseColor"
                    : material.HasProperty("_Color")
                        ? "_Color"
                        : null;

                if (colorProperty == null)
                    throw new InvalidOperationException($"Shader '{shaderName}' does not expose _BaseColor or _Color.");

                if (!created)
                    Undo.RecordObject(material, "Change material color");

                material.SetColor(colorProperty, arguments.Color.ToColor());
                colorApplied = true;
            }

            EditorUtility.SetDirty(material);
            AssetDatabase.SaveAssets();
            AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceUpdate);

            Material verified = AssetDatabase.LoadAssetAtPath<Material>(assetPath);
            if (verified == null)
                throw new InvalidOperationException($"Unity did not persist the material at '{assetPath}'.");

            return Task.FromResult<object>(new CreateMaterialResult
            {
                Succeeded = true,
                Created = created,
                AssetPath = assetPath,
                Name = verified.name,
                ShaderName = verified.shader != null ? verified.shader.name : string.Empty,
                ColorApplied = colorApplied,
                Color = arguments.Color,
                InstanceId = verified.GetInstanceID()
            });
        }


        private static string ValidateMaterialPath(string path)
        {
            string normalized = (path ?? string.Empty).Trim().Replace('\\', '/');

            if (!normalized.StartsWith("Assets/", StringComparison.Ordinal) ||
                !normalized.EndsWith(".mat", StringComparison.OrdinalIgnoreCase) ||
                normalized.Contains("../") ||
                normalized.Contains("/../"))
            {
                throw new ArgumentException(
                    "AssetPath must be a contained Unity asset path under Assets/ and end in .mat.");
            }

            return normalized;
        }
    }


    internal static class EditorSceneAuthoringUtility
    {
        internal static string ValidateScenePath(
            string scenePath)
        {
            string normalized =
                (scenePath ?? string.Empty)
                .Trim()
                .Replace('\\', '/');

            if (!normalized.StartsWith("Assets/", StringComparison.Ordinal) ||
                !normalized.EndsWith(".unity", StringComparison.OrdinalIgnoreCase) ||
                normalized.Contains("../") ||
                normalized.Contains("/../"))
            {
                throw new ArgumentException(
                    "ScenePath must be a contained Unity asset path under Assets/ and end in .unity.");
            }

            return normalized;
        }


        internal static string GetParentAssetPath(
            string assetPath)
        {
            int slash = assetPath.LastIndexOf('/');
            return slash > 0
                ? assetPath.Substring(0, slash)
                : "Assets";
        }


        internal static bool SceneAssetExists(
            string scenePath)
        {
            string normalized =
                ValidateScenePath(
                    scenePath);

            if (AssetDatabase.LoadAssetAtPath<SceneAsset>(
                    normalized) != null)
            {
                return true;
            }

            string projectRoot =
                Path.GetFullPath(
                    Path.Combine(
                        Application.dataPath,
                        ".."));

            string physicalPath =
                Path.Combine(
                    projectRoot,
                    normalized.Replace(
                        '/',
                        Path.DirectorySeparatorChar));

            return File.Exists(
                physicalPath);
        }


        internal static void EnsureAssetFolder(
            string folderPath)
        {
            string normalized =
                (folderPath ?? "Assets")
                .Replace('\\', '/')
                .TrimEnd('/');

            if (string.Equals(normalized, "Assets", StringComparison.Ordinal))
                return;

            string[] segments =
                normalized.Split('/');

            if (segments.Length == 0 ||
                !string.Equals(segments[0], "Assets", StringComparison.Ordinal))
            {
                throw new ArgumentException("Asset folders must be under Assets/.");
            }

            string parent =
                "Assets";

            for (int index = 1; index < segments.Length; index++)
            {
                string current =
                    parent + "/" + segments[index];

                if (!AssetDatabase.IsValidFolder(current))
                {
                    string projectRoot =
                        Path.GetFullPath(
                            Path.Combine(
                                Application.dataPath,
                                ".."));

                    string physicalPath =
                        Path.Combine(
                            projectRoot,
                            current.Replace(
                                '/',
                                Path.DirectorySeparatorChar));

                    if (Directory.Exists(
                            physicalPath))
                    {
                        // create_directory writes through the workspace file
                        // system. Unity may not have imported that directory
                        // yet; calling CreateFolder during this window makes
                        // Unity generate "Name 1", "Name 2", etc. Synchronize
                        // the real directory instead of creating a collision.
                        AssetDatabase.ImportAsset(
                            current,
                            ImportAssetOptions.ForceSynchronousImport);

                        if (!AssetDatabase.IsValidFolder(
                                current))
                        {
                            AssetDatabase.Refresh(
                                ImportAssetOptions.ForceSynchronousImport);
                        }

                        if (!AssetDatabase.IsValidFolder(
                                current))
                        {
                            throw new InvalidOperationException(
                                $"Asset folder '{current}' exists on disk but Unity could not import it. No duplicate folder was created.");
                        }
                    }
                    else
                    {
                        string guid =
                            AssetDatabase.CreateFolder(
                                parent,
                                segments[index]);

                        if (string.IsNullOrWhiteSpace(guid))
                        {
                            throw new InvalidOperationException(
                                $"Unity could not create asset folder '{current}'.");
                        }
                    }
                }

                parent =
                    current;
            }
        }


        internal static Scene FindLoadedScene(
            string scenePath)
        {
            string normalized =
                (scenePath ?? string.Empty)
                .Replace('\\', '/');

            for (int index = 0; index < SceneManager.sceneCount; index++)
            {
                Scene candidate =
                    SceneManager.GetSceneAt(index);

                if (string.Equals(
                        candidate.path.Replace('\\', '/'),
                        normalized,
                        StringComparison.OrdinalIgnoreCase))
                {
                    return candidate;
                }
            }

            return default(Scene);
        }


        internal static string ResolveUniqueExistingSceneAssetPath(
            string requestedScenePath)
        {
            string normalized =
                (requestedScenePath ?? string.Empty)
                .Trim()
                .Replace('\\', '/');

            if (string.IsNullOrWhiteSpace(
                    normalized))
            {
                return null;
            }

            if (AssetDatabase.LoadAssetAtPath<SceneAsset>(
                    normalized) !=
                null)
            {
                return normalized;
            }

            string requestedFileName =
                System.IO.Path.GetFileName(
                    normalized);

            string sceneName =
                System.IO.Path.GetFileNameWithoutExtension(
                    normalized);

            if (string.IsNullOrWhiteSpace(
                    requestedFileName) ||
                string.IsNullOrWhiteSpace(
                    sceneName))
            {
                return null;
            }

            string[] matches =
                AssetDatabase.GetAllAssetPaths()
                    .Where(
                        path =>
                            path.EndsWith(
                                ".unity",
                                StringComparison.OrdinalIgnoreCase) &&
                            string.Equals(
                                System.IO.Path.GetFileName(path),
                                requestedFileName,
                                StringComparison.OrdinalIgnoreCase))
                    .Concat(
                        FindScenePathsOnDisk(
                            requestedFileName))
                    .Distinct(
                        StringComparer.OrdinalIgnoreCase)
                    .ToArray();

            return matches.Length ==
                1
                ? matches[0].Replace('\\', '/')
                : null;
        }


        private static IEnumerable<string> FindScenePathsOnDisk(
            string requestedFileName)
        {
            string assetsRoot =
                Application.dataPath;

            if (!System.IO.Directory.Exists(
                    assetsRoot))
            {
                yield break;
            }

            string[] files;

            try
            {
                files =
                    System.IO.Directory.GetFiles(
                        assetsRoot,
                        requestedFileName,
                        System.IO.SearchOption.AllDirectories);
            }
            catch (
                System.IO.IOException)
            {
                yield break;
            }
            catch (
                UnauthorizedAccessException)
            {
                yield break;
            }

            foreach (string file in files)
            {
                string relative =
                    file.Substring(
                            assetsRoot.Length)
                        .TrimStart(
                            System.IO.Path.DirectorySeparatorChar,
                            System.IO.Path.AltDirectorySeparatorChar)
                        .Replace('\\', '/');

                yield return
                    "Assets/" + relative;
            }
        }


        internal static Scene ResolveLoadedScene(
            string scenePath)
        {
            if (string.IsNullOrWhiteSpace(scenePath))
            {
                Scene active =
                    SceneManager.GetActiveScene();

                if (!active.IsValid() || !active.isLoaded)
                    throw new InvalidOperationException("There is no active loaded Editor scene.");

                return active;
            }

            string requested =
                scenePath.Trim().Replace('\\', '/');

            for (int index = 0; index < SceneManager.sceneCount; index++)
            {
                Scene candidate =
                    SceneManager.GetSceneAt(index);

                if (string.Equals(candidate.name, requested, StringComparison.Ordinal) ||
                    string.Equals(candidate.path.Replace('\\', '/'), requested, StringComparison.OrdinalIgnoreCase))
                {
                    if (!candidate.isLoaded)
                    {
                        throw new InvalidOperationException(
                            $"Scene '{requested}' exists in the Editor but is not loaded.");
                    }

                    return candidate;
                }
            }

            // Preview and stage scenes are valid loaded Editor scenes but are
            // not included in SceneManager.sceneCount. Resolve their exact
            // names through their live GameObjects before interpreting the
            // input as an asset path.
            foreach (
                GameObject editorObject
                in Resources.FindObjectsOfTypeAll<GameObject>())
            {
                if (editorObject == null)
                    continue;

                Scene candidate =
                    editorObject.scene;

                if (candidate.IsValid() &&
                    candidate.isLoaded &&
                    string.Equals(
                        candidate.name,
                        requested,
                        StringComparison.Ordinal))
                {
                    return candidate;
                }
            }

            string normalized =
                ValidateScenePath(scenePath);

            normalized =
                ResolveUniqueExistingSceneAssetPath(
                    normalized) ??
                normalized;

            Scene loaded =
                FindLoadedScene(normalized);

            if (!loaded.IsValid() || !loaded.isLoaded)
            {
                throw new InvalidOperationException(
                    $"Scene '{normalized}' is not loaded. Call create_editor_scene first.");
            }

            return loaded;
        }


        internal static void EnsureSceneCanBeSavedNonInteractively(
            Scene scene)
        {
            if (!scene.IsValid() ||
                !scene.isLoaded)
            {
                throw new InvalidOperationException(
                    "The target scene is not a valid loaded Editor scene.");
            }

            if (string.IsNullOrWhiteSpace(
                    scene.path))
            {
                throw new InvalidOperationException(
                    "The target scene has no asset path. Automated capabilities cannot open Unity's Save As dialog. Create or open a persisted scene and pass its exact scenePath, or retry with saveScene=false.");
            }
        }
    }
}
