using AssetStoreTools.Uploader.Data;
using AssetStoreTools.Uploader.Utility;
using AssetStoreTools.Utility;
using AssetStoreTools.Utility.Json;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;

namespace AssetStoreTools.Uploader
{
    /// <summary>
    /// A class for retrieving data from the Asset Store backend <para/>
    /// <b>Note:</b> most data retrieval methods require <see cref="SavedSessionId"/> to be set
    /// </summary>
    internal static class AssetStoreAPI
    {
        public const string ToolVersion = "V11.4.3";

        private const string UnauthSessionId = "26c4202eb475d02864b40827dfff11a14657aa41";
        private const string KharmaSessionId = "kharma.sessionid";
        private const int UploadResponseTimeoutMs = 10000;

        public static string AssetStoreProdUrl = "https://kharma.unity3d.com";
        private static string s_sessionId = EditorPrefs.GetString(KharmaSessionId);
        private static HttpClient httpClient = new HttpClient();
        private static CancellationTokenSource s_downloadCancellationSource;

        public static string SavedSessionId
        {
            get => s_sessionId;
            set
            {
                s_sessionId = value;
                EditorPrefs.SetString(KharmaSessionId, value);
                httpClient.DefaultRequestHeaders.Clear();
                if (!string.IsNullOrEmpty(value))
                    httpClient.DefaultRequestHeaders.Add("X-Unity-Session", SavedSessionId);
            }
        }

        public static bool IsCloudUserAvailable => CloudProjectSettings.userName != "anonymous";
        public static string LastLoggedInUser = "";
        public static ConcurrentDictionary<string, OngoingUpload> ActiveUploads = new ConcurrentDictionary<string, OngoingUpload>();
        public static bool IsUploading => (ActiveUploads.Count > 0);

        static AssetStoreAPI()
        {
            ServicePointManager.DefaultConnectionLimit = 500;
            httpClient.DefaultRequestHeaders.ConnectionClose = false;
            httpClient.Timeout = TimeSpan.FromMinutes(1320);
        }

        /// <summary>
        /// A structure used to return the success outcome and the result of Asset Store API calls
        /// </summary>
        internal class APIResult
        {
            public JsonValue Response;
            public bool Success;
            public bool SilentFail;
            public ASError Error;

            public static implicit operator bool(APIResult value)
            {
                return value != null && value.Success != false;
            }
        }

        #region Login API

        /// <summary>
        /// A login API call that uses the email and password credentials
        /// </summary>
        /// <remarks>
        /// <b>Note:</b> this method only returns a response from the server and does not set the <see cref="SavedSessionId"/> itself
        /// </remarks>
        public static async Task<APIResult> LoginWithCredentialsAsync(string email, string password)
        {
            FormUrlEncodedContent data = GetLoginContent(new Dictionary<string, string> { { "user", email }, { "pass", password } });
            return await LoginAsync(data);
        }

        /// <summary>
        /// A login API call that uses the <see cref="SavedSessionId"/>
        /// </summary>
        /// <remarks>
        /// <b>Note:</b> this method only returns a response from the server and does not set the <see cref="SavedSessionId"/> itself
        /// </remarks>
        public static async Task<APIResult> LoginWithSessionAsync()
        {
            if (string.IsNullOrEmpty(SavedSessionId))
                return new APIResult() { Success = false, SilentFail = true, Error = ASError.GetGenericError(new Exception("No active session available")) };

            FormUrlEncodedContent data = GetLoginContent(new Dictionary<string, string> { { "reuse_session", SavedSessionId }, { "xunitysession", UnauthSessionId } });
            return await LoginAsync(data);
        }

        /// <summary>
        /// A login API call that uses the <see cref="CloudProjectSettings.accessToken"/><para/>
        /// </summary>
        /// <remarks>
        /// <b>Note:</b> this method only returns a response from the server and does not set the <see cref="SavedSessionId"/> itself
        /// </remarks>
        /// <param name="token">Cloud access token. Can be retrieved by calling <see cref="CloudProjectSettings.accessToken"/></param>
        public static async Task<APIResult> LoginWithTokenAsync(string token)
        {
            FormUrlEncodedContent data = GetLoginContent(new Dictionary<string, string> { { "user_access_token", token } });
            return await LoginAsync(data);
        }

