using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Unity.Ugc.Common;
using Unity.Ugc.Model;
using Unity.Ugc.Utils;
using UnityEngine;
using UnityEngine.Networking;

namespace Unity.Ugc
{
    public class UgcClient
    {
        private static string _appId;
        private static string _appSecret;
        private static string _accessToken;
        private static string _refreshToken;
        
        // private const string TraceIdHeader = "Grpc-Metadata-X-Trace-Id";
        private const string FallbackErrorMsg = "Unknown Error";
        private const string UgcJwtTokenPrefix = "bearer ";
        private const string UgcNonceTokenPrefix = "nonce ";
        
        private const int DefaultTimeout = 10;
        private const string SDKName = "UOS_UGC";
        private const string SDKVersion = "0.1.5";

        public static void Initialize(string appId, string appSecret)
        {
            _appId = appId;
            _appSecret = appSecret;
            _accessToken = "";
            _refreshToken = "";
        }
        
        public static void UpdateAuthorizationToken(string accessToken)
        {
            _accessToken = GenAuthorizationToken(accessToken);
        }
        
        public static void UpdateRefreshToken(string refreshToken)
        {
            _refreshToken = refreshToken;
        }

        private static string GenAuthorizationToken(string accessToken)
        {
            if (!string.IsNullOrEmpty(accessToken))
            {
                return UgcJwtTokenPrefix + accessToken;
            }

            return "";
        }
        
        public static Task<UnityWebRequest> GetRaw(
            string url,
            Dictionary<string, object> queryParams = null,
            bool nonceAuth = false,
            bool noAuth = false)
        {
            return RequestRaw(url, UnityWebRequest.kHttpVerbGET, "", queryParams, nonceAuth: nonceAuth);
        }

        public static Task<UnityWebRequest> PostRaw(
            string url,
            string data = "",
            Dictionary<string, object> queryParams = null,
            bool nonceAuth = false,
            bool noAuth = false)
        {
            return RequestRaw(url, UnityWebRequest.kHttpVerbPOST, data, queryParams, nonceAuth: nonceAuth, noAuth: noAuth);
        }

        public static Task<UnityWebRequest> PutRaw(
            string url,
            string data = "",
            Dictionary<string, object> queryParams = null,
            bool nonceAuth = false,
            bool noAuth = false)
        {
            return RequestRaw(url, UnityWebRequest.kHttpVerbPUT, data, queryParams, nonceAuth: nonceAuth);
        }

        public static Task<UnityWebRequest> DeleteRaw(
            string url,
            string data = "",
            Dictionary<string, object> queryParams = null,
            bool nonceAuth = false,
            bool noAuth = false)
        {
            return RequestRaw(url, UnityWebRequest.kHttpVerbDELETE, data, queryParams);
        }

        private static async Task<UnityWebRequest> RequestRaw(
            string url,
            string method,
            string data = "",
            Dictionary<string, object> queryParams = null,
            bool nonceAuth = false,
            bool noAuth = false)
        {
            // Debug.Log($"{method} {url}");
            url = ConstructUrl(url, queryParams);

            var uwr = new UnityWebRequest(url, method);
            ConfigureHeaders(uwr, nonceAuth, noAuth);
            
            uwr.disposeDownloadHandlerOnDispose = true;
            uwr.disposeUploadHandlerOnDispose = true;
            if (data != "")
            {
                uwr.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(data));
            }

            uwr.downloadHandler = new DownloadHandlerBuffer();
            uwr.timeout = DefaultTimeout;

            var sendOperation = uwr.SendWebRequest();
            while (!sendOperation.isDone)
            {
                await Task.Yield();
            }

            return uwr;
        }

        public static Task<T> Get<T>(
            string url,
            Dictionary<string, object> queryParams = null,
            bool canRetry = true,
            Action<UgcException> exceptionHandler = null,
            bool nonceAuth = false)
            where T : class, new()
        {
            return Request<T>(url, UnityWebRequest.kHttpVerbGET, "", queryParams, canRetry, exceptionHandler,
                nonceAuth);
        }

        public static Task<T> Post<T>(
            string url,
            string data = "",
            Dictionary<string, object> queryParams = null,
            bool canRetry = true,
            bool nonceAuth = false)
            where T : class, new()
        {
            return Request<T>(url, UnityWebRequest.kHttpVerbPOST, data, queryParams, canRetry,
                nonceAuth: nonceAuth);
        }

        public static Task<T> Put<T>(
            string url,
            string data = "",
            Dictionary<string, object> queryParams = null,
            bool canRetry = true)
            where T : class, new()
        {
            return Request<T>(url, UnityWebRequest.kHttpVerbPUT, data, queryParams, canRetry);
        }

        public static Task<T> Delete<T>(
            string url,
            string data = "",
            Dictionary<string, object> queryParams = null,
            bool canRetry = true)
            where T : class, new()
        {
            return Request<T>(url, UnityWebRequest.kHttpVerbDELETE, data, queryParams, canRetry);
        }

        private static async Task<T> Request<T>(
            string url,
            string method,
            string data = "",
            Dictionary<string, object> queryParams = null,
            bool canRetry = true,
            Action<UgcException> exceptionHandler = null, 
            bool nonceAuth = false,
            bool noAuth = false)
            where T : class, new()
        {
            url = ConstructUrl(url, queryParams);

            using var uwr = new UnityWebRequest(url, method);
            ConfigureHeaders(uwr, nonceAuth, noAuth);
            
            uwr.disposeDownloadHandlerOnDispose = true;
            uwr.disposeUploadHandlerOnDispose = true;
            if (data != "")
            {
                uwr.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(data));
            }

