/*************************************************************************************************
 * Copyright 2022-2025 Theai, Inc. dba Inworld AI
 *
 * Use of this source code is governed by the Inworld.ai Software Development Kit License Agreement
 * that can be found in the LICENSE.md file or at https://www.inworld.ai/sdk-license
 *************************************************************************************************/

#if UNITY_EDITOR
using System;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;
using Debug = UnityEngine.Debug;

namespace Inworld
{
    public class DependencyDownloaderWindow : EditorWindow
    {
        const string k_PluginsPath = "Assets/InworldRuntime/Plugins";
        const string k_StreamingAssetsPath = "Assets/StreamingAssets";
        const string k_ConfigAssetPath = "Assets/InworldRuntime/Resources/InworldRuntime.asset";
        const string k_APIKeyInstruction =
            "Please enter your Inworld API key here.\nYou can find it in https://platform.inworld.ai's Get API Key.";
        const string k_PluginDownloadUrl =
            "https://storage.googleapis.com/assets-inworld-ai/unity-packages/Plugins_260219.zip";
        const string k_StreamingAssetsDownloadUrl = 
            "https://storage.googleapis.com/assets-inworld-ai/unity-packages/StreamingAssets.zip";

        bool m_HasValidApiKey;
        string m_ApiKeyInput;
        bool m_Initialized;
        UnityEngine.Object m_ConfigObject;

        public static void ShowWindow()
        {
            DependencyDownloaderWindow window = GetWindow<DependencyDownloaderWindow>(true, "Dependency Downloader", true);
            window.minSize = new Vector2(460, 180);
            window.maxSize = new Vector2(800, 300);
            window.ShowUtility();
        }

        void OnEnable()
        {
            InitializeState();
        }

        void InitializeState()
        {
            m_Initialized = true;
            m_ConfigObject = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(k_ConfigAssetPath);
            if (!m_ConfigObject)
            {
                // If the config asset is missing, do not block the dependency download step. Just skip the API key wizard.
                m_HasValidApiKey = true;
                return;
            }

            SerializedObject serializedConfig = new SerializedObject(m_ConfigObject);
            SerializedProperty apiKeyProp = serializedConfig.FindProperty("m_APIKey");
            m_ApiKeyInput = apiKeyProp != null ? apiKeyProp.stringValue : string.Empty;
            m_HasValidApiKey = !string.IsNullOrEmpty(m_ApiKeyInput);
        }

        async void OnGUI()
        {
            if (!m_Initialized)
                InitializeState();

            // Ensure API key is present; otherwise, show the input flow.
            if (!m_HasValidApiKey)
            {
                DrawApiKeyStep();
                return;
            }

            bool missingPlugins = !Directory.Exists(k_PluginsPath) || !Directory.EnumerateFileSystemEntries(k_PluginsPath).Any();
            bool missingStreaming = !Directory.Exists(k_StreamingAssetsPath) || !Directory.EnumerateFileSystemEntries(k_StreamingAssetsPath).Any();
            EditorGUILayout.Space(4);

            if (!missingPlugins && !missingStreaming)
            {
                EditorGUILayout.HelpBox("DLLs and Models are downloaded", MessageType.Info);
            }
            else
            {
                EditorGUILayout.HelpBox(
                    "- Assets/InworldRuntime/Plugins\n- Assets/StreamingAssets",
                    MessageType.Warning);
            }

            EditorGUILayout.Space(6);

            using (new EditorGUILayout.HorizontalScope())
            {
                using (new EditorGUI.DisabledScope(!missingPlugins))
                {
                    if (GUILayout.Button("Download Plugins", GUILayout.Height(28)))
                    {
                        await DownloadAndInstall(k_PluginDownloadUrl, k_PluginsPath, "Plugins");
                        Repaint();
                    }
                }
                using (new EditorGUI.DisabledScope(!missingStreaming))
                {
                    if (GUILayout.Button("Download StreamingAssets", GUILayout.Height(28)))
                    {
                        await DownloadAndInstall(k_StreamingAssetsDownloadUrl, k_StreamingAssetsPath, "StreamingAssets");
                        Repaint();
                    }
                }
                if (GUILayout.Button("Later", GUILayout.Height(28)))
                {
                    Close();
                }
            }

            EditorGUILayout.Space(6);

            using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
            {
                DrawStatusRow("Assets/InworldRuntime/Plugins", !missingPlugins);
                DrawStatusRow("Assets/StreamingAssets", !missingStreaming);
            }
        }