        private static async Task<APIResult> LoginAsync(FormUrlEncodedContent data)
        {
            OverrideAssetStoreUrl();
            Uri uri = new Uri($"{AssetStoreProdUrl}/login");

            httpClient.DefaultRequestHeaders.Clear();
            httpClient.DefaultRequestHeaders.Add("Accept", "application/json");

            try
            {
                var response = await httpClient.PostAsync(uri, data);
                return UploadValuesCompletedLogin(response);
            }
            catch (Exception e)
            {
                return new APIResult() { Success = false, Error = ASError.GetGenericError(e) };
            }
        }

        private static APIResult UploadValuesCompletedLogin(HttpResponseMessage response)
        {
            ASDebug.Log($"Upload Values Complete {response.ReasonPhrase}");
            ASDebug.Log($"Login success? {response.IsSuccessStatusCode}");
            try
            {
                response.EnsureSuccessStatusCode();
                var responseResult = response.Content.ReadAsStringAsync().Result;
                var success = JSONParser.AssetStoreResponseParse(responseResult, out ASError error, out JsonValue jsonResult);
                if (success)
                    return new APIResult() { Success = true, Response = jsonResult };
                else
                    return new APIResult() { Success = false, Error = error };
            }
            catch (HttpRequestException ex)
            {
                return new APIResult() { Success = false, Error = ASError.GetLoginError(response, ex) };
            }
        }

        #endregion

        #region Package Metadata API

        private static async Task<JsonValue> GetPackageDataMain()
        {
            return await GetAssetStoreData(APIUri("asset-store-tools", "metadata/0", SavedSessionId));
        }

        private static async Task<JsonValue> GetPackageDataExtra()
        {
            return await GetAssetStoreData(APIUri("management", "packages", SavedSessionId));
        }

        private static async Task<JsonValue> GetCategories(bool useCached)
        {
            if (useCached)
            {
                if (AssetStoreCache.GetCachedCategories(out JsonValue cachedCategoryJson))
                    return cachedCategoryJson;

                ASDebug.LogWarning("Failed to retrieve cached category data. Proceeding to download");
            }
            var categoryJson = await GetAssetStoreData(APIUri("management", "categories", SavedSessionId));
            AssetStoreCache.CacheCategories(categoryJson);

            return categoryJson;
        }

        /// <summary>
        /// Retrieve data for all packages associated with the currently logged in account (identified by <see cref="SavedSessionId"/>)
        /// </summary>
        /// <param name="useCached"></param>
        /// <returns></returns>
        public static async Task<APIResult> GetFullPackageDataAsync(bool useCached)
        {
            if (useCached)
            {
                if (AssetStoreCache.GetCachedPackageMetadata(out JsonValue cachedData))
                    return new APIResult() { Success = true, Response = cachedData };

                ASDebug.LogWarning("Failed to retrieve cached package metadata. Proceeding to download");
            }

            try
            {
                var jsonMainData = await GetPackageDataMain();
                var jsonExtraData = await GetPackageDataExtra();
                var jsonCategoryData = await GetCategories(useCached);

                var joinedData = MergePackageData(jsonMainData, jsonExtraData, jsonCategoryData);
                AssetStoreCache.CachePackageMetadata(joinedData);

                return new APIResult() { Success = true, Response = joinedData };
            }
            catch (OperationCanceledException e)
            {
                ASDebug.Log("Package metadata download operation cancelled");
                DisposeDownloadCancellation();
                return new APIResult() { Success = false, SilentFail = true, Error = ASError.GetGenericError(e) };
            }
            catch (Exception e)
            {
                return new APIResult() { Success = false, Error = ASError.GetGenericError(e) };
            }
        }

