﻿using AsmodeeDigital.Common.Plugin.Domain.Data;
using AsmodeeDigital.Common.Plugin.Manager.Event;
using AsmodeeDigital.Common.Plugin.Utils;
using BestHTTP;
using System;
using System.Collections;
using System.Text;
using UnityEngine;

namespace AsmodeeDigital.Common.Plugin.Network
{
    public partial class RestAPI
    {
        public const string DebugModuleName = "RestAPI";

        #region Events
        public event Action<AuthenticationTokens> ConnectionSuccessEvent;

        public event Action<DataUser> EmailFoundEvent;

        public event Action EmailNotFoundEvent;

        public event Action<DataUser> LoginFoundEvent;

        public event Action LoginNotFoundEvent;

        public event Action<AuthenticationTokens> AuthenticationSuccessEvent;

        public event Action<string> AuthenticationErrorEvent;

        public event Action<DataUser> AccountCreatedEvent;

        public event Action<string> AccountCreationErrorEvent;

        public event Action LostPasswordEmailSentEvent;

        public event Action<string> LostPasswordErrorEvent;

        public event Action<string> ErrorEvent;

        public event Action RequestTimedOutEvent;
        #endregion

        /// <summary>
        /// Connection parameters to DoW server
        /// </summary>
        private DoWNetworkParameters _networkParameters;

        /// <summary>
        /// Custom X509 certificate verifier
        /// </summary>
        private CustomVerifier _customVerifyer;

        /// <summary>
        /// OAuth2 access token and refresh token
        /// </summary>
        private AuthenticationTokens _authenticationTokens;

        private string _platform;
        private string _language;

        public RestAPI(DoWNetworkParameters networkParameters)
        {
            _networkParameters = networkParameters;

            _customVerifyer = new CustomVerifier(networkParameters);

            Init();
        }

        private void Init()
        {
            RuntimePlatform platform = Application.platform;

            if (platform == RuntimePlatform.IPhonePlayer)
            {
                _platform = "appstore";
            }
            else if (platform == RuntimePlatform.Android)
            {
                _platform = "googleplay";
            }
            // TODO : What if DRM Free ? Humble Store
            else
            {
                _platform = "steam";
            }

            SetLanguage(Application.systemLanguage);
            AsmoLogger.Info(DebugModuleName, "REST API initialized", new Hashtable() { { "Platform", _platform }, { "Language", _language } });
        }

        public void SetLanguage(SystemLanguage language)
        {
            _language = LanguageHelper.Get2LetterISOCodeFromSystemLanguage(language);
        }

        /// <summary>
        /// Create HTTP User-Agent for REST Requests
        /// </summary>
        /// <returns></returns>
        private string GetUserAgent()
        {
            return String.Format(
                "{0}/{1} {2}/ Unity/{3}", Application.productName, Application.version,
                Application.platform, Application.unityVersion);
        }

        private HTTPRequest RequestCallGet(
            string accessToken, string endpoint, Action<Data> onSuccess,
            Action<DataError> error = null)
        {
            return RequestCall(accessToken, endpoint, true, null, true, HTTPMethods.Get, onSuccess, error);
        }

        private HTTPRequest RequestCallPost(
            string accessToken, string endpoint, bool transformParametersToJson, Hashtable parameters,
            bool useDefaultLogs, Action<string> onSuccess, Action<DataError> error = null)
        {
            return RequestCall(
                accessToken, endpoint, transformParametersToJson, parameters, useDefaultLogs,
                HTTPMethods.Post, onSuccess, error);
        }

        private HTTPRequest RequestCallPost(
            string accessToken, string endpoint, bool transformParametersToJson, Hashtable parameters,
            bool useDefaultLogs, Action<Data> onSuccess, Action<DataError> error = null)
        {
            return RequestCall(
                accessToken, endpoint, transformParametersToJson, parameters, true,
                HTTPMethods.Post, onSuccess, error);
        }

