using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;

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

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;


namespace AIRuntime.Editor
{
    [Serializable]
    public sealed class FindUnityAssetsArguments
    {
        [Description("Unity AssetDatabase search filter, for example t:Prefab Player.")]
        public string Filter { get; set; }
        [Description("Optional Assets/... or Packages/... folders. Defaults to the complete AssetDatabase.")]
        public List<string> SearchFolders { get; set; } = new List<string>();
        [DefaultValue(200)] public int MaximumResults { get; set; } = 200;
    }


    public sealed class FindUnityAssetsCapability : CapabilityBase<FindUnityAssetsArguments>
    {
        public override string Name => "find_unity_assets";
        public override string Description =>
            "Finds Unity assets through AssetDatabase and returns stable GUIDs, paths, main types, labels, and package/write scope.";
        public override CapabilityPermission Permission => CapabilityPermission.ReadOnly;

        protected override Task<object> ExecuteAsync(FindUnityAssetsArguments arguments, CapabilityContext context)
        {
            context.CancellationToken.ThrowIfCancellationRequested();
            arguments = arguments ?? new FindUnityAssetsArguments();
            string[] folders = (arguments.SearchFolders ?? new List<string>())
                .Where(value => !string.IsNullOrWhiteSpace(value))
                .Select(SubstrateAssetUtility.ValidateSearchFolder)
                .Distinct(StringComparer.Ordinal)
                .ToArray();
            string[] guids = folders.Length == 0
                ? AssetDatabase.FindAssets(arguments.Filter ?? string.Empty)
                : AssetDatabase.FindAssets(arguments.Filter ?? string.Empty, folders);
            int maximum = Math.Max(1, Math.Min(5000, arguments.MaximumResults));
            var assets = guids.Take(maximum).Select(SubstrateAssetUtility.DescribeAsset).ToArray();
            return Task.FromResult<object>(new
            {
                Succeeded = true,
                TotalMatches = guids.Length,
                Truncated = guids.Length > maximum,
                Assets = assets
            });
        }
    }


    [Serializable]
    public sealed class InspectUnityAssetArguments
    {
        public string AssetPath { get; set; }
        public string AssetGuid { get; set; }
        [DefaultValue(true)] public bool IncludeDependencies { get; set; } = true;
        [DefaultValue(false)] public bool RecursiveDependencies { get; set; }
        [DefaultValue(true)] public bool IncludeSubAssets { get; set; } = true;
    }


    public sealed class InspectUnityAssetCapability : CapabilityBase<InspectUnityAssetArguments>
    {
        public override string Name => "inspect_unity_asset";
        public override string Description =>
            "Inspects one exact Unity asset by path or GUID, including identity, type, labels, importer, sub-assets, and AssetDatabase dependencies.";
        public override CapabilityPermission Permission => CapabilityPermission.ReadOnly;

        protected override Task<object> ExecuteAsync(InspectUnityAssetArguments arguments, CapabilityContext context)
        {
            context.CancellationToken.ThrowIfCancellationRequested();
            string path = SubstrateAssetUtility.ResolveAssetPath(arguments?.AssetPath, arguments?.AssetGuid, true);
            UnityEngine.Object main = AssetDatabase.LoadMainAssetAtPath(path);
            string[] dependencies = arguments.IncludeDependencies
                ? AssetDatabase.GetDependencies(path, arguments.RecursiveDependencies)
                    .Where(value => !string.Equals(value, path, StringComparison.Ordinal)).ToArray()
                : Array.Empty<string>();
            object[] subAssets = arguments.IncludeSubAssets
                ? AssetDatabase.LoadAllAssetsAtPath(path)
                    .Where(value => value != null && value != main)
                    .Select(value => (object)new
                    {
                        value.name,
                        Type = value.GetType().FullName,
                        InstanceId = value.GetInstanceID()
                    }).ToArray()
                : Array.Empty<object>();
            AssetImporter importer = AssetImporter.GetAtPath(path);
            return Task.FromResult<object>(new
            {
                Succeeded = true,
                Asset = SubstrateAssetUtility.DescribeAsset(AssetDatabase.AssetPathToGUID(path)),
                ImporterType = importer?.GetType().FullName ?? string.Empty,
                Dependencies = dependencies.Select(SubstrateAssetUtility.DescribePath).ToArray(),
                SubAssets = subAssets
            });
        }
    }