        /// <summary>
        /// Retrieve the thumbnail textures for all packages within the provided json structure and perform a given action after each retrieval
        /// </summary>
        /// <param name="packageJson">A json file retrieved from <see cref="GetFullPackageDataAsync(bool)"/></param>
        /// <param name="useCached">Return cached thumbnails if they are found</param>
        /// <param name="onSuccess">
        /// Action to perform upon a successful thumbnail retrieval <para/>
        /// <see cref="string"/> - Package Id <br/>
        /// <see cref="Texture2D"/> - Associated Thumbnail
        /// </param>
        /// <param name="onFail">
        /// Action to perform upon a failed thumbnail retrieval <para/>
        /// <see cref="string"/> - Package Id <br/>
        /// <see cref="ASError"/> - Associated error
        /// </param>
        public static async void GetPackageThumbnails(JsonValue packageJson, bool useCached, Action<string, Texture2D> onSuccess, Action<string, ASError> onFail)
        {
            SetupDownloadCancellation();
            var packageDict = packageJson["packages"].AsDict();
            var packageEnum = packageDict.GetEnumerator();

            for (int i = 0; i < packageDict.Count; i++)
            {
                packageEnum.MoveNext();
                var package = packageEnum.Current;

                try
                {
                    s_downloadCancellationSource.Token.ThrowIfCancellationRequested();

                    if (package.Value["icon_url"]
                        .IsNull()) // If no URL is found in the package metadata, use the default image
                    {
                        Texture2D fallbackTexture = null;
                        ASDebug.Log($"Package {package.Key} has no thumbnail. Returning default image");
                        onSuccess?.Invoke(package.Key, fallbackTexture);
                        continue;
                    }

                    if (useCached &&
                        AssetStoreCache.GetCachedTexture(package.Key,
                            out Texture2D texture)) // Try returning cached thumbnails first 
                    {
                        ASDebug.Log($"Returning cached thumbnail for package {package.Key}");
                        onSuccess?.Invoke(package.Key, texture);
                        continue;
                    }

                    var textureBytes =
                        await DownloadPackageThumbnail(package.Value["icon_url"].AsString());
                    Texture2D tex = new Texture2D(1, 1, TextureFormat.RGBA32, false);
                    tex.LoadImage(textureBytes);
                    AssetStoreCache.CacheTexture(package.Key, tex);
                    ASDebug.Log($"Returning downloaded thumbnail for package {package.Key}");
                    onSuccess?.Invoke(package.Key, tex);
                }
                catch (OperationCanceledException)
                {
                    DisposeDownloadCancellation();
                    ASDebug.Log("Package thumbnail download operation cancelled");
                    return;
                }
                catch (Exception e)
                {
                    onFail?.Invoke(package.Key, ASError.GetGenericError(e));
                }
                finally
                {
                    packageEnum.Dispose();
                }
            }
        }

        private static async Task<byte[]> DownloadPackageThumbnail(string url)
        {
            // icon_url is presented without http/https
            Uri uri = new Uri($"https:{url}");

            var textureBytes = await httpClient.GetAsync(uri, s_downloadCancellationSource.Token).
                ContinueWith((response) => response.Result.Content.ReadAsByteArrayAsync().Result, s_downloadCancellationSource.Token);
            s_downloadCancellationSource.Token.ThrowIfCancellationRequested();
            return textureBytes;
        }

