// Unity C# reference source
// Copyright (c) Unity Technologies. For terms of use, see
// https://unity3d.com/legal/licenses/Unity_Reference_Only_License

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor.Connect;

namespace UnityEditor.PackageManager.UI.Internal
{
    internal class AssetStoreRestAPI
    {
        // This instance reference is kept for compatibility reasons, as it is internal visible to the Quick Search package
        // To be addressed further in https://jira.unity3d.com/browse/PAX-1306
        internal static AssetStoreRestAPI instance => ServicesContainer.instance.Resolve<AssetStoreRestAPI>();

        private const string k_PurchasesUri = "/-/api/purchases";
        private const string k_TaggingsUri = "/-/api/taggings";
        private const string k_ProductInfoUri = "/-/api/product";
        private const string k_UpdateInfoUri = "/-/api/legacy-package-update-info";
        private const string k_DownloadInfoUri = "/-/api/legacy-package-download-info";
        private const string k_TermsCheckUri = "/-/api/terms/check";
        private const string k_TermsAcceptUri = "/-/api/terms/accept";

        internal const int k_MaxRetries = 3;
        internal const int k_ClientErrorResponseCode = 400;
        internal const int k_ServerErrorResponseCode = 500;

        internal static string k_ErrorMessage = "Something went wrong. Please try again later.";

        private const int k_GeneralServerError = 599;
        private const int k_GeneralClientError = 499;
        internal static readonly Dictionary<int, string> k_KnownErrors = new Dictionary<int, string>
        {
            [400] = "Bad Request",
            [401] = "Unauthorized",
            [403] = "Forbidden",
            [404] = "Not Found",
            [407] = "Proxy Authentication Required",
            [408] = "Request Timeout",
            [k_GeneralClientError] = "Client Error",
            [500] = "Internal Server Error",
            [502] = "Bad Gateway",
            [503] = "Service Unavailable",
            [504] = "Gateway Timeout",
            [k_GeneralServerError] = "Server Error"
        };

        private static readonly string[] k_Categories =
        {
            "3D",
            "Add-Ons",
            "2D",
            "Audio",
            "Essentials",
            "Templates",
            "Tools",
            "VFX",
            "Decentralization"
        };

        private string m_Host;
        private string host
        {
            get
            {
                if (string.IsNullOrEmpty(m_Host))
                    m_Host = m_UnityConnect.GetConfigurationURL(CloudConfigUrl.CloudPackagesApi);
                return m_Host;
            }
        }

        [NonSerialized]
        private UnityConnectProxy m_UnityConnect;
        [NonSerialized]
        private AssetStoreOAuth m_AssetStoreOAuth;
        [NonSerialized]
        private AssetStoreCache m_AssetStoreCache;
        [NonSerialized]
        private HttpClientFactory m_HttpClientFactory;
        public void ResolveDependencies(UnityConnectProxy unityConnect,
            AssetStoreOAuth assetStoreOAuth,
            AssetStoreCache assetStoreCache,
            HttpClientFactory httpClientFactory)
        {
            m_UnityConnect = unityConnect;
            m_AssetStoreOAuth = assetStoreOAuth;
            m_AssetStoreCache = assetStoreCache;
            m_HttpClientFactory = httpClientFactory;
        }

        public virtual void GetPurchases(string query, Action<Dictionary<string, object>> doneCallbackAction, Action<UIError> errorCallbackAction)
        {
            HandleHttpRequest(() =>
            {
                var httpRequest = m_HttpClientFactory.GetASyncHTTPClient($"{host}{k_PurchasesUri}{query ?? string.Empty}");
                httpRequest.tag = $"GetPurchases{query}";
                return httpRequest;
            }, doneCallbackAction, errorCallbackAction);
        }

        public virtual void AbortGetPurchases(string query)
        {
            // Abort any previous request
            m_HttpClientFactory.AbortByTag($"GetPurchases{query}");
        }

        public virtual void GetCategories(Action<Dictionary<string, object>> doneCallbackAction, Action<UIError> errorCallbackAction)
        {
            IList<string> categories = k_Categories.ToList();
            var result = new Dictionary<string, object>
            {
                ["total"] = categories.Count,
                ["results"] = categories
            };

            doneCallbackAction?.Invoke(result);
        }