    [Serializable]
    public sealed class GetUnityAssetReferencersArguments
    {
        public string AssetPath { get; set; }
        public string AssetGuid { get; set; }
        public List<string> SearchFolders { get; set; } = new List<string> { "Assets" };
        [DefaultValue(200)] public int MaximumResults { get; set; } = 200;
    }


    public sealed class GetUnityAssetReferencersCapability : CapabilityBase<GetUnityAssetReferencersArguments>
    {
        public override string Name => "get_unity_asset_referencers";
        public override string Description =>
            "Finds Unity assets whose direct AssetDatabase dependency set references an exact target asset. Results are bounded and GUID-aware.";
        public override CapabilityPermission Permission => CapabilityPermission.ReadOnly;

        protected override Task<object> ExecuteAsync(GetUnityAssetReferencersArguments arguments, CapabilityContext context)
        {
            context.CancellationToken.ThrowIfCancellationRequested();
            string target = SubstrateAssetUtility.ResolveAssetPath(arguments?.AssetPath, arguments?.AssetGuid, true);
            string[] roots = (arguments.SearchFolders ?? new List<string> { "Assets" })
                .Select(SubstrateAssetUtility.ValidateSearchFolder).Distinct(StringComparer.Ordinal).ToArray();
            int maximum = Math.Max(1, Math.Min(5000, arguments.MaximumResults));
            List<object> referencers = new List<object>();
            bool truncated = false;
            foreach (string candidate in AssetDatabase.GetAllAssetPaths())
            {
                context.CancellationToken.ThrowIfCancellationRequested();
                if (string.Equals(candidate, target, StringComparison.Ordinal) ||
                    !roots.Any(root => candidate == root || candidate.StartsWith(root + "/", StringComparison.Ordinal)))
                    continue;
                if (!AssetDatabase.GetDependencies(candidate, false).Contains(target)) continue;
                if (referencers.Count >= maximum) { truncated = true; break; }
                referencers.Add(SubstrateAssetUtility.DescribePath(candidate));
            }
            return Task.FromResult<object>(new
            {
                Succeeded = true,
                Target = SubstrateAssetUtility.DescribePath(target),
                ReferencerCount = referencers.Count,
                Truncated = truncated,
                Referencers = referencers
            });
        }
    }


    [Serializable]
    public sealed class TransferUnityAssetArguments
    {
        [Required] public string SourceAssetPath { get; set; }
        [Required] public string DestinationAssetPath { get; set; }
        [Required]
        [Description("Must exactly repeat SourceAssetPath as a current-state confirmation.")]
        public string ExpectedSourceAssetPath { get; set; }
    }


    public abstract class TransferUnityAssetCapabilityBase :
        CapabilityBase<TransferUnityAssetArguments>, ICapabilityAuthorizationSemantics
    {
        public override CapabilityPermission Permission => CapabilityPermission.Write;
        public IReadOnlyList<CapabilityPermission> RequiredPermissions =>
            new[] { CapabilityPermission.Write };
        public IReadOnlyList<string> EffectScopes => new[] { "workspace:Assets", "unity:asset-guid-lifecycle" };

        protected abstract bool Transfer(string source, string destination, out string error);

        protected override Task<object> ExecuteAsync(TransferUnityAssetArguments arguments, CapabilityContext context)
        {
            context.CancellationToken.ThrowIfCancellationRequested();
            string source = SubstrateAssetUtility.ValidateWritableAssetPath(arguments?.SourceAssetPath);
            string destination = SubstrateAssetUtility.ValidateWritableAssetPath(arguments?.DestinationAssetPath);
            if (!string.Equals(source, arguments.ExpectedSourceAssetPath?.Trim().Replace('\\', '/'), StringComparison.Ordinal))
                throw new InvalidOperationException("ExpectedSourceAssetPath does not match SourceAssetPath.");
            if (AssetDatabase.LoadMainAssetAtPath(source) == null)
                throw new FileNotFoundException($"Unity asset does not exist: '{source}'.", source);
            if (AssetDatabase.LoadMainAssetAtPath(destination) != null || File.Exists(SubstrateAssetUtility.AssetPathToFullPath(destination)))
                throw new IOException($"Destination asset already exists: '{destination}'.");
            SubstrateAssetUtility.EnsureAssetFolder(SubstrateAssetUtility.ParentAssetPath(destination));
            if (!Transfer(source, destination, out string error))
                throw new InvalidOperationException(error);
            AssetDatabase.SaveAssets();
            AssetDatabase.ImportAsset(destination, ImportAssetOptions.ForceSynchronousImport);
            return Task.FromResult<object>(new
            {
                Succeeded = true,
                SourceAssetPath = source,
                Destination = SubstrateAssetUtility.DescribePath(destination)
            });
        }
    }