        /// <summary>
        /// Retrieve, update the cache and return the updated data for a previously cached package
        /// </summary>
        public static async Task<APIResult> GetRefreshedPackageData(string packageId)
        {
            try
            {
                var refreshedDataJson = await GetPackageDataExtra();
                var refreshedPackage = default(JsonValue);

                // Find the updated package data in the latest data json
                foreach (var p in refreshedDataJson["packages"].AsList())
                {
                    if (p["id"] == packageId)
                    {
                        refreshedPackage = p["versions"].AsList()[p["versions"].AsList().Count - 1];
                        break;
                    }
                }

                if (refreshedPackage.Equals(default(JsonValue)))
                    return new APIResult() { Success = false, Error = ASError.GetGenericError(new MissingMemberException($"Unable to find downloaded package data for package id {packageId}")) };

                // Check if the supplied package id data has been cached and if it contains the corresponding package
                if (!AssetStoreCache.GetCachedPackageMetadata(out JsonValue cachedData) ||
                    !cachedData["packages"].AsDict().ContainsKey(packageId))
                    return new APIResult() { Success = false, Error = ASError.GetGenericError(new MissingMemberException($"Unable to find cached package id {packageId}")) };

                var cachedPackage = cachedData["packages"].AsDict()[packageId];

                // Retrieve the category map
                var categoryJson = await GetCategories(true);
                var categories = CreateCategoryDictionary(categoryJson);

                // Update the package data
                cachedPackage["name"] = refreshedPackage["name"].AsString();
                cachedPackage["status"] = refreshedPackage["status"].AsString();
                cachedPackage["extra_info"].AsDict()["category_info"].AsDict()["id"] = refreshedPackage["category_id"].AsString();
                cachedPackage["extra_info"].AsDict()["category_info"].AsDict()["name"] =
                    categories.ContainsKey(refreshedPackage["category_id"]) ? categories[refreshedPackage["category_id"].AsString()] : "Unknown";
                cachedPackage["extra_info"].AsDict()["modified"] = refreshedPackage["modified"].AsString();
                cachedPackage["extra_info"].AsDict()["size"] = refreshedPackage["size"].AsString();

                AssetStoreCache.CachePackageMetadata(cachedData);
                return new APIResult() { Success = true, Response = cachedPackage };
            }
            catch (OperationCanceledException)
            {
                ASDebug.Log("Package metadata download operation cancelled");
                DisposeDownloadCancellation();
                return new APIResult() { Success = false, SilentFail = true };
            }
            catch (Exception e)
            {
                return new APIResult() { Success = false, Error = ASError.GetGenericError(e) };
            }
        }

        /// <summary>
        /// Retrieve all Unity versions that the given package has already had uploaded content with
        /// </summary>
        /// <param name="packageId"></param>
        /// <param name="versionId"></param>
        /// <returns></returns>
        public static List<string> GetPackageUploadedVersions(string packageId, string versionId)
        {
            var versions = new List<string>();
            try
            {
                // Retrieve the data for already uploaded versions (should prevent interaction with Uploader)
                var versionsTask = Task.Run(() => GetAssetStoreData(APIUri("content", $"preview/{packageId}/{versionId}", SavedSessionId)));
                if (!versionsTask.Wait(5000))
                    throw new TimeoutException("Could not retrieve uploaded versions within a reasonable time interval");

                var versionsJson = versionsTask.Result;
                foreach (var version in versionsJson["content"].AsDict()["unity_versions"].AsList())
                    versions.Add(version.AsString());
            }
            catch (OperationCanceledException)
            {
                ASDebug.Log("Package version download operation cancelled");
                DisposeDownloadCancellation();
            }
            catch (Exception e)
            {
                ASDebug.LogError(e);
            }

            return versions;
        }

        #endregion

        #region Package Upload API

        /// <summary>
        /// Upload a content file (.unitypackage) to a provided package version
        /// </summary>
        /// <param name="versionId"></param>
        /// <param name="packageName">Name of the package. Only used for identifying the package in <see cref="OngoingUpload"/> class</param>
        /// <param name="filePath">Path to the .unitypackage file</param>
        /// <param name="localPackageGuid">The <see cref="AssetDatabase.AssetPathToGUID(string)"/> value of the main content folder for the provided package</param>
        /// <param name="localPackagePath">The local path (relative to the root project folder) of the main content folder for the provided package</param>
        /// <param name="localProjectPath">The path to the project that this package was built from</param>
        /// <returns></returns>
        public static async Task<PackageUploadResult> UploadPackageAsync(string versionId, string packageName, string filePath,
            string localPackageGuid, string localPackagePath, string localProjectPath)
        {
            try
            {
                ASDebug.Log("Upload task starting");
                EditorApplication.LockReloadAssemblies();

                if (!IsUploading) // Only subscribe before the first upload
                    EditorApplication.playModeStateChanged += EditorPlayModeStateChangeHandler;

                var progressData = new OngoingUpload(versionId, packageName);
                ActiveUploads.TryAdd(versionId, progressData);

                var result = await Task.Run(() => UploadPackageTask(progressData, filePath, localPackageGuid, localPackagePath, localProjectPath));

                ActiveUploads.TryRemove(versionId, out OngoingUpload _);

                ASDebug.Log("Upload task finished");
                return result;
            }
            catch (Exception e)
            {
                ASDebug.LogError("Upload task failed with an exception: " + e);
                ActiveUploads.TryRemove(versionId, out OngoingUpload _);
                return PackageUploadResult.PackageUploadFail(ASError.GetGenericError(e));
            }
            finally
            {
                if (!IsUploading) // Only unsubscribe after the last upload
                    EditorApplication.playModeStateChanged -= EditorPlayModeStateChangeHandler;

                EditorApplication.UnlockReloadAssemblies();
            }
        }