        public virtual void GetTaggings(Action<Dictionary<string, object>> doneCallbackAction, Action<UIError> errorCallbackAction)
        {
            // Abort any previous request
            m_HttpClientFactory.AbortByTag("GetTaggings");

            HandleHttpRequest(() =>
            {
                var httpRequest = m_HttpClientFactory.GetASyncHTTPClient($"{host}{k_TaggingsUri}");
                httpRequest.tag = "GetTaggings";
                return httpRequest;
            },
                result =>
                {
                    doneCallbackAction?.Invoke(result);
                },
                errorCallbackAction);
        }

        public virtual void GetProductDetail(long productID, Action<Dictionary<string, object>> doneCallbackAction)
        {
            // Abort any previous request
            m_HttpClientFactory.AbortByTag($"GetProductDetail_{productID}");

            HandleHttpRequest(() =>
            {
                var httpRequest = m_HttpClientFactory.GetASyncHTTPClient($"{host}{k_ProductInfoUri}/{productID}");
                httpRequest.tag = $"GetProductDetail_{productID}";
                return httpRequest;
            },
                result =>
                {
                    doneCallbackAction?.Invoke(result);
                },
                error =>
                {
                    var ret = new Dictionary<string, object> { ["errorMessage"] = error.message };
                    doneCallbackAction?.Invoke(ret);
                });
        }

        public virtual void GetDownloadDetail(long productID, Action<AssetStoreDownloadInfo> doneCallbackAction)
        {
            // Abort any previous request
            m_HttpClientFactory.AbortByTag($"GetDownloadDetail{productID}");

            HandleHttpRequest(() =>
            {
                var httpRequest = m_HttpClientFactory.GetASyncHTTPClient($"{host}{k_DownloadInfoUri}/{productID}");
                httpRequest.tag = $"GetDownloadDetail{productID}";
                return httpRequest;
            },
                result =>
                {
                    var downloadInfo = AssetStoreDownloadInfo.ParseDownloadInfo(result);
                    doneCallbackAction?.Invoke(downloadInfo);
                },
                error =>
                {
                    var downloadInfo = new AssetStoreDownloadInfo
                    {
                        isValid = false,
                        errorMessage = error.message,
                        errorCode = error.operationErrorCode
                    };
                    doneCallbackAction?.Invoke(downloadInfo);
                });
        }

        public virtual void GetProductUpdateDetail(IEnumerable<string> productIds, Action<Dictionary<string, object>> doneCallbackAction)
        {
            if (productIds?.Any() != true)
            {
                doneCallbackAction?.Invoke(new Dictionary<string, object>());
                return;
            }

            var localInfos = new List<AssetStoreLocalInfo>();
            foreach (var productId in productIds)
            {
                var localInfo = m_AssetStoreCache.GetLocalInfo(productId);
                if (localInfo != null && m_AssetStoreCache.GetUpdateInfo(localInfo.uploadId) == null)
                    localInfos.Add(localInfo);
            }
            var localInfosJsonData = Json.Serialize(localInfos.Select(info => info?.ToDictionary() ?? new Dictionary<string, string>()).ToList());

            HandleHttpRequest(() => m_HttpClientFactory.PostASyncHTTPClient($"{host}{k_UpdateInfoUri}", localInfosJsonData),
                result =>
                {
                    var ret = result["result"] as Dictionary<string, object>;
                    doneCallbackAction?.Invoke(ret);
                },
                error =>
                {
                    var ret = new Dictionary<string, object>
                    {
                        ["errorMessage"] = error.message,
                        ["errorCode"] = error.operationErrorCode
                    };
                    doneCallbackAction?.Invoke(ret);
                });
        }