    public sealed class MoveUnityAssetCapability : TransferUnityAssetCapabilityBase
    {
        public override string Name => "move_unity_asset";
        public override string Description => "Moves or renames a Unity asset through AssetDatabase while preserving its GUID and references.";
        protected override bool Transfer(string source, string destination, out string error)
        {
            error = AssetDatabase.MoveAsset(source, destination);
            return string.IsNullOrWhiteSpace(error);
        }
    }


    public sealed class CopyUnityAssetCapability : TransferUnityAssetCapabilityBase
    {
        public override string Name => "copy_unity_asset";
        public override string Description => "Copies a Unity asset through AssetDatabase so Unity creates and imports an independent GUID-safe asset.";
        protected override bool Transfer(string source, string destination, out string error)
        {
            bool result = AssetDatabase.CopyAsset(source, destination);
            error = result ? string.Empty : $"Unity could not copy '{source}' to '{destination}'.";
            return result;
        }
    }


    [Serializable]
    public sealed class DeleteUnityAssetArguments
    {
        [Required] public string AssetPath { get; set; }
        [Required] public string ExpectedAssetGuid { get; set; }
        [DefaultValue(false)]
        [Description("Must be true after get_unity_asset_referencers has established that deletion is intended despite references.")]
        public bool ConfirmReferencedDeletion { get; set; }
    }


    public sealed class DeleteUnityAssetCapability :
        CapabilityBase<DeleteUnityAssetArguments>, ICapabilityAuthorizationSemantics
    {
        public override string Name => "delete_unity_asset";
        public override string Description =>
            "Deletes an exact Unity asset and its metadata through AssetDatabase after GUID confirmation. Referenced deletion requires explicit confirmation.";
        public override CapabilityPermission Permission => CapabilityPermission.Destructive;
        public IReadOnlyList<CapabilityPermission> RequiredPermissions =>
            new[] { CapabilityPermission.Destructive, CapabilityPermission.Write };
        public IReadOnlyList<string> EffectScopes => new[] { "workspace:Assets", "unity:asset-guid-lifecycle" };

        protected override Task<object> ExecuteAsync(DeleteUnityAssetArguments arguments, CapabilityContext context)
        {
            context.CancellationToken.ThrowIfCancellationRequested();
            string path = SubstrateAssetUtility.ValidateWritableAssetPath(arguments?.AssetPath);
            string guid = AssetDatabase.AssetPathToGUID(path);
            if (string.IsNullOrWhiteSpace(guid)) throw new FileNotFoundException($"Unity asset does not exist: '{path}'.", path);
            if (!string.Equals(guid, arguments.ExpectedAssetGuid?.Trim(), StringComparison.OrdinalIgnoreCase))
                throw new InvalidOperationException($"Asset GUID confirmation mismatch. Current GUID is '{guid}'.");
            string firstReferencer = AssetDatabase.GetAllAssetPaths()
                .Where(candidate => candidate.StartsWith("Assets/", StringComparison.Ordinal) && candidate != path)
                .FirstOrDefault(candidate => AssetDatabase.GetDependencies(candidate, false).Contains(path));
            if (firstReferencer != null && !arguments.ConfirmReferencedDeletion)
                throw new InvalidOperationException($"Asset is referenced by '{firstReferencer}'. Inspect referencers and explicitly confirm referenced deletion.");
            if (!AssetDatabase.DeleteAsset(path)) throw new InvalidOperationException($"Unity could not delete '{path}'.");
            AssetDatabase.SaveAssets();
            return Task.FromResult<object>(new { Succeeded = true, DeletedAssetPath = path, DeletedAssetGuid = guid });
        }
    }


