using System;
using System.Collections.Generic;
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;
using UnityEngine.SceneManagement;
using Object = UnityEngine.Object;

namespace AIRuntime.Editor
{
    [Serializable]
    public sealed class InspectGameplayReadinessArguments
    {
        [Required, Description("Exact Assets/...unity path to inspect. Never saves or repairs the scene.")]
        public string ScenePath { get; set; }
        public bool IncludeReferencedPrefabs { get; set; } = true;
        [Description("Opt in to a bounded Play Mode startup check. Requires the target to be the only loaded scene and idle Edit Mode. This does not verify gameplay behavior.")]
        public bool RunStartupCheck { get; set; }
    }

    public sealed class InspectGameplayReadinessCapability : CapabilityBase<InspectGameplayReadinessArguments>
    {
        private readonly IKanbanTaskExecutionServices _tasks;
        private readonly ControlPlayModeCapability _play;
        private string _lastFingerprint;
        public InspectGameplayReadinessCapability() : this(null, null) { }
        internal InspectGameplayReadinessCapability(IKanbanTaskExecutionServices tasks, IKanbanRunIdentity identity)
        { _tasks = tasks; _play = new ControlPlayModeCapability(identity); }
        public override string Name => "inspect_gameplay_readiness";
        public override string Description => "One bounded report of scene identity, hierarchy, components, serialized values and references, referenced prefabs, compilation and ownership. Optional guarded startup check. Use its stable object IDs and findings instead of repeatedly searching the same components. Null optional fields are observations, not automatic failures.";
        // Conservatively gate the entire capability because an opt-in call can enter Play Mode.
        public override CapabilityPermission Permission => CapabilityPermission.Write;