        void DrawApiKeyStep()
        {
            EditorGUILayout.Space(8);
            EditorGUILayout.LabelField("Inworld API Key", EditorStyles.boldLabel);
            EditorGUILayout.Space(4);

            EditorGUILayout.HelpBox(k_APIKeyInstruction, MessageType.Info);

            EditorGUILayout.Space(6);

            using (new EditorGUILayout.HorizontalScope())
            {
                GUILayout.Label("API Key", GUILayout.Width(70));
                m_ApiKeyInput = EditorGUILayout.TextField(m_ApiKeyInput ?? string.Empty);
            }

            EditorGUILayout.Space(10);

            using (new EditorGUILayout.HorizontalScope())
            {
                GUILayout.FlexibleSpace();

                if (GUILayout.Button("Next", GUILayout.Width(100), GUILayout.Height(28)))
                {
                    string trimmed = (m_ApiKeyInput ?? string.Empty).Trim();
                    if (string.IsNullOrEmpty(trimmed))
                    {
                        EditorUtility.DisplayDialog("API Key Required",
                            "Please enter a valid Inworld API key.", "OK");
                    }
                    else
                    {
                        if (m_ConfigObject)
                        {
                            SerializedObject serializedConfig = new SerializedObject(m_ConfigObject);
                            SerializedProperty apiKeyProp = serializedConfig.FindProperty("m_APIKey");
                            if (apiKeyProp != null)
                            {
                                serializedConfig.Update();
                                apiKeyProp.stringValue = trimmed;
                                serializedConfig.ApplyModifiedProperties();
                                EditorUtility.SetDirty(m_ConfigObject);
                                AssetDatabase.SaveAssets();
                                AssetDatabase.Refresh();
                            }
                        }
                        m_HasValidApiKey = true;
                        Repaint();
                    }
                }

                if (GUILayout.Button("Cancel", GUILayout.Width(100), GUILayout.Height(28)))
                {
                    Close();
                }
            }
        }

        static void DrawStatusRow(string label, bool exists)
        {
            using (new EditorGUILayout.HorizontalScope())
            {
                GUILayout.Label(label);
                GUILayout.FlexibleSpace();
                GUIContent status = exists ? new GUIContent("Exist") : new GUIContent("Missing");
                Color old = GUI.color;
                GUI.color = exists ? new Color(0.25f, 0.7f, 0.3f) : new Color(0.85f, 0.4f, 0.2f);
                GUILayout.Label(status);
                GUI.color = old;
            }
        }

        public static async Task DownloadEssentials()
        {
            try
            {
                EssentialManifestSnapshot manifestSnapshot = await EssentialPackageManifestStore.FetchRemoteAsync();
                bool importSucceeded = await DownloadAndImportUnityPackage(
                    manifestSnapshot.Manifest.PackageUrl,
                    manifestSnapshot.Manifest.PackageDisplayName);

                if (importSucceeded)
                    EssentialPackageManifestStore.SaveCache(manifestSnapshot);
            }
            catch (Exception exception)
            {
                Debug.LogError(exception);
                EditorUtility.DisplayDialog("Failed", exception.Message, "OK");
            }
        }

        public static async Task<bool> DownloadAndImportUnityPackage(string packageUrl, string displayName)
        {
            string tempDir = null;
            string tempPackagePath = null;
            DownloadProgress downloadProgress = new DownloadProgress
            {
                Title = "Downloading",
                Message = $"Preparing to download {displayName}...",
                Value = 0.02f
            };

            using IProgressRenderer progressRenderer = ProgressRendererFactory.Create(displayName);

            try
            {
                tempDir = Path.Combine(Path.GetTempPath(), "InworldUnityTempPackages");
                if (!Directory.Exists(tempDir))
                    Directory.CreateDirectory(tempDir);
                tempPackagePath = Path.Combine(tempDir, $"{displayName}_{Guid.NewGuid():N}.unitypackage");

                Task downloadTask = Task.Run(() => DownloadPackageFile(packageUrl, tempPackagePath, displayName, downloadProgress));
                await PumpProgressAsync(downloadProgress, downloadTask, progressRenderer);

                downloadProgress.Report("Importing", $"Importing {displayName}...", 0.98f);
                progressRenderer.Render(downloadProgress.GetSnapshot());
                AssetDatabase.ImportPackage(tempPackagePath, false);
                progressRenderer.CompleteSuccess($"Imported {displayName}");
                return true;
            }
            catch (Exception e)
            {
                Debug.LogError(e);
                progressRenderer.CompleteFailure(e.Message);
                return false;
            }
            finally
            {
                try
                {
                    if (!string.IsNullOrEmpty(tempPackagePath) && File.Exists(tempPackagePath))
                    {
                        File.Delete(tempPackagePath);
                    }
                    if (!string.IsNullOrEmpty(tempDir) && Directory.Exists(tempDir))
                    {
                        if (!Directory.EnumerateFileSystemEntries(tempDir).Any())
                            Directory.Delete(tempDir, false);
                    }
                }
                catch (Exception cleanupErr)
                {
                    Debug.LogWarning($"Clear temp package failed: {cleanupErr.Message}");
                }
            }
        }