    [Serializable]
    public sealed class RefreshUnityAssetsArguments
    {
        public List<string> AssetPaths { get; set; } = new List<string>();
        [DefaultValue(false)] public bool ForceUpdate { get; set; }
    }


    public sealed class RefreshUnityAssetsCapability :
        CapabilityBase<RefreshUnityAssetsArguments>, ICapabilityAuthorizationSemantics
    {
        public override string Name => "refresh_unity_assets";
        public override string Description =>
            "Starts a durably journaled Unity asset import/refresh operation. Return immediately with OperationId, then poll get_unity_operation across compilation or domain reload.";
        public override CapabilityPermission Permission => CapabilityPermission.Write;
        public IReadOnlyList<CapabilityPermission> RequiredPermissions => new[] { CapabilityPermission.Write };
        public IReadOnlyList<string> EffectScopes => new[] { "unity:asset-import", "unity:compilation", "workspace:Library/AIRuntime" };

        protected override Task<object> ExecuteAsync(RefreshUnityAssetsArguments arguments, CapabilityContext context)
        {
            context.CancellationToken.ThrowIfCancellationRequested();
            arguments = arguments ?? new RefreshUnityAssetsArguments();
            string[] paths = (arguments.AssetPaths ?? new List<string>())
                .Where(value => !string.IsNullOrWhiteSpace(value))
                .Select(SubstrateAssetUtility.ValidateAnyAssetPath)
                .Distinct(StringComparer.Ordinal)
                .OrderBy(path => path, StringComparer.Ordinal)
                .ToArray();
            string inputFingerprint = UnityOperationJournal.ComputeAssetInputFingerprint(paths);
            UnityOperationRecord operation =
                arguments.ForceUpdate
                    ? UnityOperationJournal.TryFindReusable("AssetRefresh", paths, inputFingerprint)
                    : null;

            if (operation != null)
            {
                if (operation.State != "Succeeded" && operation.State != "Failed")
                    UnityOperationJournal.ScheduleReconciliation(operation.OperationId);
                return Task.FromResult<object>(operation);
            }

            operation = UnityOperationJournal.Start("AssetRefresh", paths, inputFingerprint);
            ImportAssetOptions options = arguments.ForceUpdate ? ImportAssetOptions.ForceUpdate : ImportAssetOptions.Default;
            if (paths.Length == 0) AssetDatabase.Refresh(options);
            else foreach (string path in paths) AssetDatabase.ImportAsset(path, options);
            UnityOperationJournal.ScheduleReconciliation(operation.OperationId);
            return Task.FromResult<object>(UnityOperationJournal.Read(operation.OperationId));
        }
    }


    [Serializable]
    public sealed class GetUnityOperationArguments
    {
        [Required] public string OperationId { get; set; }
    }


    public sealed class GetUnityOperationCapability : CapabilityBase<GetUnityOperationArguments>
    {
        public override string Name => "get_unity_operation";
        public override string Description =>
            "Returns durable status for a Unity import/compilation operation, including reload generation and current compiler diagnostics.";
        public override CapabilityPermission Permission => CapabilityPermission.ReadOnly;
        protected override Task<object> ExecuteAsync(GetUnityOperationArguments arguments, CapabilityContext context)
        {
            context.CancellationToken.ThrowIfCancellationRequested();
            return Task.FromResult<object>(UnityOperationJournal.Read(arguments?.OperationId));
        }
    }


    [Serializable]
    public sealed class EditSerializedCollectionArguments : EditorSerializedTargetArguments
    {
        [Required] public string PropertyPath { get; set; }
        [Required]
        [Description("Resize, Insert, Delete, Move, or SetElement.")]
        public string Action { get; set; }
        [DefaultValue(-1)] public int Index { get; set; } = -1;
        [DefaultValue(-1)] public int DestinationIndex { get; set; } = -1;
        [DefaultValue(-1)] public int NewSize { get; set; } = -1;
        [DefaultValue(-1)]
        [Description("Required optimistic-concurrency check. Must equal the currently observed array size.")]
        public int ExpectedSize { get; set; } = -1;
        [Description("JSON value for Insert or SetElement when the element type is supported by set_serialized_property.")]
        public string ValueJson { get; set; }
        [DefaultValue(false)] public bool SaveAssets { get; set; }
    }


