using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;

namespace FoxxRecovery.EditorTools
{
    [InitializeOnLoad]
    public static class FoxxScriptRepair
    {
        private const string Root = "Assets/FoxxRecovery/Converted";
        private static bool scheduled;
        private static bool running;

        [Serializable] public class Reference { public string guid; public string assembly; public string type; }
        [Serializable] public class Manifest { public Reference[] scripts; }

        static FoxxScriptRepair() { Schedule(); }

        public static void Schedule()
        {
            if (scheduled || running || Application.isBatchMode) return;
            scheduled = true;
            EditorApplication.delayCall += () =>
            {
                scheduled = false;
                if (EditorApplication.isCompiling || EditorApplication.isUpdating) { Schedule(); return; }
                Repair(false);
            };
        }

        [MenuItem("Tools/Foxx Recovery/Repair Missing VRChat Scripts")]
        public static void RepairFromMenu() { Repair(true); }

#if UNITY_2021_2_OR_NEWER
        [MenuItem("Tools/Foxx Recovery/Use Low-Memory Import (This Session)")]
        public static void UseLowMemoryImport()
        {
            AssetDatabase.DesiredWorkerCount = 2;
            AssetDatabase.ForceToDesiredWorkerCount();
            Debug.Log("Foxx: requested two Unity import workers for this session. Imports may take longer but fewer assets are processed simultaneously. No avatar data was changed.");
        }
#endif

        public static int Repair(bool interactive)
        {
            if (running || !Directory.Exists(Root)) return 0;
            running = true;
            int changed = 0;
            var report = new List<string>();
            string backupRoot = null;
            try
            {
                var installed = MonoImporter.GetAllRuntimeMonoScripts()
                    .Where(script => script != null && script.GetClass() != null &&
                        AssetDatabase.GetAssetPath(script).StartsWith("Packages/com.vrchat.", StringComparison.Ordinal))
                    .GroupBy(script => Key(script.GetClass().Assembly.GetName().Name, script.GetClass().FullName))
                    .Where(group => group.Count() == 1).ToDictionary(group => group.Key, group => group.Single());
                foreach (string category in Directory.GetDirectories(Root))
                foreach (string directory in Directory.GetDirectories(category))
                {
                    string packageRoot = directory.Replace('\\', '/');
                    var references = ReadReferences(packageRoot);
                    var replacements = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
                    foreach (Reference reference in references)
                    {
                        if (!Regex.IsMatch(reference.guid ?? "", "^[a-fA-F0-9]{32}$") ||
                            !(reference.assembly ?? "").StartsWith("VRC.", StringComparison.Ordinal)) continue;
                        MonoScript target;
                        if (!installed.TryGetValue(Key(reference.assembly, reference.type), out target)) continue;
                        string oldPath = AssetDatabase.GUIDToAssetPath(reference.guid);
                        if (!string.IsNullOrEmpty(oldPath) && AssetDatabase.LoadAllAssetsAtPath(oldPath)
                            .OfType<MonoScript>().Any(script => script.GetClass() != null)) continue;
                        string targetGuid;
                        long targetId;
                        if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(target, out targetGuid, out targetId))
                            replacements[reference.guid] = "{fileID: " + targetId + ", guid: " + targetGuid + ", type: 3}";
                    }
                    if (replacements.Count == 0) continue;
                    foreach (string file in Directory.EnumerateFiles(directory, "*", SearchOption.AllDirectories))
                    {
                        if (!new[] { ".prefab", ".unity", ".asset", ".anim", ".controller", ".overrideController" }
                            .Contains(Path.GetExtension(file), StringComparer.OrdinalIgnoreCase)) continue;
                        if (new FileInfo(file).Length > 64L * 1024 * 1024)
                        {
                            report.Add(file + ": exceeds the repair memory budget; left unchanged.");
                            continue;
                        }
                        // Only the external MonoScript reference changes; all component data and IDs stay intact.
                        string original = File.ReadAllText(file);
                        if (!original.StartsWith("%YAML", StringComparison.Ordinal)) continue;
                        int count = 0;
                        string repaired = Regex.Replace(original,
                            @"\{fileID:\s*11500000,\s*guid:\s*([a-fA-F0-9]{32}),\s*type:\s*3\s*\}", match =>
                            {
                                string replacement;
                                if (!replacements.TryGetValue(match.Groups[1].Value, out replacement)) return match.Value;
                                count++;
                                return replacement;
                            });
                        if (count == 0) continue;
                        if (backupRoot == null) backupRoot = Path.Combine("FoxxRecoveryBackups", "Scripts",
                            DateTime.UtcNow.ToString("yyyyMMdd-HHmmss") + "-" + Guid.NewGuid().ToString("N").Substring(0, 8));
                        string backup = Path.Combine(backupRoot, file);
                        Directory.CreateDirectory(Path.GetDirectoryName(backup));
                        File.Copy(file, backup, false);
                        if (File.Exists(file + ".meta")) File.Copy(file + ".meta", backup + ".meta");
                        bool bom;
                        using (FileStream stream = File.OpenRead(file))
                            bom = stream.ReadByte() == 0xef && stream.ReadByte() == 0xbb && stream.ReadByte() == 0xbf;
                        string temporary = file + ".foxx-repair-tmp";
                        try
                        {
                            File.WriteAllText(temporary, repaired, new UTF8Encoding(bom));
                            File.Replace(temporary, file, null);
                        }
                        finally { if (File.Exists(temporary)) File.Delete(temporary); }
                        AssetDatabase.ImportAsset(file.Replace('\\', '/'), ImportAssetOptions.ForceSynchronousImport);
                        changed += count;
                        report.Add(file + ": restored " + count + " SDK script reference(s); component settings preserved.");
                    }
                }
                string summary = "Foxx script repair: " + changed + " VRChat reference(s) restored. Unrecognized scripts were not removed or replaced.";
                if (backupRoot != null)
                {
                    File.WriteAllLines(Path.Combine(backupRoot, "repair-report.txt"), new[] { summary }.Concat(report).ToArray());
                    summary += " Backups: " + Path.GetFullPath(backupRoot);
                }
                if (interactive || changed > 0) Debug.Log(summary + "\n" + string.Join("\n", report.ToArray()));
                if (interactive) EditorUtility.DisplayDialog("Foxx Script Repair", summary +
                    "\nIf a required component is unavailable, install/update the official VRChat SDK with Creator Companion and run repair again.", "OK");
                return changed;
            }
            finally { running = false; }
        }