        private HTTPRequest RequestCallDelete(
            string accessToken, string endpoint, Action<Data> onSuccess,
            Action<DataError> error = null)
        {
            return RequestCall(
                accessToken, endpoint, false, null, true, HTTPMethods.Delete, onSuccess, error);
        }

        private HTTPRequest RequestCall(
            string accessToken, string endpoint, bool transformParametersToJson,
            Hashtable parameters, bool useDefaultLogs, BestHTTP.HTTPMethods httpMethod,
            Action<string> onSuccess, Action<DataError> onError = null)
        {
            //---> Build the Uri, always https
            Uri uri = GetApiUri(endpoint);
            Hashtable logParams = new Hashtable() {
                {"method", httpMethod.ToString().ToUpper()},
                {"uri", uri.ToString()}
            };

            BestHTTP.HTTPRequest request = new BestHTTP.HTTPRequest(uri, httpMethod,
                (BestHTTP.HTTPRequest req, BestHTTP.HTTPResponse resp) =>
                {
                    if (resp == null)
                    {
                        AsmoLogger.Error(DebugModuleName + ".receiver", "No response", logParams);
                        return;
                    }

                    //---> Check if the response is an error
                    DataError dataError = JsonUtility.FromJson<DataError>(resp.DataAsText);

                    if (dataError != null && !String.IsNullOrEmpty(dataError.error_description))
                    {
                        if (useDefaultLogs)
                        {
                            logParams.Add("error_code", dataError.error_code);
                            logParams.Add("error_description", dataError.error_description);
                            AsmoLogger.Error(DebugModuleName + ".receiver", "Request Failure", logParams);
                        }

                        if (onError == null && ErrorEvent != null)
                            EventManager.Instance.QueueEvent<string>(ErrorEvent, dataError.error_description);
                        else if (onError != null)
                            onError(dataError);
                    }
                    //---> If not an error, call the data response callback
                    else
                    {
                        if (useDefaultLogs)
                        {
                            logParams.Add("result", resp.DataAsText);
                            AsmoLogger.Info(DebugModuleName + ".receiver", "Request Success", logParams);
                        }

                        if (req.State == HTTPRequestStates.TimedOut && RequestTimedOutEvent != null)
                            RequestTimedOutEvent();
                        else
                            onSuccess(resp.DataAsText);
                    }
                });

            //--- Header
            if (!string.IsNullOrEmpty(accessToken))
                request.AddHeader("Authorization", "bearer " + accessToken);

            request.AddHeader("User-Agent", GetUserAgent());
            //---

            if (httpMethod == HTTPMethods.Post)
            {
                request.AddHeader("Cache-Control", "no-cache");

                if (transformParametersToJson)
                    request.AddHeader("Content-Type", "application/json; charset=UTF-8");
                else
                    request.AddHeader("Content-Type", "application/x-www-form-urlencoded");

                if (parameters != null)
                {
                    if (transformParametersToJson)
                    {
                        string json = MiniJSON.Json.Serialize(parameters);
                        request.RawData = Encoding.UTF8.GetBytes(json);
                    }
                    else
                    {
                        foreach (string key in parameters.Keys)
                        {
                            request.AddField(key, parameters[key].ToString());
                        }
                    }
                }
            }

            request.CustomCertificateVerifyer = _customVerifyer;
            request.UseAlternateSSL = true;
            if (useDefaultLogs)
            {
                logParams.Add("parameters", parameters);
                AsmoLogger.Info(DebugModuleName + ".sender", "Sending Request", logParams);
            }
            request.Send();

            return request;
        }