    public sealed class EditSerializedCollectionCapability : CapabilityBase<EditSerializedCollectionArguments>
    {
        public override string Name => "edit_serialized_collection";
        public override string Description =>
            "Resizes, inserts, deletes, moves, or sets one element of an exact serialized array/list with an expected-size concurrency check, Undo, prefab recording, and verification.";
        public override CapabilityPermission Permission => CapabilityPermission.Write;

        protected override Task<object> ExecuteAsync(EditSerializedCollectionArguments arguments, CapabilityContext context)
        {
            context.CancellationToken.ThrowIfCancellationRequested();
            if (arguments == null || arguments.ExpectedSize < 0)
                throw new ArgumentException("Arguments and non-negative ExpectedSize are required.");
            UnityEngine.Object target = EditorCapabilityUtility.ResolveSerializedTarget(arguments);
            SerializedObject serialized = new SerializedObject(target);
            serialized.UpdateIfRequiredOrScript();
            SerializedProperty property = serialized.FindProperty(arguments.PropertyPath);
            if (property == null) throw new InvalidOperationException($"Serialized property '{arguments.PropertyPath}' was not found.");
            if (!property.isArray || property.propertyType == SerializedPropertyType.String)
                throw new InvalidOperationException($"Serialized property '{property.propertyPath}' is not an array or list.");
            if (!property.editable) throw new InvalidOperationException($"Serialized collection '{property.propertyPath}' is read-only.");
            if (property.arraySize != arguments.ExpectedSize)
                throw new InvalidOperationException($"Stale collection edit rejected. Expected size {arguments.ExpectedSize}, current size {property.arraySize}.");

            int previousSize = property.arraySize;
            string action = arguments.Action?.Trim();
            Undo.RecordObject(target, $"{action} {target.name}.{property.displayName}");
            if (string.Equals(action, "Resize", StringComparison.OrdinalIgnoreCase))
            {
                if (arguments.NewSize < 0 || arguments.NewSize > 100000) throw new ArgumentOutOfRangeException(nameof(arguments.NewSize));
                property.arraySize = arguments.NewSize;
            }
            else if (string.Equals(action, "Insert", StringComparison.OrdinalIgnoreCase))
            {
                int index = arguments.Index < 0 ? property.arraySize : arguments.Index;
                if (index < 0 || index > property.arraySize) throw new ArgumentOutOfRangeException(nameof(arguments.Index));
                property.InsertArrayElementAtIndex(index);
                if (arguments.ValueJson != null)
                    EditorCapabilityUtility.AssignSerializedValue(property.GetArrayElementAtIndex(index),
                        EditorCapabilityUtility.ParseInputValue(arguments.ValueJson));
            }
            else if (string.Equals(action, "Delete", StringComparison.OrdinalIgnoreCase))
            {
                RequireElementIndex(property, arguments.Index);
                int size = property.arraySize;
                property.DeleteArrayElementAtIndex(arguments.Index);
                if (property.arraySize == size) property.DeleteArrayElementAtIndex(arguments.Index);
            }
            else if (string.Equals(action, "Move", StringComparison.OrdinalIgnoreCase))
            {
                RequireElementIndex(property, arguments.Index);
                RequireElementIndex(property, arguments.DestinationIndex);
                property.MoveArrayElement(arguments.Index, arguments.DestinationIndex);
            }
            else if (string.Equals(action, "SetElement", StringComparison.OrdinalIgnoreCase))
            {
                RequireElementIndex(property, arguments.Index);
                if (arguments.ValueJson == null) throw new ArgumentException("ValueJson is required for SetElement.");
                EditorCapabilityUtility.AssignSerializedValue(property.GetArrayElementAtIndex(arguments.Index),
                    EditorCapabilityUtility.ParseInputValue(arguments.ValueJson));
            }
            else throw new ArgumentException("Action must be Resize, Insert, Delete, Move, or SetElement.");

            bool changed = serialized.ApplyModifiedProperties();
            EditorUtility.SetDirty(target);
            if (PrefabUtility.IsPartOfPrefabInstance(target))
                PrefabUtility.RecordPrefabInstancePropertyModifications(target);
            Component component = target as Component;
            if (component != null && component.gameObject.scene.IsValid())
                EditorSceneManager.MarkSceneDirty(component.gameObject.scene);
            if (arguments.SaveAssets && AssetDatabase.Contains(target)) AssetDatabase.SaveAssets();

            SerializedObject verification = new SerializedObject(target);
            verification.UpdateIfRequiredOrScript();
            SerializedProperty verified = verification.FindProperty(arguments.PropertyPath);
            int count = Math.Min(verified.arraySize, 256);
            JArray values = new JArray();
            for (int index = 0; index < count; index++)
                values.Add(EditorCapabilityUtility.ReadSerializedValue(verified.GetArrayElementAtIndex(index)));
            return Task.FromResult<object>(new
            {
                Succeeded = true,
                Changed = changed,
                ObjectInstanceId = target.GetInstanceID(),
                PropertyPath = arguments.PropertyPath,
                Action = action,
                PreviousSize = previousSize,
                CurrentSize = verified.arraySize,
                ElementsTruncated = verified.arraySize > count,
                Elements = values
            });
        }