        private static PackageUploadResult UploadPackageTask(OngoingUpload currentUpload, string filePath,
            string localPackageGuid, string localPackagePath, string localProjectPath)
        {
            ASDebug.Log("Preparing to upload package within API");
            string api = "asset-store-tools";
            string uri = $"package/{currentUpload.VersionId}/unitypackage";

            Dictionary<string, string> packageParams = new Dictionary<string, string>
            {
                // Note: project_path is currently used to store UI selections
                {"root_guid", localPackageGuid},
                {"root_path", localPackagePath},
                {"project_path", localProjectPath}
            };

            ASDebug.Log($"Creating upload request for {currentUpload.VersionId} {currentUpload.PackageName}");

            FileStream requestFileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);

            bool responseTimedOut = false;
            long chunkSize = 32768;
            try
            {
                ASDebug.Log("Starting upload process...");

                var content = new StreamContent(requestFileStream, (int)chunkSize);
                var response = httpClient.PutAsync(APIUri(api, uri, SavedSessionId, packageParams), content, currentUpload.CancellationToken);

                // Progress tracking
                int updateIntervalMs = 100;
                bool allBytesSent = false;
                DateTime timeOfCompletion = default(DateTime);

                while (!response.IsCompleted)
                {
                    float uploadProgress = (float)requestFileStream.Position / requestFileStream.Length * 100;
                    currentUpload.UpdateProgress(uploadProgress);
                    Thread.Sleep(updateIntervalMs);

                    // A timeout for rare cases, when package uploading reaches 100%, but PutAsync task IsComplete value remains 'False'
                    if (requestFileStream.Position == requestFileStream.Length)
                    {
                        if (!allBytesSent)
                        {
                            allBytesSent = true;
                            timeOfCompletion = DateTime.UtcNow;
                        }
                        else if (DateTime.UtcNow.Subtract(timeOfCompletion).TotalMilliseconds > UploadResponseTimeoutMs)
                        {
                            responseTimedOut = true;
                            currentUpload.Cancel();
                            break;
                        }
                    }
                }

                // 2020.3 - although cancellation token shows a requested cancellation, the HttpClient
                // tends to return a false 'IsCanceled' value, thus yielding an exception when attempting to read the response.
                // For now we'll just check the token as well, but this needs to be investigated later on.
                if (response.IsCanceled || currentUpload.CancellationToken.IsCancellationRequested)
                    currentUpload.CancellationToken.ThrowIfCancellationRequested();

                var responseString = response.Result.Content.ReadAsStringAsync().Result;

                var success = JSONParser.AssetStoreResponseParse(responseString, out ASError error, out JsonValue json);
                ASDebug.Log("Upload response JSON: " + json.ToString());
                if (success)
                    return PackageUploadResult.PackageUploadSuccess();
                else
                    return PackageUploadResult.PackageUploadFail(error);
            }
            catch (OperationCanceledException)
            {
                // Uploading is canceled
                if (!responseTimedOut)
                {
                    ASDebug.Log("Upload operation cancelled");
                    return PackageUploadResult.PackageUploadCancelled();
                }
                else
                {
                    ASDebug.LogWarning("All data has been uploaded, but waiting for the response timed out");
                    return PackageUploadResult.PackageUploadResponseTimeout();
                }
            }
            catch (Exception e)
            {
                ASDebug.LogError("Upload operation encountered an undefined exception: " + e);
                var fullError = e.InnerException != null ? ASError.GetGenericError(e.InnerException) : ASError.GetGenericError(e);
                return PackageUploadResult.PackageUploadFail(fullError);
            }
            finally
            {
                requestFileStream.Dispose();
                currentUpload.Dispose();
            }
        }