        protected override async Task<object> ExecuteAsync(InspectGameplayReadinessArguments arguments, CapabilityContext context)
        {
            context.CancellationToken.ThrowIfCancellationRequested();
            string path = arguments?.ScenePath?.Replace('\\', '/');
            if (string.IsNullOrWhiteSpace(path) || !path.StartsWith("Assets/", StringComparison.Ordinal) ||
                path.Split('/').Contains("..") || !path.EndsWith(".unity", StringComparison.OrdinalIgnoreCase))
                throw new InvalidOperationException("Provide an exact scene asset path under Assets.");
            JObject contract = _tasks == null ? null : JObject.FromObject(_tasks.ReadTaskContext());
            bool scoped = contract?["IsChildTask"]?.Value<bool>() == true;
            var readable = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            foreach (string key in new[] { "ProducedArtifactPaths", "ConsumedArtifactPaths", "CompletedDependencyArtifactPaths" })
                foreach (string value in (contract?[key] as JArray ?? new JArray()).Values<string>())
                    if (!string.IsNullOrWhiteSpace(value)) readable.Add(value.Replace('\\', '/').TrimEnd('/'));
            bool CanRead(string asset) => !scoped || readable.Contains(asset);
            if (!CanRead(path)) throw new InvalidOperationException("[ArtifactOwnership] Scene is outside this card's readable artifacts: " + path);
            if (EditorApplication.isCompiling || EditorApplication.isPlayingOrWillChangePlaymode)
                throw new InvalidOperationException("Inspection requires idle Edit Mode with compilation finished.");
            if (AssetDatabase.LoadAssetAtPath<SceneAsset>(path) == null)
                throw new InvalidOperationException("Scene asset not found: " + path);

            Scene scene = SceneManager.GetSceneByPath(path);
            bool opened = !scene.IsValid() || !scene.isLoaded;
            if (opened) scene = EditorSceneManager.OpenScene(path, OpenSceneMode.Additive);
            var objects = new JArray(); var findings = new JArray(); var coverage = new JArray();
            var prefabPaths = new Queue<string>(); var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            int objectCount = 0, componentCount = 0, propertyCount = 0;
            bool truncated = false;
            try
            {
                JObject Owner(string asset)
                {
                    if (contract == null) return new JObject { ["Status"] = "NoBoardContext" };
                    var matches = contract.DescendantsAndSelf().OfType<JObject>().Where(node =>
                        (node["ProducedArtifactPaths"] as JArray)?.Values<string>().Contains(asset) == true)
                        .Select(node => new JObject { ["TaskId"] = node["TaskId"] ?? node["Id"], ["Title"] = node["Title"] }).ToArray();
                    return new JObject { ["Status"] = matches.Length == 0 ? "OwnerNotExposedInTaskContext" : "DeclaredOwner", ["Candidates"] = new JArray(matches) };
                }
                JObject Identity(Object obj)
                {
                    if (obj == null) return null;
                    var go = obj as GameObject ?? (obj as Component)?.gameObject;
                    return new JObject { ["Name"] = obj.name, ["InstanceId"] = obj.GetInstanceID(),
                        ["GlobalObjectId"] = GlobalObjectId.GetGlobalObjectIdSlow(obj).ToString(),
                        ["ScenePath"] = go != null && go.scene.IsValid() ? go.scene.path : "",
                        ["AssetPath"] = AssetDatabase.GetAssetPath(obj) };
                }
                void Scan(GameObject root, string asset)
                {
                    var pending = new Stack<Transform>(); pending.Push(root.transform);
                    while (pending.Count > 0)
                    {
                        context.CancellationToken.ThrowIfCancellationRequested();
                        if (objectCount >= 200 || componentCount >= 600 || propertyCount >= 4000) { truncated = true; return; }
                        var transform = pending.Pop(); objectCount++;
                        for (int i = transform.childCount - 1; i >= 0; i--) pending.Push(transform.GetChild(i));
                        string instancePrefab = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(transform.gameObject);
                        if (arguments.IncludeReferencedPrefabs && !string.IsNullOrEmpty(instancePrefab) && visited.Add(instancePrefab)) prefabPaths.Enqueue(instancePrefab);
                        var node = Identity(transform.gameObject); node["ArtifactPath"] = asset;
                        node["ActiveSelf"] = transform.gameObject.activeSelf;
                        node["HierarchyPath"] = AnimationUtility.CalculateTransformPath(transform, root.transform);
                        var components = new JArray(); node["Components"] = components; objects.Add(node);
                        foreach (Component component in transform.GetComponents<Component>())
                        {
                            if (componentCount >= 600 || propertyCount >= 4000) { truncated = true; return; }
                            componentCount++;
                            if (component == null) { findings.Add(new JObject { ["Kind"] = "MissingScript", ["Object"] = Identity(transform.gameObject), ["ArtifactPath"] = asset }); continue; }
                            var entry = Identity(component); entry["Type"] = component.GetType().FullName; entry["Owner"] = Owner(asset);
                            if (component is MonoBehaviour behaviour)
                            {
                                string scriptPath = AssetDatabase.GetAssetPath(MonoScript.FromMonoBehaviour(behaviour));
                                entry["ScriptPath"] = scriptPath;
                                entry["ScriptOwner"] = Owner(scriptPath);
                            }
                            var values = new JObject(); entry["Properties"] = values; components.Add(entry);
                            using (var serialized = new SerializedObject(component))
                            {
                                var property = serialized.GetIterator();
                                bool enterChildren = true;
                                while (property.Next(enterChildren))
                                {
                                    // Strings, references and vectors have internal children;
                                    // their typed value above is sufficient. Only recurse into
                                    // actual serialized structures/arrays.
                                    enterChildren = property.propertyType == SerializedPropertyType.Generic;
                                    context.CancellationToken.ThrowIfCancellationRequested();
                                    if (++propertyCount > 4000) { truncated = true; return; }
                                    if (property.propertyPath == "m_CorrespondingSourceObject" || property.propertyPath == "m_PrefabInstance" ||
                                        property.propertyPath == "m_PrefabAsset" || property.propertyPath == "m_GameObject" || property.propertyPath == "m_Script") continue;
                                    if (property.propertyType == SerializedPropertyType.ObjectReference)
                                    {
                                        Object reference = property.objectReferenceValue;
                                        values[property.propertyPath] = Identity(reference);
                                        if (reference == null && property.propertyPath != "m_Script")
                                            findings.Add(new JObject { ["Kind"] = property.objectReferenceInstanceIDValue != 0 ? "BrokenReference" : "UnassignedReference", ["Requiredness"] = "Unknown", ["Component"] = Identity(component), ["ComponentType"] = component.GetType().FullName, ["Property"] = property.propertyPath, ["ArtifactPath"] = asset, ["Owner"] = Owner(asset) });
                                        var referencedGo = reference as GameObject ?? (reference as Component)?.gameObject;
                                        if (referencedGo != null && referencedGo.scene.IsValid() && transform.gameObject.scene.IsValid() && referencedGo.scene != transform.gameObject.scene)
                                            findings.Add(new JObject { ["Kind"] = "CrossSceneReference", ["Component"] = Identity(component), ["Property"] = property.propertyPath, ["Reference"] = Identity(reference) });
                                        string referencePath = AssetDatabase.GetAssetPath(reference);
                                        if (arguments.IncludeReferencedPrefabs && referencePath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase) && visited.Add(referencePath)) prefabPaths.Enqueue(referencePath);
                                    }
                                    else if (property.propertyType == SerializedPropertyType.String) values[property.propertyPath] = (property.stringValue ?? "").Substring(0, Math.Min(256, (property.stringValue ?? "").Length));
                                    else if (property.propertyType == SerializedPropertyType.Boolean) values[property.propertyPath] = property.boolValue;
                                    else if (property.propertyType == SerializedPropertyType.Integer || property.propertyType == SerializedPropertyType.Enum) values[property.propertyPath] = property.intValue;
                                    else if (property.propertyType == SerializedPropertyType.Float) values[property.propertyPath] = property.doubleValue;
                                    else if (property.propertyType == SerializedPropertyType.Vector3) values[property.propertyPath] = new JObject { ["x"] = property.vector3Value.x, ["y"] = property.vector3Value.y, ["z"] = property.vector3Value.z };
                                }
                            }
                        }
                    }
                }
                foreach (var root in scene.GetRootGameObjects()) Scan(root, path);
                int prefabs = 0;
                while (prefabPaths.Count > 0 && prefabs < 16 && !truncated)
                {
                    string prefabPath = prefabPaths.Dequeue();
                    if (!CanRead(prefabPath)) { coverage.Add("Skipped prefab outside declared read scope: " + prefabPath); continue; }
                    var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
                    if (prefab != null) { prefabs++; Scan(prefab, prefabPath); }
                    else coverage.Add("Prefab could not be loaded: " + prefabPath);
                }
                if (prefabPaths.Count > 0) truncated = true;
                var loaded = new JArray();
                for (int i = 0; i < SceneManager.sceneCount; i++)
                { var s = SceneManager.GetSceneAt(i); loaded.Add(new JObject { ["Path"] = s.path, ["Dirty"] = s.isDirty, ["Loaded"] = s.isLoaded }); }
                object compilation = await ((ICapability)new GetCompilationErrorsCapability()).ExecuteObjectAsync(new GetCompilationErrorsArguments { MaxMessages = 30 }, context);
                var report = new JObject { ["ScenePath"] = path, ["Dirty"] = scene.isDirty, ["EvidenceSource"] = opened ? "SavedSceneOpenedForInspection" : "LiveSceneMayContainUnsavedChanges",
                    ["LoadedScenes"] = loaded, ["Objects"] = objects, ["Findings"] = findings, ["Compilation"] = JToken.FromObject(compilation),
                    ["Coverage"] = new JObject { ["Truncated"] = truncated, ["Objects"] = objectCount, ["Components"] = componentCount, ["Properties"] = propertyCount, ["ReferencedPrefabs"] = prefabs, ["Skipped"] = coverage, ["GameplayBehaviorVerified"] = false },
                    ["Guidance"] = "Use the supplied IDs and property findings; do not repeat object discovery. Unassigned fields may be optional. Route confirmed defects to the declared artifact owner. Startup success does not prove movement, jumping, respawn, camera or victory. Truncated/skipped evidence cannot support a complete pass." };
                // Keep one response bounded even for heavily serialized scenes.
                while (report.ToString(Formatting.None).Length > 150000 && (objects.Count > 0 || findings.Count > 0))
                {
                    if (objects.Count > 0) objects.Last.Remove();
                    else findings.Last.Remove();
                    report["Coverage"]["Truncated"] = true;
                }
                report["Coverage"]["ReturnedObjects"] = objects.Count;
                report["Coverage"]["ReturnedFindings"] = findings.Count;
                using (var sha = SHA256.Create()) report["ReportId"] = BitConverter.ToString(sha.ComputeHash(Encoding.UTF8.GetBytes(report.ToString(Formatting.None)))).Replace("-", "");
                if (arguments.RunStartupCheck)
                {
                    if (SceneManager.sceneCount != 1 || scene.isDirty)
                        report["Startup"] = new JObject { ["Status"] = "NotRun", ["Reason"] = "Requires one clean loaded scene; other loaded or unsaved scenes are preserved." };
                    else
                    {
                        try
                        {
                            object startup = await ((ICapability)_play).ExecuteObjectAsync(new ControlPlayModeArguments { Action = "Enter", TransitionTimeoutSeconds = 30 }, context);
                            report["Startup"] = JToken.FromObject(startup);
                            var attribution = new JArray();
                            foreach (var error in report["Startup"]["StartupErrors"] as JArray ?? new JArray())
                            {
                                string diagnostic = (error["Message"]?.Value<string>() ?? "") + " " + (error["StackTrace"]?.Value<string>() ?? "");
                                var candidates = objects.OfType<JObject>().SelectMany(o => (o["Components"] as JArray ?? new JArray()).OfType<JObject>())
                                    .Where(c => c["ScriptPath"] != null && diagnostic.Contains((c["Type"]?.Value<string>() ?? "").Split('.').Last()))
                                    .Take(16).Select(c => new JObject { ["Component"] = c["GlobalObjectId"], ["Type"] = c["Type"], ["ScenePath"] = c["ScenePath"], ["AssetPath"] = c["AssetPath"], ["Owner"] = c["Owner"], ["ScriptOwner"] = c["ScriptOwner"] });
                                attribution.Add(new JObject { ["Message"] = error["Message"], ["Attribution"] = "CandidatesOnly", ["Candidates"] = new JArray(candidates) });
                            }
                            report["StartupCandidates"] = attribution;
                            report["StartupAttribution"] = "Startup log stack traces identify script methods; object identity is not provided by Unity's log callback. Compare component/prefab candidates above; do not claim an exact instance when ambiguous.";
                        }
                        finally
                        {
                            if (EditorApplication.isPlaying)
                                await ((ICapability)_play).ExecuteObjectAsync(new ControlPlayModeArguments { Action = "Exit", TransitionTimeoutSeconds = 30 }, new CapabilityContext { CancellationToken = System.Threading.CancellationToken.None });
                        }
                    }
                }
                else report["Startup"] = new JObject { ["Status"] = "NotRequested" };
                string fingerprint = report["ReportId"].Value<string>();
                if (!arguments.RunStartupCheck && _lastFingerprint == fingerprint)
                    return new JObject { ["ReportId"] = fingerprint, ["Unchanged"] = true, ["Guidance"] = "No new evidence. Use the previous report and its object IDs. Perform the missing behavioral test or report the concrete blocker instead of repeating inspection." };
                _lastFingerprint = fingerprint;
                return report;
            }
            finally { if (opened && !EditorApplication.isPlayingOrWillChangePlaymode && scene.IsValid()) EditorSceneManager.CloseScene(scene, true); }
        }
    }
}