        private HTTPRequest RequestCall(
            string accessToken, string endpoint, bool transformParametersToJson, Hashtable parameters,
            bool useDefaultLogs, BestHTTP.HTTPMethods httpMethod, Action<Data> onSuccess,
            Action<DataError> error = null)
        {
            return RequestCall(
                accessToken, endpoint, transformParametersToJson, parameters, useDefaultLogs, httpMethod,
                (string response) =>
                {
                    DataContainer dataContainer = new DataContainer();
                    if (!String.IsNullOrEmpty(response))
                    {
                        dataContainer = JsonUtility.FromJson<DataContainer>(response);
                    }

                    onSuccess(dataContainer.data);
                }
                , error);
        }

        protected Uri GetApiUri(string endpoint)
        {
            return new Uri(String.Format("https://{0}{1}", _networkParameters.RestAPIHostName, endpoint));
        }

        /// <summary>
        /// Connection to the DoW Server. Must be called first to obtain a public access token
        /// </summary>
        public HTTPRequest Connect()
        {
            string endpoint = "/main/v2/oauth/token";

            Hashtable parameters = new Hashtable() {
                {"grant_type", "client_credentials"},
                {"client_id", _networkParameters.ClientId},
                {"client_secret", _networkParameters.ClientSecret}
            };

            bool useDefaultLogs = false;

            Hashtable logParams = parameters.Clone() as Hashtable;
            logParams.Remove("client_secret");
            logParams.Add("uri", GetApiUri(endpoint));

            AsmoLogger.Info(DebugModuleName + ".sender", "Authenticating", logParams);

            return RequestCallPost(
                string.Empty, endpoint, false, parameters, useDefaultLogs,
                (string responseText) =>
                {
                    _authenticationTokens = JsonUtility.FromJson<AuthenticationTokens>(responseText);
                    logParams.Add("expires_in", _authenticationTokens.expires_in);
                    logParams.Add("scope", _authenticationTokens.scope);
                    AsmoLogger.Info(DebugModuleName + ".receiver", "Authentication successful", logParams);

                    if (ConnectionSuccessEvent != null)
                    {
                        EventManager.Instance.QueueEvent(ConnectionSuccessEvent, _authenticationTokens);
                    }
                },
                (DataError dataError) =>
                {
                    logParams.Add("error_description", dataError.error_description);
                    AsmoLogger.Error(DebugModuleName + ".receiver", "Authentication failed", logParams);
                }
            );
        }

        /// <summary>
        /// Search a DoW user by his email. More informations here : https://apidoc.daysofwonder.com/#api-user-SearchByEmail
        /// </summary>
        /// <param name="email">Email to search</param>
        public HTTPRequest SearchByEmail(string accessToken, string email)
        {
            string endpoint = String.Format("/main/v1/users?email={0}", Uri.EscapeDataString(email));

            return RequestCallGet(accessToken, endpoint,
            (Data data) =>
            {
                if (data.users.Length > 0)
                {
                    DataUser user = data.users[0];

                    if (EmailFoundEvent != null)
                        EventManager.Instance.QueueEvent<DataUser>(EmailFoundEvent, user);
                }
                else
                {
                    if (EmailNotFoundEvent != null)
                        EventManager.Instance.QueueEvent(EmailNotFoundEvent);
                }
            });
        }

        /// <summary>
        /// Search a DoW User by his login. More informations here : https://apidoc.daysofwonder.com/#api-user-UserSignup
        /// </summary>
        /// <param name="login">Login to search</param>
        /// <param name="useWildcardAtTheEnd">Add a wildcard at the end of the login</param>
        public HTTPRequest SearchByLogin(string accessToken, string login, bool useWildcardAtTheEnd)
        {
            string endpoint = String.Format(
                "/main/v1/users?login={0}{1}", Uri.EscapeDataString(login),
                useWildcardAtTheEnd ? Uri.EscapeDataString("%") : "");

            return RequestCallGet(accessToken, endpoint,
            (Data data) =>
            {
                if (data.users.Length > 0)
                {
                    DataUser user = data.users[0];

                    if (LoginFoundEvent != null)
                        EventManager.Instance.QueueEvent<DataUser>(LoginFoundEvent, user);
                }
                else
                {
                    if (LoginNotFoundEvent != null)
                        EventManager.Instance.QueueEvent(LoginNotFoundEvent);
                }
            });
        }