        static async Task PumpProgressAsync(DownloadProgress downloadProgress, Task downloadTask, IProgressRenderer renderer)
        {
            renderer.Render(downloadProgress.GetSnapshot());
            while (!downloadTask.IsCompleted)
            {
                await Task.Delay(100);
                renderer.Render(downloadProgress.GetSnapshot());
            }
            await downloadTask; // Propagate exception if any.
            renderer.Render(downloadProgress.GetSnapshot());
        }

        static void DownloadPackageFile(string packageUrl, string destinationPath, string displayName, DownloadProgress progress)
        {
            using HttpClient httpClient = new HttpClient();
            HttpResponseMessage response =
                httpClient.GetAsync(packageUrl, HttpCompletionOption.ResponseHeadersRead).GetAwaiter().GetResult();
            response.EnsureSuccessStatusCode();

            long? contentLength = response.Content.Headers.ContentLength;
            progress.Report("Downloading", $"Connecting to {displayName}...", 0.05f);

            using Stream httpStream = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult();
            using FileStream fileStream = new FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None);

            byte[] buffer = new byte[1024 * 512];
            long totalRead = 0;
            int read;

            while ((read = httpStream.Read(buffer, 0, buffer.Length)) > 0)
            {
                fileStream.Write(buffer, 0, read);
                totalRead += read;

                if (contentLength.HasValue && contentLength.Value > 0)
                {
                    float ratio = Mathf.Clamp01((float)totalRead / contentLength.Value);
                    progress.Report(
                        "Downloading",
                        $"Downloading {displayName}... ({FormatBytes(totalRead)} / {FormatBytes(contentLength.Value)})",
                        Mathf.Lerp(0.05f, 0.9f, ratio));
                }
                else
                {
                    float fallbackValue = progress.GetSnapshot().Value + 0.05f;
                    if (fallbackValue > 0.9f)
                        fallbackValue = 0.4f;
                    progress.Report(
                        "Downloading",
                        $"Downloading {displayName}... ({FormatBytes(totalRead)} downloaded)",
                        fallbackValue);
                }
            }

            progress.Report("Downloading", $"Finished downloading {displayName}", 0.93f);
        }
        
        static async Task DownloadAndInstall(string zipUrl, string targetFolder, string expectedTopLevel)
        {
            string tempDir = null;
            string zipPath = null;
            string extractDir = null;
            try
            {
                EditorUtility.DisplayProgressBar("Downloading", $"Start Downloading {targetFolder}...", 0.1f);

                tempDir = Path.Combine(Path.GetTempPath(), "InworldUnityTemp");
                if (!Directory.Exists(tempDir))
                    Directory.CreateDirectory(tempDir);
                zipPath = Path.Combine(tempDir, Guid.NewGuid().ToString("N") + ".zip");
                extractDir = Path.Combine(tempDir, Guid.NewGuid().ToString("N"));

                using (HttpClient httpClient = new HttpClient())
                await using (Stream httpStream = await httpClient.GetStreamAsync(zipUrl))
                await using (FileStream fileStream = new FileStream(zipPath, FileMode.Create, FileAccess.Write, FileShare.None))
                {
                    await httpStream.CopyToAsync(fileStream);
                }

                EditorUtility.DisplayProgressBar("Unzipping", $"Start Unzipping {targetFolder}...", 0.6f);
                Directory.CreateDirectory(extractDir);
                ZipFile.ExtractToDirectory(zipPath, extractDir);

                if (!Directory.Exists(targetFolder))
                    Directory.CreateDirectory(targetFolder);

                string candidate = Path.Combine(extractDir, expectedTopLevel);
                string sourceToMerge = Directory.Exists(candidate) ? candidate : extractDir;

                EditorUtility.DisplayProgressBar("Installing", "Copy files to directory...", 0.85f);
                MergeDirectories(sourceToMerge, targetFolder);

                AssetDatabase.Refresh();
                EditorUtility.DisplayDialog("Finished", $"Extracted to {targetFolder}", "OK");
            }
            catch (Exception e)
            {
                Debug.LogError(e);
                EditorUtility.DisplayDialog("Failed", e.Message, "OK");
            }
            finally
            {
                EditorUtility.ClearProgressBar();
                try
                {
                    if (!string.IsNullOrEmpty(zipPath) && File.Exists(zipPath))
                        File.Delete(zipPath);
                    if (!string.IsNullOrEmpty(extractDir) && Directory.Exists(extractDir))
                        Directory.Delete(extractDir, true);
                }
                catch (Exception cleanupErr)
                {
                    Debug.LogWarning($"Clear temp file failed: {cleanupErr.Message}");
                }
            }
        }