        private static void RequireElementIndex(SerializedProperty property, int index)
        {
            if (index < 0 || index >= property.arraySize) throw new ArgumentOutOfRangeException(nameof(index));
        }
    }


    [Serializable]
    internal sealed class UnityOperationRecord
    {
        public string OperationId;
        public string Kind;
        public string State;
        public string[] AssetPaths;
        public string StartedUtc;
        public string UpdatedUtc;
        public int ReloadGenerationAtStart;
        public int ReloadGenerationObserved;
        public bool IsCompiling;
        public bool ScriptCompilationFailed;
        public int QuietObservationCount;
        public string InputFingerprint;
        public string Error;
    }


    [InitializeOnLoad]
    internal static class UnityOperationJournal
    {
        private const string ReloadGenerationKey = "AIRuntime.UnityOperationReloadGeneration";
        private static readonly string DirectoryPath = Path.Combine(Application.dataPath, "..", "Library", "AIRuntime", "UnityOperations");

        static UnityOperationJournal()
        {
            int generation = SessionState.GetInt(ReloadGenerationKey, 0) + 1;
            SessionState.SetInt(ReloadGenerationKey, generation);
            EditorApplication.delayCall += ReconcileAll;
        }

        internal static UnityOperationRecord Start(
            string kind,
            string[] paths,
            string inputFingerprint = null)
        {
            Directory.CreateDirectory(DirectoryPath);
            UnityOperationRecord record = new UnityOperationRecord
            {
                OperationId = Guid.NewGuid().ToString("N"),
                Kind = kind,
                State = "Pending",
                AssetPaths = paths ?? Array.Empty<string>(),
                StartedUtc = DateTime.UtcNow.ToString("O"),
                UpdatedUtc = DateTime.UtcNow.ToString("O"),
                ReloadGenerationAtStart = SessionState.GetInt(ReloadGenerationKey, 0),
                ReloadGenerationObserved = SessionState.GetInt(ReloadGenerationKey, 0),
                IsCompiling = EditorApplication.isCompiling,
                ScriptCompilationFailed = EditorUtility.scriptCompilationFailed,
                QuietObservationCount = 0,
                InputFingerprint = inputFingerprint ?? string.Empty,
                Error = string.Empty
            };
            Write(record);
            return record;
        }

        internal static UnityOperationRecord TryFindReusable(
            string kind,
            string[] paths,
            string inputFingerprint)
        {
            if (string.IsNullOrWhiteSpace(inputFingerprint) || !Directory.Exists(DirectoryPath))
                return null;

            DateTime reuseThreshold = DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(10));

            foreach (string file in Directory.EnumerateFiles(DirectoryPath, "*.json")
                         .OrderByDescending(File.GetLastWriteTimeUtc)
                         .Take(64))
            {
                try
                {
                    UnityOperationRecord candidate =
                        JsonConvert.DeserializeObject<UnityOperationRecord>(File.ReadAllText(file));

                    if (candidate == null ||
                        string.Equals(candidate.State, "Failed", StringComparison.Ordinal) ||
                        !string.Equals(candidate.Kind, kind, StringComparison.Ordinal) ||
                        !string.Equals(candidate.InputFingerprint, inputFingerprint, StringComparison.Ordinal) ||
                        !(candidate.AssetPaths ?? Array.Empty<string>())
                            .SequenceEqual(paths ?? Array.Empty<string>(), StringComparer.Ordinal) ||
                        !DateTime.TryParse(candidate.StartedUtc, out DateTime startedUtc) ||
                        startedUtc.ToUniversalTime() < reuseThreshold)
                    {
                        continue;
                    }

                    return candidate;
                }
                catch (Exception)
                {
                    // Ignore a malformed or concurrently replaced historical
                    // record and continue looking for a reusable operation.
                }
            }