        private static List<Reference> ReadReferences(string packageRoot)
        {
            var result = new List<Reference>();
            string path = packageRoot + "/FoxxScriptReferences.json";
            if (File.Exists(path))
            {
                try
                {
                    Manifest manifest = JsonUtility.FromJson<Manifest>(File.ReadAllText(path));
                    if (manifest != null && manifest.scripts != null) result.AddRange(manifest.scripts.Where(item => item != null));
                }
                catch (Exception error) { Debug.LogWarning("Foxx: cannot read script reference manifest: " + error.Message); }
            }
            // Verified legacy AssetRipper identity, before packages included a reference manifest.
            const string assembly = "VRC.SDK3.Dynamics.Constraint";
            const string type = "VRC.SDK3.Dynamics.Constraint.Components.VRCParentConstraint";
            const string original = "8a30bb5bc05db584e599aa2bfeab9812";
            string relative = ".Scripts/" + assembly + "/" + type.Replace('.', '/') + ".cs.meta";
            result.Add(new Reference { guid = original, assembly = assembly, type = type });
            // Older Windows workers salted script metadata paths using backslashes.
            result.Add(new Reference { guid = StableGuid(packageRoot + "|" + original + "|" + relative.Replace('/', '\\')), assembly = assembly, type = type });
            result.Add(new Reference { guid = StableGuid(packageRoot + "|" + original + "|" + relative), assembly = assembly, type = type });
            return result;
        }

        private static string Key(string assembly, string type) { return assembly + ":" + type; }
        private static string StableGuid(string value)
        {
            using (SHA256 sha = SHA256.Create()) return BitConverter.ToString(sha.ComputeHash(Encoding.UTF8.GetBytes(value)))
                .Replace("-", "").Substring(0, 32).ToLowerInvariant();
        }
    }

    public class FoxxScriptImportWatcher : AssetPostprocessor
    {
        private static void OnPostprocessAllAssets(string[] imported, string[] deleted, string[] moved, string[] movedFrom)
        {
            if (imported.Concat(moved).Any(path => path.StartsWith("Assets/FoxxRecovery/", StringComparison.Ordinal) ||
                path.StartsWith("Packages/com.vrchat.", StringComparison.Ordinal))) FoxxScriptRepair.Schedule();
        }
    }
}