        /// <summary>
        /// Cancel the uploading task for a package with the provided package id
        /// </summary>
        public static void AbortPackageUpload(string packageId)
        {
            ActiveUploads[packageId]?.Cancel();
        }

        #endregion

        #region Utility Methods
        
        public static async Task<APIResult> GetLatestAssetStoreToolsVersion()
        {
            try
            {
                var url = "https://api.assetstore.unity3d.com/package/latest-version/115";
                var result = await httpClient.GetAsync(url);

                result.EnsureSuccessStatusCode();

                var resultStr = await result.Content.ReadAsStringAsync();

                var json = JSONParser.SimpleParse(resultStr);

                return new APIResult() { Success = true, Response = json };
            }
            catch (Exception e)
            {
                return new APIResult() { Success = false, Error = ASError.GetGenericError(e) };
            }
        }
        
        private static string GetLicenseHash()
        {
            return UnityEditorInternal.InternalEditorUtility.GetAuthToken().Substring(0, 40);
        }

        private static string GetHardwareHash()
        {
            return UnityEditorInternal.InternalEditorUtility.GetAuthToken().Substring(40, 40);
        }

        private static FormUrlEncodedContent GetLoginContent(Dictionary<string, string> loginData)
        {
            loginData.Add("unityversion", Application.unityVersion);
            loginData.Add("toolversion", ToolVersion);
            loginData.Add("license_hash", GetLicenseHash());
            loginData.Add("hardware_hash", GetHardwareHash());

            return new FormUrlEncodedContent(loginData);
        }

        private static async Task<JsonValue> GetAssetStoreData(Uri uri)
        {
            SetupDownloadCancellation();

            var response = await httpClient.GetAsync(uri, s_downloadCancellationSource.Token)
                .ContinueWith((x) => x.Result.Content.ReadAsStringAsync().Result, s_downloadCancellationSource.Token);
            s_downloadCancellationSource.Token.ThrowIfCancellationRequested();

            if (!JSONParser.AssetStoreResponseParse(response, out var error, out var jsonMainData))
                throw error.Exception;

            return jsonMainData;
        }

        private static Uri APIUri(string apiPath, string endPointPath, string sessionId)
        {
            return APIUri(apiPath, endPointPath, sessionId, null);
        }

        // Method borrowed from A$ tools, could maybe be simplified to only retain what is necessary?
        private static Uri APIUri(string apiPath, string endPointPath, string sessionId, IDictionary<string, string> extraQuery)
        {
            Dictionary<string, string> extraQueryMerged;

            if (extraQuery == null)
                extraQueryMerged = new Dictionary<string, string>();
            else
                extraQueryMerged = new Dictionary<string, string>(extraQuery);

            extraQueryMerged.Add("unityversion", Application.unityVersion);
            extraQueryMerged.Add("toolversion", ToolVersion);
            extraQueryMerged.Add("xunitysession", sessionId);

            string uriPath = $"{AssetStoreProdUrl}/api/{apiPath}/{endPointPath}.json";
            UriBuilder uriBuilder = new UriBuilder(uriPath);

            StringBuilder queryToAppend = new StringBuilder();
            foreach (KeyValuePair<string, string> queryPair in extraQueryMerged)
            {
                string queryName = queryPair.Key;
                string queryValue = Uri.EscapeDataString(queryPair.Value);

                queryToAppend.AppendFormat("&{0}={1}", queryName, queryValue);
            }
            if (!string.IsNullOrEmpty(uriBuilder.Query))
                uriBuilder.Query = uriBuilder.Query.Substring(1) + queryToAppend;
            else
                uriBuilder.Query = queryToAppend.Remove(0, 1).ToString();

            return uriBuilder.Uri;
        }