        /// <summary>
        /// Authentication of a user on the DoW server (to get a private access token)
        /// </summary>
        /// <param name="login">Login of the user</param>
        /// <param name="password">Password of the user</param>
        public HTTPRequest Authentication(string login, string password)
        {
            string endpoint = "/main/v2/oauth/token";

            Hashtable parameters = new Hashtable() {
                {"grant_type", "password"},
                {"client_id", _networkParameters.ClientId},
                {"client_secret", _networkParameters.ClientSecret},
                {"username", login},
                {"password", password}
            };

            bool useDefaultLogs = false;

            Hashtable logParams = parameters.Clone() as Hashtable;
            logParams.Remove("client_secret");
            logParams.Remove("password");
            logParams.Add("uri", GetApiUri(endpoint));

            AsmoLogger.Info(DebugModuleName + ".sender", "Authenticating", logParams);

            return RequestCallPost(
                string.Empty, endpoint, false, parameters, useDefaultLogs,
                (string responseText) =>
                {
                    _authenticationTokens = JsonUtility.FromJson<AuthenticationTokens>(responseText);
                    logParams.Add("expires_in", _authenticationTokens.expires_in);
                    logParams.Add("scope", _authenticationTokens.scope);
                    AsmoLogger.Info(DebugModuleName + ".receiver", "Authentication successful", logParams);

                    if (AuthenticationSuccessEvent != null)
                    {
                        EventManager.Instance.QueueEvent(AuthenticationSuccessEvent, _authenticationTokens);
                    }
                },
                (DataError dataError) =>
                {
                    logParams.Add("error_description", dataError.error_description);
                    AsmoLogger.Error(DebugModuleName + ".receiver", "Authentication failed", logParams);
                    if (AuthenticationErrorEvent != null)
                        EventManager.Instance.QueueEvent<string>(AuthenticationErrorEvent, dataError.error_description);
                });
        }

        /// <summary>
        /// Authentication of a user on the DoW server with a refresh token
        /// </summary>
        ///<param name="refreshToken">Refresh token</param>
        public HTTPRequest AuthenticationWithRefreshToken(string refreshToken)
        {
            string endpoint = "/main/v2/oauth/token";

            Hashtable parameters = new Hashtable() {
                {"grant_type", "refresh_token"},
                {"client_id", _networkParameters.ClientId},
                {"client_secret", _networkParameters.ClientSecret},
                {"refresh_token", refreshToken}
            };

            bool useDefaultLogs = false;

            Hashtable logParams = parameters.Clone() as Hashtable;
            logParams.Remove("client_secret");
            logParams.Remove("refresh_token");
            logParams.Add("uri", GetApiUri(endpoint));

            AsmoLogger.Info(DebugModuleName + ".sender", "Authenticating", logParams);

            return RequestCallPost(
                string.Empty, endpoint, false, parameters, useDefaultLogs,
                (string responseText) =>
                {
                    _authenticationTokens = JsonUtility.FromJson<AuthenticationTokens>(responseText);
                    logParams.Add("expires_in", _authenticationTokens.expires_in);
                    logParams.Add("scope", _authenticationTokens.scope);
                    AsmoLogger.Info(DebugModuleName + ".receiver", "Authentication successful", logParams);

                    if (AuthenticationSuccessEvent != null)
                    {
                        EventManager.Instance.QueueEvent(AuthenticationSuccessEvent, _authenticationTokens);
                    }
                },
                (DataError dataError) =>
                {
                    logParams.Add("error_description", dataError.error_description);
                    AsmoLogger.Error(DebugModuleName + ".receiver", "Authentication failed", logParams);
                    if (AuthenticationErrorEvent != null)
                        EventManager.Instance.QueueEvent<string>(AuthenticationErrorEvent, dataError.error_description);
                });
        }