        public virtual void CheckTermsAndConditions(Action<Dictionary<string, object>> doneCallbackAction, Action<UIError> errorCallbackAction)
        {
            // Abort any previous request
            m_HttpClientFactory.AbortByTag("CheckTermsAndConditions");

            HandleHttpRequest(() =>
            {
                var httpRequest = m_HttpClientFactory.GetASyncHTTPClient($"{host}{k_TermsCheckUri}");
                httpRequest.tag = "CheckTermsAndConditions";
                return httpRequest;
            },
                result =>
                {
                    doneCallbackAction?.Invoke(result);
                },
                error =>
                {
                    errorCallbackAction?.Invoke(error);
                });
        }

        public virtual void HandleHttpRequest(IAsyncHTTPClient httpRequest, Action<Dictionary<string, object>> doneCallbackAction, Action<UIError> errorCallbackAction)
        {
            HandleHttpRequest(() => httpRequest, doneCallbackAction, errorCallbackAction);
        }

        private void HandleHttpRequest(Func<IAsyncHTTPClient> httpRequestCreate, Action<Dictionary<string, object>> doneCallbackAction, Action<UIError> errorCallbackAction)
        {
            m_AssetStoreOAuth.FetchAccessToken(
                token =>
                {
                    var maxRetryCount = k_MaxRetries;

                    void DoHttpRequest(Action<int> retryCallbackAction)
                    {
                        var httpRequest = httpRequestCreate();

                        httpRequest.header["Content-Type"] = "application/json";
                        httpRequest.header["Authorization"] = "Bearer " + token.accessToken;
                        httpRequest.doneCallback = request =>
                        {
                            // Ignore if aborted
                            if (request.IsAborted())
                                return;

                            var responseCode = request.responseCode;
                            if (responseCode == 0)
                            {
                                errorCallbackAction?.Invoke(new UIError(UIErrorCode.AssetStoreRestApiError, k_ErrorMessage, operationErrorCode: responseCode));
                                return;
                            }

                            if (responseCode >= k_ServerErrorResponseCode)
                            {
                                retryCallbackAction?.Invoke(responseCode);
                                return;
                            }

                            if (responseCode >= k_ClientErrorResponseCode && responseCode < k_ServerErrorResponseCode)
                            {
                                var errorMessage = k_KnownErrors[k_GeneralClientError];
                                k_KnownErrors.TryGetValue(request.responseCode, out errorMessage);
                                errorCallbackAction?.Invoke(new UIError(UIErrorCode.AssetStoreRestApiError, $"{responseCode} {errorMessage}. {k_ErrorMessage}", operationErrorCode: responseCode));
                                return;
                            }

                            var parsedResult = m_HttpClientFactory.ParseResponseAsDictionary(request);
                            if (parsedResult == null)
                                retryCallbackAction?.Invoke(responseCode);
                            else
                            {
                                if (parsedResult.ContainsKey("errorMessage"))
                                {
                                    var operationErrorCode = parsedResult.ContainsKey("errorCode") ? int.Parse(parsedResult.GetString("errorCode")) : -1;
                                    errorCallbackAction?.Invoke(new UIError(UIErrorCode.AssetStoreRestApiError, parsedResult.GetString("errorMessage"), operationErrorCode: operationErrorCode));
                                }
                                else
                                    doneCallbackAction?.Invoke(parsedResult);
                            }
                        };

                        httpRequest.Begin();
                    }

                    void RetryCallbackAction(int lastResponseCode)
                    {
                        maxRetryCount--;
                        if (maxRetryCount > 0)
                            DoHttpRequest(RetryCallbackAction);
                        else
                        {
                            if (lastResponseCode >= k_ServerErrorResponseCode)
                            {
                                var errorMessage = k_KnownErrors[k_GeneralServerError];
                                k_KnownErrors.TryGetValue(lastResponseCode, out errorMessage);
                                errorCallbackAction?.Invoke(new UIError(UIErrorCode.AssetStoreRestApiError, $"{lastResponseCode} {errorMessage}. {k_ErrorMessage}", operationErrorCode: lastResponseCode));
                            }
                            else
                                errorCallbackAction?.Invoke(new UIError(UIErrorCode.AssetStoreRestApiError, k_ErrorMessage, operationErrorCode: lastResponseCode));
                        }
                    }

                    DoHttpRequest(RetryCallbackAction);
                },
                errorCallbackAction);
        }
    }
}
