using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Text;
using System.Threading.Tasks;
using JetBrains.Annotations;
using UnityEditor;
using UnityEditor.PackageManager;
using UnityEditor.PackageManager.Requests;
using Debug = UnityEngine.Debug;
using PackageInfo = UnityEditor.PackageManager.PackageInfo;

// ReSharper disable once CheckNamespace
namespace Fluctio.FluctioSim.Installer
{
    internal static class Installer
    {

        #region Config

        internal const string InstallerGuid = "dedb9733ae48f0544ad6549a2d108fd3";
        internal const string PackageId = "ai.fluctio.fluctio-sim";

        private static readonly ScopedRegistry[] Registries =
        {
            new()
            {
                name = "Unity NuGet",
                url = "https://unitynuget-registry.openupm.com",
                scopes =
                {
                    "org.nuget",
                },
            },
            new()
            {
                name = "OpenUPM",
                url = "https://package.openupm.com",
                scopes =
                {
                    "org.mujoco.mujoco",
                    "io.github.balint-h.mj-unity-extensions",
                },
            },
            new()
            {
                name = "Fluctio",
                url = "https://npm.fluctio.ai",
                scopes =
                {
                    "ai.fluctio",
                },
            },
        };

        #endregion

        #region Installing
        
        private static readonly JsonFile<Manifest> ManifestFile = new("Packages/manifest.json");
        private static string GetSettingKey(string key) => $"{typeof(Installer).FullName}_{key}";
        private static bool? InstallationAttempted
        {
            get => SessionState.GetBool(GetSettingKey(nameof(InstallationAttempted)), false);
            set
            {
                var key = GetSettingKey(nameof(InstallationAttempted));
                if (value == null)
                {
                    SessionState.EraseBool(key);
                }
                else
                {
                    SessionState.SetBool(key, value.Value);
                }
            }
        }

        [InitializeOnLoadMethod]
        private static async void Init()
        {
#if FLUCTIO_INSTALLER_DISABLED
            Debug.Log("Installer started initializing, but it is disabled");
            await Task.CompletedTask; // needed to suppress warning
#else
            var doesPackageExist = await DoesPackageExist();
            if (doesPackageExist)
            {
                await LogPackageVersion();
            }

            if (doesPackageExist || InstallationAttempted is true)
            {
                DeleteInstaller();
                return;
            }
            
            try
            {
                AssetDatabase.DisallowAutoRefresh();
                EditorApplication.LockReloadAssemblies();
                await Install();
                Client.Resolve();
            }
            finally
            {
                EditorApplication.UnlockReloadAssemblies();
                AssetDatabase.AllowAutoRefresh();
            }
#endif
        }

        private static async Task<bool> DoesPackageExist()
        {
            var manifest = await ManifestFile.GetContentAsync();
            return manifest.dependencies.ContainsKey(PackageId);
        }

        private static async Task LogPackageVersion()
        {
            var packageInfo = await GetPackageInfo(true);
            Debug.Log(InstallationAttempted is true
                ? $"Installed {packageInfo.displayName} {packageInfo.version}"
                : $"{packageInfo.displayName} was already installed, version {packageInfo.version}");
        }

        private static async Task Install()
        {
            InstallationAttempted = true;
            await using (var manifestHandle = ManifestFile.Load())
            {
                Array.ForEach(Registries, manifestHandle.Content.AddRegistry);
            }
            var packageInfo = await GetPackageInfo(false);
            await using (var manifestHandle = ManifestFile.Load())
            {
                manifestHandle.Content.dependencies[PackageId] = packageInfo.versions.latestCompatible;
            }
        }

        private static void DeleteInstaller()
        {
            InstallationAttempted = null;
            var filePath = AssetDatabase.GUIDToAssetPath(InstallerGuid);
#if FLUCTIO_INSTALLER_AUTODELETE_DISABLED
            Debug.Log($"Debug mode enabled, without debug mode the following file would have been deleted:\n{filePath}");
#else
            File.Delete(filePath);
            File.Delete(filePath + ".meta");
            AssetDatabase.Refresh();
#endif
        }

        #endregion

        #region Package manager helpers
        
        private static async Task<PackageInfo> GetPackageInfo(bool offlineMode)
        {
            var packagesInfo = await Client.Search(PackageId, offlineMode).ToAsync();
            var packageInfo = packagesInfo?.Single();
            return packageInfo;
        }

        [ItemCanBeNull]
        private static Task<T> ToAsync<T>(this Request<T> request)
        {
            var completionSource = new TaskCompletionSource<T>();
            EditorApplication.update += Handler;
            return completionSource.Task;

            void Handler()
            {
                switch (request.Status)
                {
                    case StatusCode.InProgress:
                        return;
                    case StatusCode.Success:
                        completionSource.SetResult(request.Result);
                        break;
                    case StatusCode.Failure:
                        completionSource.SetException(new InvalidOperationException(request.Error.message));
                        break;
                    default:
                        throw new InvalidEnumArgumentException(
                            nameof(request.Status),
                            (int)request.Status,
                            request.Status.GetType());
                }

                EditorApplication.update -= Handler;
            }
        }