            uwr.downloadHandler = new DownloadHandlerBuffer();
            uwr.timeout = DefaultTimeout;

            var sendOperation = uwr.SendWebRequest();
            while (!sendOperation.isDone)
            {
                await Task.Yield();
            }

            try
            {
                if (uwr.error == null)
                {
                    if (uwr.responseCode == (long)HttpStatusCode.NoContent)
                    {
                        return new T();
                    }
                    
                    var settings = new JsonSerializerSettings();
                    return JsonConvert.DeserializeObject<T>(uwr.downloadHandler.text, settings);
                }
                
                // Debug.Log($"Request: {method} {url}");
                
                var errorMessage = ParseErrorMessage(uwr);
                if (canRetry && ShouldRefreshToken(errorMessage))
                {
                    await RefreshAccessToken();
                    if (!string.IsNullOrEmpty(_accessToken))
                    {
                        return await Request<T>(url, method, data, queryParams, false);
                    }
                }

                throw new UgcException(errorMessage.Code, errorMessage.Message);
            }
            catch (Exception e)
            {
                switch (e)
                {
                    case UgcException exception:
                        if (exceptionHandler == null)
                        {
                            throw;
                        }

                        exceptionHandler(exception);
                        return new T();
                }

                // Debug.Log(e);
                if (exceptionHandler == null)
                {
                    throw new UgcException((int)ErrorCode.Unknown, e.Message);
                }

                exceptionHandler(new UgcException((int)ErrorCode.Unknown, e.Message));
                return new T();
            }
        }
        
        private static void ConfigureHeaders(UnityWebRequest uwr, bool nonceAuth = false, bool noAuth = false)
        {
            uwr.SetRequestHeader("content-type", "application/json;charset=utf-8");
            uwr.SetRequestHeader("X-SDK-Name", SDKName);
            uwr.SetRequestHeader("X-SDK-Version", SDKVersion);

            if (noAuth)
            {
                return;
            }
            
            if (!string.IsNullOrEmpty(_accessToken) && !nonceAuth && !noAuth)
            {
                uwr.SetRequestHeader("Authorization", _accessToken);
            }

            var nonceToken = SetNonce(uwr);

            // higher priority
            if (nonceAuth)
            {
                uwr.SetRequestHeader("Authorization", UgcNonceTokenPrefix + nonceToken);
            }
        }

        private static string SetNonce(UnityWebRequest uwr)
        {
            var nonce = Guid.NewGuid().ToString();
            var timestamp = EncryptUtils.GetUnixTimeStampSeconds(DateTime.UtcNow);
            var tokenContent = $"{_appId}:{_appSecret}:{timestamp}:{nonce}";
            var token = EncryptUtils.HexString(EncryptUtils.Sha256(tokenContent));
            uwr.SetRequestHeader("X-Timestamp", $"{timestamp}");
            uwr.SetRequestHeader("X-Nonce", nonce);
            uwr.SetRequestHeader("X-Nonce-Token", token);
            uwr.SetRequestHeader("X-AppID", _appId);
            return token;
        }

        private static string ConstructUrl(
            string url,
            Dictionary<string, object> queryParams)
        {
            var str1 = url;
            if (queryParams != null)
            {
                var str2 = string.Join("&", queryParams.Where(kv => kv.Value != null).Select(kv =>
                {
                    if (!kv.Value.GetType().IsArray)
                    {
                        return kv.Key + "=" + Uri.EscapeDataString(kv.Value.ToString());
                    }

                    if (kv.Value is not IEnumerable values)
                    {
                        return "";
                    }

                    return string.Join("&",
                        values.Cast<object>().Select(value => kv.Key + "=" + Uri.EscapeDataString(value.ToString())));
                }));
                str1 = str1 + "?" + str2;
            }

            return str1;
        }

        private static bool ShouldRefreshToken(UgcErrorMessage message)
        {
            return message.Message.Contains("access token expired");
        }

        private static async Task RefreshAccessToken()
        {
            var req = new RefreshAccessTokenRequest()
            {
                AccessToken = _accessToken.Split(" ")[1],
                RefreshToken = _refreshToken
            };

            _accessToken = "";
            _refreshToken = "";
            
            try
            {
                var response = await UgcSDK.Refresh(req);
                if (response != null)
                {
                    UpdateAuthorizationToken(response.AccessToken);
                    UpdateRefreshToken(response.RefreshToken);
                }
            }
            catch (Exception e)
            {
                Debug.Log(e);
            }
        }

        private static UgcErrorMessage ParseErrorMessage(UnityWebRequest request)
        {
            if (request.result == UnityWebRequest.Result.ConnectionError || request.downloadHandler.text == "")
            {
                return new UgcErrorMessage()
                    { Code = (int)ErrorCode.SDKNetworkError, Message = request.error };
            }

            try
            {
                var customError = JsonConvert.DeserializeObject<UgcCustomError>(request.downloadHandler.text);
                var code = customError.Code;
                var message = customError.Reason + " : " + string.Join(";", customError.Details.Cast<string>().Select(value => value));

                return new UgcErrorMessage()
                    { Code = code, Message = message };
            }
            catch (Exception e)
            {
                Debug.Log(e);
                switch (e)
                {
                    case UgcException:
                        throw;
                }

                return new UgcErrorMessage() { Code = (int)ErrorCode.Unknown, Message = FallbackErrorMsg };
            }
        }
    }
}