        /// <summary>
        /// Create a DoW user account
        /// </summary>
        /// <param name="login">Login of the user</param>
        /// <param name="password">Password of the user</param>
        /// <param name="email">Email of the user</param>
        /// <param name="subscribeNewsletter">Subscribe or not to the DoW newsletter</param>
        public HTTPRequest CreateAccount(string accessToken, string login, string password, string email, bool subscribeNewsletter)
        {
            string endpoint = "/main/v1/user";

            Hashtable parameters = new Hashtable() {
                {"login_name", login},
                {"password", password},
                {"email", email},
                {"newsletter", subscribeNewsletter}
            };

            bool useDefaultLogs = false;

            Hashtable logParams = parameters.Clone() as Hashtable;
            logParams.Remove("password");
            logParams.Remove("email");
            logParams.Add("uri", GetApiUri(endpoint));

            AsmoLogger.Info(DebugModuleName + ".sender", "Creating account", logParams);

            return RequestCallPost(
                accessToken, endpoint, true, parameters, useDefaultLogs,
                (Data data) =>
                {
                    AsmoLogger.Info(DebugModuleName + ".receiver", "Account created", logParams);
                    DataUser user = new DataUser(data.user_id, data.login_name);

                    if (AccountCreatedEvent != null)
                        EventManager.Instance.QueueEvent<DataUser>(AccountCreatedEvent, user);
                },
                (DataError dataError) =>
                {
                    logParams.Add("error_code", dataError.error_code);
                    logParams.Add("error_description", dataError.error_description);
                    AsmoLogger.Error(DebugModuleName + ".receiver", "Account creation failed", logParams);
                    if (AccountCreationErrorEvent != null)
                        EventManager.Instance.QueueEvent<string>(AccountCreationErrorEvent, dataError.error_description);
                });
        }

        /// <summary>
        /// Reset the password of a user
        /// </summary>
        /// <param name="userID">DoW user ID</param>
        public HTTPRequest ResetPassword(string accessToken, int userID)
        {
            string endpoint = String.Format("/main/v1/user/{0}/password", userID);

            return RequestCallDelete(
                accessToken, endpoint,
                (Data data) =>
                {
                    if (LostPasswordEmailSentEvent != null)
                        EventManager.Instance.QueueEvent(LostPasswordEmailSentEvent);
                },
                (DataError dataError) =>
                {
                    if (LostPasswordErrorEvent != null)
                        EventManager.Instance.QueueEvent<string>(LostPasswordErrorEvent, dataError.error_description);
                });
        }

        /// <summary>
        /// X509 Certificate verifier
        /// </summary>
        private class CustomVerifier : Org.BouncyCastle.Crypto.Tls.ICertificateVerifyer
        {
            private DoWNetworkParameters _networkParameters = null;

            public CustomVerifier(DoWNetworkParameters networkParameters)
            {
                _networkParameters = networkParameters;
            }

            public bool IsValid(Uri serverUri, Org.BouncyCastle.Asn1.X509.X509CertificateStructure[] certs)
            {
                bool isValid = false;

                //for (int i = 0; i < certs.Length; i++)
                //{
                //    string publicKeyServer = certs[i].SubjectPublicKeyInfo.PublicKeyData.GetString();
                //    Debug.Log(publicKeyServer);
                //}

                for (int i = 0; i < certs.Length && !isValid; i++)
                {
                    string publicKeyServer = certs[i].SubjectPublicKeyInfo.PublicKeyData.GetString();

                    foreach (string key in _networkParameters.RestAPIPinPublicKeys)
                    {
                        if (publicKeyServer == key)
                        {
                            isValid = true;
                            break;
                        }
                    }
                }

                return isValid;
            }
        }
    }
}