        #endregion

        #region Json file management

        private class JsonFileHandle<T> : IDisposable, IAsyncDisposable
        {

            private readonly DataContractJsonSerializer _serializer;
            private readonly FileStream _stream;
            private T _content;
            // ReSharper disable once RedundantDefaultMemberInitializer
            private bool _isDisposed = false;

            [SuppressMessage("ReSharper", "MemberCanBePrivate.Local")]
            public T Content
            {
                get
                {
                    CheckDisposed();
                    return _content;
                }
                set
                {
                    CheckDisposed();
                    _content = value;
                }
            }

            private void CheckDisposed()
            {
                if (_isDisposed)
                {
                    throw new ObjectDisposedException(GetType().FullName);
                }
            }

            public JsonFileHandle(string path, DataContractJsonSerializer serializer)
            {
                _serializer = serializer;
                _stream = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.None);
                Content = (T)_serializer.ReadObject(_stream);
            }

            private void DisposeCommon()
            {
                if (_isDisposed)
                {
                    return;
                }

                _stream.SetLength(0);
                using var writer = JsonReaderWriterFactory.CreateJsonWriter(_stream, Encoding.UTF8, true, true, "  ");
                _serializer.WriteObject(writer, _content);
                _isDisposed = true;
            }

            public void Dispose()
            {
                DisposeCommon();
                _stream?.Dispose();
            }

            public async ValueTask DisposeAsync()
            {
                DisposeCommon();
                await _stream.DisposeAsync();
            }

            ~JsonFileHandle() => Dispose();
        }

        private class JsonFile<T>
        {
            private static readonly DataContractJsonSerializer Serializer = new(typeof(T),
                new DataContractJsonSerializerSettings
                {
                    UseSimpleDictionaryFormat = true,
                });
            
            private readonly string _path;
            public JsonFile(string path) => _path = path;
            public JsonFileHandle<T> Load() => new(_path, Serializer);

            public T GetContent()
            {
                using var stream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
                return (T)Serializer.ReadObject(stream);
            }

            public async Task<T> GetContentAsync()
            {
                await using var stream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
                return (T)Serializer.ReadObject(stream);
            }
        }

        #endregion

        #region DataContract list

        [DataContract]
        [SuppressMessage("ReSharper", "InconsistentNaming")]
        private record Manifest : IExtensibleDataObject
        {
            [DataMember] public Dictionary<string, string> dependencies { get; set; } = new();
            [DataMember] public List<ScopedRegistry> scopedRegistries { get; set; } = new();
            public ExtensionDataObject ExtensionData { get; set; }

            [OnDeserializing]
            private void OnDeserializing(StreamingContext context)
            {
                dependencies = new Dictionary<string, string>();
                scopedRegistries = new List<ScopedRegistry>();
            }

            public void AddRegistry(ScopedRegistry registryToAdd)
            {

                // remove already existing scopes
                {
                    var existingScopes = scopedRegistries.SelectMany(registry => registry.scopes).ToHashSet();
                    var filteredScopes = registryToAdd.scopes.Where(scope => !existingScopes.Contains(scope)).ToList();
                    if (filteredScopes.Count == 0)
                    {
                        return;
                    }

                    registryToAdd = registryToAdd with
                    {
                        scopes = filteredScopes,
                    };
                }

                {
                    var matchingRegistries = scopedRegistries
                        .Where(registry => registry.url == registryToAdd.url)
                        .Take(2)
                        .ToList();
                    if (matchingRegistries.Count == 1)
                    {
                        // registry with this URL exists, add to existing registry
                        registryToAdd.scopes.ForEach(matchingRegistries[0].scopes.Add);
                        return;
                    }
                }

                // change name, if registry with this name already exists
                {
                    var originalName = registryToAdd.name;
                    while (scopedRegistries.Any(registry => registry.name == registryToAdd.name))
                    {
                        registryToAdd = registryToAdd with
                        {
                            name = $"{originalName} {Guid.NewGuid().GetHashCode()}",
                        };
                    }
                }

                scopedRegistries.Add(registryToAdd);
            }
        }

        [DataContract]
        [SuppressMessage("ReSharper", "InconsistentNaming")]
        private record ScopedRegistry : IExtensibleDataObject
        {
            [DataMember] public string name { get; set; }
            [DataMember] public string url { get; set; }
            [DataMember] public List<string> scopes { get; set; } = new();
            public ExtensionDataObject ExtensionData { get; set; }

            [OnDeserializing]
            private void OnDeserializing(StreamingContext context)
            {
                scopes = new List<string>();
            }
        }

        #endregion

    }
}