            return null;
        }

        internal static string ComputeAssetInputFingerprint(IEnumerable<string> paths)
        {
            string projectRoot = Directory.GetParent(Application.dataPath)?.FullName;
            string[] normalized = (paths ?? Enumerable.Empty<string>())
                .Where(path => !string.IsNullOrWhiteSpace(path))
                .Select(path => path.Trim().Replace('\\', '/'))
                .Distinct(StringComparer.Ordinal)
                .OrderBy(path => path, StringComparer.Ordinal)
                .ToArray();

            if (string.IsNullOrWhiteSpace(projectRoot) || normalized.Length == 0)
                return string.Empty;

            StringBuilder input = new StringBuilder();

            using (SHA256 contentHash = SHA256.Create())
            {
                foreach (string path in normalized)
                {
                    string fullPath = Path.GetFullPath(
                        Path.Combine(projectRoot, path.Replace('/', Path.DirectorySeparatorChar)));

                    if (!File.Exists(fullPath))
                    {
                        // Missing and directory targets need a real refresh;
                        // they cannot be safely deduplicated by file content.
                        return string.Empty;
                    }

                    byte[] digest = contentHash.ComputeHash(File.ReadAllBytes(fullPath));
                    input.Append(path)
                        .Append(':')
                        .Append(Convert.ToBase64String(digest))
                        .Append('\n');
                }
            }

            using (SHA256 aggregateHash = SHA256.Create())
            {
                return Convert.ToBase64String(
                    aggregateHash.ComputeHash(Encoding.UTF8.GetBytes(input.ToString())));
            }
        }

        internal static void ScheduleReconciliation(string operationId)
        {
            EditorApplication.delayCall += () => Reconcile(operationId);
        }

        internal static UnityOperationRecord Read(string operationId)
        {
            if (string.IsNullOrWhiteSpace(operationId) || operationId.Any(character => !char.IsLetterOrDigit(character)))
                throw new ArgumentException("OperationId must be an alphanumeric journal identity.");
            string path = RecordPath(operationId);
            if (!File.Exists(path)) throw new FileNotFoundException($"Unity operation '{operationId}' does not exist.");
            return JsonConvert.DeserializeObject<UnityOperationRecord>(File.ReadAllText(path));
        }

        private static void ReconcileAll()
        {
            if (!Directory.Exists(DirectoryPath)) return;
            foreach (string file in Directory.EnumerateFiles(DirectoryPath, "*.json"))
            {
                try { Reconcile(Path.GetFileNameWithoutExtension(file)); }
                catch (Exception exception) { Debug.LogWarning($"AIRuntime could not reconcile Unity operation '{file}': {exception.Message}"); }
            }
        }

        private static void Reconcile(string operationId)
        {
            UnityOperationRecord record = Read(operationId);
            if (record.State == "Succeeded" || record.State == "Failed") return;
            record.ReloadGenerationObserved = SessionState.GetInt(ReloadGenerationKey, 0);
            record.IsCompiling = EditorApplication.isCompiling;
            record.ScriptCompilationFailed = EditorUtility.scriptCompilationFailed;
            record.UpdatedUtc = DateTime.UtcNow.ToString("O");
            if (record.IsCompiling)
            {
                record.State = "Compiling";
                record.QuietObservationCount = 0;
                Write(record);
                EditorApplication.delayCall += () => Reconcile(operationId);
                return;
            }
            record.QuietObservationCount++;
            if (record.QuietObservationCount < 2)
            {
                record.State = "ImportSettling";
                Write(record);
                EditorApplication.delayCall += () => Reconcile(operationId);
                return;
            }
            record.State = record.ScriptCompilationFailed ? "Failed" : "Succeeded";
            record.Error = record.ScriptCompilationFailed ? "Unity reports scriptCompilationFailed after this operation." : string.Empty;
            Write(record);
        }