        static void MergeDirectories(string sourceDir, string destDir)
        {
            foreach (string dir in Directory.GetDirectories(sourceDir, "*", SearchOption.AllDirectories))
            {
                string relative = dir.Substring(sourceDir.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
                string target = Path.Combine(destDir, relative);
                if (!Directory.Exists(target))
                    Directory.CreateDirectory(target);
            }
            foreach (string file in Directory.GetFiles(sourceDir, "*", SearchOption.AllDirectories))
            {
                string relative = file.Substring(sourceDir.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
                string target = Path.Combine(destDir, relative);
                string targetDir = Path.GetDirectoryName(target);
                if (!string.IsNullOrEmpty(targetDir) && !Directory.Exists(targetDir))
                    Directory.CreateDirectory(targetDir);
                File.Copy(file, target, true);
            }
        }

        interface IProgressRenderer : IDisposable
        {
            void Render(DownloadProgress.Snapshot snapshot);
            void CompleteSuccess(string message);
            void CompleteFailure(string message);
        }

        static class ProgressRendererFactory
        {
            public static IProgressRenderer Create(string displayName)
            {
#if UNITY_6000_0_OR_NEWER
                return new UnityProgressRenderer(displayName);
#else
                return new LegacyProgressRenderer(displayName);
#endif
            }
        }

#if UNITY_6000_0_OR_NEWER
        class UnityProgressRenderer : IProgressRenderer
        {
            readonly int m_ProgressId;
            bool m_Completed;

            public UnityProgressRenderer(string displayName)
            {
                m_ProgressId = Progress.Start($"Downloading {displayName}", $"Preparing to download {displayName}", Progress.Options.Sticky);
            }

            public void Render(DownloadProgress.Snapshot snapshot)
            {
                Progress.Report(m_ProgressId, Mathf.Clamp01(snapshot.Value), snapshot.Message ?? string.Empty);
            }

            public void CompleteSuccess(string message)
            {
                Progress.Report(m_ProgressId, 1f, message ?? "Finished");
                Progress.Finish(m_ProgressId);
                EditorUtility.DisplayDialog("Finished", message ?? "Download completed.", "OK");
                m_Completed = true;
            }

            public void CompleteFailure(string message)
            {
                Progress.Report(m_ProgressId, 1f, $"Failed: {message}");
                Progress.Remove(m_ProgressId);
                EditorUtility.DisplayDialog("Failed", message ?? "Download failed.", "OK");
                m_Completed = true;
            }

            public void Dispose()
            {
                if (!m_Completed)
                    Progress.Remove(m_ProgressId);
            }
        }
#endif

        class LegacyProgressRenderer : IProgressRenderer
        {
            public LegacyProgressRenderer(string displayName)
            {
                Progress.Start($"Downloading {displayName}", $"Preparing to download {displayName}", Progress.Options.Sticky);
            }
            public void Render(DownloadProgress.Snapshot snapshot)
            {
                EditorUtility.DisplayProgressBar(
                    snapshot.Title ?? "Downloading",
                    snapshot.Message ?? string.Empty,
                    Mathf.Clamp01(snapshot.Value));
            }

            public void CompleteSuccess(string message)
            {
                EditorUtility.ClearProgressBar();
                EditorUtility.DisplayDialog("Finished", message ?? "Download completed.", "OK");
            }

            public void CompleteFailure(string message)
            {
                EditorUtility.ClearProgressBar();
                EditorUtility.DisplayDialog("Failed", message ?? "Download failed.", "OK");
            }

            public void Dispose()
            {
                EditorUtility.ClearProgressBar();
            }
        }

        static string FormatBytes(long bytes)
        {
            string[] suffixes = { "B", "KB", "MB", "GB" };
            double value = bytes;
            int suffixIndex = 0;
            while (value >= 1024 && suffixIndex < suffixes.Length - 1)
            {
                value /= 1024;
                suffixIndex++;
            }
            return $"{value:0.##}{suffixes[suffixIndex]}";
        }

        class DownloadProgress
        {
            readonly object m_Lock = new object();

            public string Title { get; set; }
            public string Message { get; set; }
            public float Value { get; set; }

            public void Report(string title, string message, float value)
            {
                lock (m_Lock)
                {
                    Title = title;
                    Message = message;
                    Value = value;
                }
            }

            public Snapshot GetSnapshot()
            {
                lock (m_Lock)
                {
                    return new Snapshot(Title ?? "Downloading", Message ?? string.Empty, Value);
                }
            }

            public readonly struct Snapshot
            {
                public readonly string Title;
                public readonly string Message;
                public readonly float Value;

                public Snapshot(string title, string message, float value)
                {
                    Title = title;
                    Message = message;
                    Value = value;
                }
            }
        }
    }
}
#endif