        private static JsonValue MergePackageData(JsonValue mainPackageData, JsonValue extraPackageData, JsonValue categoryData)
        {
            ASDebug.Log($"Main package data\n{mainPackageData}");
            var mainDataDict = mainPackageData["packages"].AsDict();

            // Most likely both of them will be true at the same time, but better to be safe
            if (mainDataDict.Count == 0 || !extraPackageData.ContainsKey("packages"))
                return new JsonValue();

            ASDebug.Log($"Extra package data\n{extraPackageData}");
            var extraDataDict = extraPackageData["packages"].AsList();

            var categories = CreateCategoryDictionary(categoryData);

            foreach (var md in mainDataDict)
            {
                foreach (var ed in extraDataDict)
                {
                    if (ed["id"].AsString() != md.Key)
                        continue;

                    // Create a field for extra data
                    var extraData = JsonValue.NewDict();

                    // Add category field
                    var categoryEntry = JsonValue.NewDict();

                    var categoryId = ed["category_id"].AsString();
                    var categoryName = categories.ContainsKey(categoryId) ? categories[categoryId] : "Unknown";

                    categoryEntry["id"] = categoryId;
                    categoryEntry["name"] = categoryName;

                    extraData["category_info"] = categoryEntry;

                    // Add modified time and size
                    var versions = ed["versions"].AsList();
                    extraData["modified"] = versions[versions.Count - 1]["modified"];
                    extraData["size"] = versions[versions.Count - 1]["size"];

                    md.Value.AsDict()["extra_info"] = extraData;
                }
            }

            mainPackageData.AsDict()["packages"] = new JsonValue(mainDataDict);
            return mainPackageData;
        }

        private static Dictionary<string, string> CreateCategoryDictionary(JsonValue json)
        {
            var categories = new Dictionary<string, string>();

            var list = json.AsList();

            for (int i = 0; i < list.Count; i++)
            {
                var category = list[i].AsDict();
                if (category["status"].AsString() == "deprecated")
                    continue;
                categories.Add(category["id"].AsString(), category["assetstore_name"].AsString());
            }

            return categories;
        }

        /// <summary>
        /// Check if the account data is for a valid publisher account
        /// </summary>
        /// <param name="json">Json structure retrieved from one of the API login methods</param>
        public static bool IsPublisherValid(JsonValue json, out ASError error)
        {
            error = ASError.GetPublisherNullError(json["name"]);

            if (!json.ContainsKey("publisher"))
                return false;

            // If publisher account is not created - let them know
            return !json["publisher"].IsNull();
        }

        /// <summary>
        /// Cancel all data retrieval tasks
        /// </summary>
        public static void AbortDownloadTasks()
        {
            s_downloadCancellationSource?.Cancel();
        }

        /// <summary>
        /// Cancel all data uploading tasks
        /// </summary>
        public static void AbortUploadTasks()
        {
            foreach (var upload in ActiveUploads)
            {
                AbortPackageUpload(upload.Key);
            }
        }

        private static void SetupDownloadCancellation()
        {
            if (s_downloadCancellationSource != null && s_downloadCancellationSource.IsCancellationRequested)
                DisposeDownloadCancellation();

            if (s_downloadCancellationSource == null)
                s_downloadCancellationSource = new CancellationTokenSource();
        }

        private static void DisposeDownloadCancellation()
        {
            s_downloadCancellationSource?.Dispose();
            s_downloadCancellationSource = null;
        }

        private static void EditorPlayModeStateChangeHandler(PlayModeStateChange state)
        {
            if (state != PlayModeStateChange.ExitingEditMode)
                return;

            EditorApplication.ExitPlaymode();
            EditorUtility.DisplayDialog("Notice", "Entering Play Mode is not allowed while there's a package upload in progress.\n\n" +
                                                  "Please wait until the upload is finished or cancel the upload from the Asset Store Uploader window", "OK");
        }

        private static void OverrideAssetStoreUrl()
        {
            var args = Environment.GetCommandLineArgs();
            for (var i = 0; i < args.Length; i++)
            {
                if (!args[i].Equals("-assetStoreUrl"))
                    continue;

                if (i + 1 >= args.Length)
                    return;

                ASDebug.Log($"Overriding A$ URL to: {args[i + 1]}");
                AssetStoreProdUrl = args[i + 1];
                return;
            }
        }

        #endregion
    }
}