        private static void Write(UnityOperationRecord record)
        {
            Directory.CreateDirectory(DirectoryPath);
            string path = RecordPath(record.OperationId);
            string temporary = path + ".tmp";
            File.WriteAllText(temporary, JsonConvert.SerializeObject(record, Formatting.Indented));
            if (File.Exists(path)) File.Replace(temporary, path, null);
            else File.Move(temporary, path);
        }

        private static string RecordPath(string operationId) => Path.Combine(DirectoryPath, operationId + ".json");
    }


    internal static class SubstrateAssetUtility
    {
        internal static string ValidateAnyAssetPath(string value)
        {
            string path = (value ?? string.Empty).Trim().Replace('\\', '/');
            if ((path != "Assets" && path != "Packages" &&
                 !path.StartsWith("Assets/", StringComparison.Ordinal) &&
                 !path.StartsWith("Packages/", StringComparison.Ordinal)) ||
                path.Contains("../") || path.Contains("/../"))
                throw new ArgumentException("Path must be contained under Assets or Packages.");
            return path;
        }

        internal static string ValidateWritableAssetPath(string value)
        {
            string path = ValidateAnyAssetPath(value);
            if (!path.StartsWith("Assets/", StringComparison.Ordinal))
                throw new UnauthorizedAccessException("Writable Unity assets must be below Assets/.");
            return path;
        }

        internal static string ValidateSearchFolder(string value)
        {
            string path = ValidateAnyAssetPath(value);
            if (!AssetDatabase.IsValidFolder(path)) throw new DirectoryNotFoundException($"Unity asset folder does not exist: '{path}'.");
            return path;
        }

        internal static string ResolveAssetPath(string path, string guid, bool requireExisting)
        {
            string fromGuid = string.IsNullOrWhiteSpace(guid) ? string.Empty : AssetDatabase.GUIDToAssetPath(guid.Trim());
            string resolved = string.IsNullOrWhiteSpace(path) ? fromGuid : ValidateAnyAssetPath(path);
            if (!string.IsNullOrWhiteSpace(path) && !string.IsNullOrWhiteSpace(fromGuid) &&
                !string.Equals(resolved, fromGuid, StringComparison.Ordinal))
                throw new InvalidOperationException($"AssetPath and AssetGuid identify different assets. GUID resolves to '{fromGuid}'.");
            if (string.IsNullOrWhiteSpace(resolved)) throw new ArgumentException("AssetPath or AssetGuid is required.");
            if (requireExisting && AssetDatabase.LoadMainAssetAtPath(resolved) == null)
                throw new FileNotFoundException($"Unity asset does not exist: '{resolved}'.", resolved);
            return resolved;
        }

        internal static object DescribeAsset(string guid)
        {
            string path = AssetDatabase.GUIDToAssetPath(guid);
            return DescribePath(path);
        }

        internal static object DescribePath(string path)
        {
            UnityEngine.Object main = AssetDatabase.LoadMainAssetAtPath(path);
            return new
            {
                AssetGuid = AssetDatabase.AssetPathToGUID(path),
                AssetPath = path,
                Name = main != null ? main.name : Path.GetFileName(path),
                MainType = main != null ? main.GetType().FullName : string.Empty,
                Labels = main != null ? AssetDatabase.GetLabels(main) : Array.Empty<string>(),
                IsPackageAsset = path.StartsWith("Packages/", StringComparison.Ordinal),
                IsFolder = AssetDatabase.IsValidFolder(path)
            };
        }

        internal static string AssetPathToFullPath(string path) =>
            Path.GetFullPath(Path.Combine(Application.dataPath, "..", path.Replace('/', Path.DirectorySeparatorChar)));

        internal static string ParentAssetPath(string path) =>
            (Path.GetDirectoryName(path) ?? "Assets").Replace('\\', '/');

        internal static void EnsureAssetFolder(string path)
        {
            if (AssetDatabase.IsValidFolder(path)) return;
            string parent = ParentAssetPath(path);
            if (!AssetDatabase.IsValidFolder(parent)) EnsureAssetFolder(parent);
            string name = Path.GetFileName(path);
            string guid = AssetDatabase.CreateFolder(parent, name);
            if (string.IsNullOrWhiteSpace(guid)) throw new InvalidOperationException($"Unity could not create asset folder '{path}'.");
        }
    }
}
