// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
// Standard Library
using System;
using System.IO;
using System.Text;
// Unity
using UnityEditor;
using UnityEngine;
using UnityEngine.Networking;
// GameKit
using AWS.GameKit.Common;
using AWS.GameKit.Common.Models;
using AWS.GameKit.Runtime.Utils;
using AWS.GameKit.Runtime.Models;
namespace AWS.GameKit.Runtime.Core
{
    /// 
    /// This class provides a high level interface for the SessionManager.
    /// Call SessionManager.Get() to get the singleton instance of this class.
    /// 
    /// 
    /// The SessionManager provides APIs for loading and querying the "awsGameKitClientConfig.yaml" file, also known as "the config file".
    ///
    /// The config file is loaded by calling ReloadConfig() or CopyAndReloadConfig().
    ///
    /// The config file contains settings that are needed at runtime in order to call any of the GameKit feature APIs.
    /// The config file's settings are specific to the currently selected environment code (i.e. "dev", "qa", "prd", etc.).
    /// The environment can be changed in the "Environment & Credentials" page of the AWS GameKit Settings Window.
    ///
    /// The config file is automatically created/updated each time a GameKit feature is created or redeployed, or when the environment changes (i.e. "dev", "qa", "prd", etc.).
    ///
    /// The config file is located in the folder "Assets/AWS GameKit/Resources/" and is automatically included in builds of your game.
    /// It is automatically included because it resides in a folder with the special name "Resources".
    /// Learn more about Unity's special "Resources/" folder name at: https://docs.unity3d.com/Manual/SpecialFolders.html
    /// 
    public class SessionManager : Singleton
    {
        private readonly SessionManagerWrapper _wrapper = SessionManagerWrapper.Get();
        private class ConfigPath
        {
            public const string NAME_WITHOUT_EXTENSION = "awsGameKitClientConfig";
            /// 
            /// The filename of the client config file located in the special "Resources/" folder.
            /// 
            public const string DESTINATION_NAME = NAME_WITHOUT_EXTENSION + ".yaml";
#if UNITY_EDITOR
            /// 
            /// The filename of the client config files located in the environment specific "InstanceFiles/" folders.
            /// 
            /// 
            /// The source and destination files have different extensions (.yml and .yaml respectively).
            /// The .yaml extension is required in order to load the file as a TextAsset with Resources.Load() (see https://docs.unity3d.com/Manual/class-TextAsset.html).
            /// The .yml extension is generated by the underlying AWS GameKit C++ Library.
            /// The file extension is changed when the file is copied to the destination location by CopyAndReloadConfig().
            /// 
            private const string SOURCE_NAME = NAME_WITHOUT_EXTENSION + ".yml";
            private string SourceRelativePathFromPackageFolder => Path.Combine("Editor", "CloudResources", "InstanceFiles", _gameAlias, _environmentCode, SOURCE_NAME);
            internal string SourceAbsolutePath => Path.Combine(GameKitPaths.Get().ASSETS_FULL_PATH, SourceRelativePathFromPackageFolder);
            internal string SourceAssetDatabasePath => Path.Combine(GameKitPaths.Get().ASSETS_RELATIVE_PATH, SourceRelativePathFromPackageFolder);
            internal string DestinationAssetDatabasePath => Path.Combine(GameKitPaths.Get().ASSETS_RESOURCES_RELATIVE_PATH, DESTINATION_NAME);
            
            private readonly string _gameAlias;
            private readonly string _environmentCode;
            public ConfigPath(string gameAlias, string environmentCode)
            {
                _gameAlias = gameAlias;
                _environmentCode = environmentCode;
            }
#endif
        }
        /// 
        /// Set a specified token's value.
        /// 
        /// The type of token to set.
        /// The value of the token
        public void SetToken(TokenType tokenType, string value)
        {
            _wrapper.SessionManagerSetToken(tokenType, value);
        }
        /// 
        /// Check if settings are loaded for the passed in feature.
        /// 
        /// 
        /// These settings are found in the config file, which is described in the class documentation.
        /// The file is loaded by calling either ReloadConfig() or CopyAndReloadConfig().
        /// 
        /// The feature to check.
        /// True if the feature's settings are loaded, false otherwise.
        public bool AreSettingsLoaded(FeatureType featureType)
        {
            return _wrapper.SessionManagerAreSettingsLoaded(featureType);
        }
        /// 
        /// Reload the config file from the special "Resources/" folder.
        /// 
        /// 
        /// The config file is described in the class documentation.
        /// 
        public void ReloadConfig()
        {
            Logging.LogInfo("SessionManagerWrapper::ReloadConfig()");
            TextAsset configFile = Resources.Load(ConfigPath.NAME_WITHOUT_EXTENSION);
            if (configFile == null)
            {
                if (!Application.isPlaying)
                {
                    // Don't log anything during Edit Mode.
                    // The config file won't exist until the user has submitted their AWS Credentials.
                    return;
                }
                string howToFix = Application.isEditor ? "re-enter Play Mode" : "re-build the game";
                    
                Logging.LogError($"The {ConfigPath.DESTINATION_NAME} file is missing. AWS GameKit feature APIs will not work. " +
                                 $"Please make sure at least one AWS GameKit feature is deployed and then {howToFix}.");
                return;
            }
            _wrapper.SessionManagerReloadConfigContents(configFile.text);
        }
        /// 
        /// Inject the CA certificate into the client config and load it.
        /// 
        /// 
        /// Use this for Mobile devices that need a CA Cert for the CURL Http client.
        /// 
        internal void ReloadConfigMobile()
        {
            Logging.LogInfo("SessionManagerWrapper::ReloadConfigMobile()");
#if UNITY_ANDROID
            // Helper to build URI to asset inside a jar/apk
            Func buildJarAssetPath = (string rawAsset) =>
            {
                return $"jar:file://{Application.dataPath}!/assets/raw/{rawAsset}";
            };
            // Helper to read an asset from a jar/apk. This blocks so only use it to read small assets.
            Func readTextFromJarAsset = (string fullPath) =>
            {
                Logging.LogInfo($"Reading asset from jar {fullPath}");
                UnityWebRequest request = UnityWebRequest.Get(fullPath);
                request.SendWebRequest();
                while (!request.isDone)
                {
                    // NO-OP
                }
                if (request.result != UnityWebRequest.Result.Success)
                {
                    Logging.LogError($"Could not read asset {fullPath}, error {request.error}");
                    return string.Empty;
                }
                return request.downloadHandler.text;
            };
            // Load client config text
            string clientConfigPath = buildJarAssetPath(ConfigPath.DESTINATION_NAME);
            string clientConfigContent = readTextFromJarAsset(clientConfigPath);
            // Load CA Certificate and save it to Persistent Data Path if it doesn't exist
            string caCertReadPath = buildJarAssetPath("cacert.pem");
            string caCertWritePath = $"{Application.persistentDataPath}/cacert.pem"; // persistentDataPath ends in /
            if (!File.Exists(caCertWritePath))
            {
                Logging.LogInfo($"Writing CA Certificate to file {caCertWritePath}");
                string caCertContent = readTextFromJarAsset(caCertReadPath);
                File.WriteAllText(caCertWritePath, caCertContent);
            }
            else
            {
                Logging.LogInfo($"CA Certificate found at {caCertWritePath}");
            }
            // Add CA Certificate to client config. We need this for HTTPS calls to the backend
            Logging.LogInfo("Updating Client config");
            StringBuilder sb = new StringBuilder(clientConfigContent);
            sb.Append(System.Environment.NewLine);
            sb.Append($"ca_cert_file: {caCertWritePath}");
            sb.Append(System.Environment.NewLine);
            Logging.LogInfo("Reloading Client config");
            _wrapper.SessionManagerReloadConfigContents(sb.ToString());
#endif
#if UNITY_IPHONE
            TextAsset configFile = Resources.Load(ConfigPath.NAME_WITHOUT_EXTENSION);
            string sslPemDataPath = $"{Application.dataPath}/Security/Certs/cacert.pem";
            StringBuilder sb = new StringBuilder(configFile.text);
            sb.Append(System.Environment.NewLine);
            sb.Append($"ca_cert_file: {sslPemDataPath}");
            sb.Append(System.Environment.NewLine);
            
            _wrapper.SessionManagerReloadConfigContents(sb.ToString());
#endif
        }
        /// 
        /// Releases all resources for the SessionManager.
        /// 
        /// /// Should only be called by the GameKitManager. Calling outside of the manager many cause runtime exceptions. 
        public virtual void Release()
        {
            _wrapper.ReleaseInstance();
        }
#if UNITY_EDITOR
        /// 
        /// Copy the specific environment's config file to the special "Resources/" folder, then reload settings from this config file.
        /// 
        /// 
        /// This method is only called in Editor Mode. It is called whenever a GameKit feature is created or redeployed, or when the environment changes (i.e. "dev", "qa", "prd", etc.).
        ///
        /// The purpose of this method is to persist the current environment's config file to the special "Resources/" folder so it can be loaded by ReloadConfig().
        ///
        /// The config file and the special "Resources/" folder are described in the class documentation.
        /// 
        /// The game's alias, ex: "mygame".
        /// The environment to copy the config from, ex: "dev".
        public void CopyAndReloadConfig(string gameAlias, string environmentCode)
        {
            Debug.Log($"SessionManagerWrapper::CopyAndReloadConfig({nameof(gameAlias)}={gameAlias}, {nameof(environmentCode)}={environmentCode})");
            ConfigPath configPath = new ConfigPath(gameAlias, environmentCode);
            if (!File.Exists(configPath.SourceAbsolutePath))
            {
                Logging.LogError($"Source config file does not exist: {configPath.SourceAbsolutePath}");
                return;
            }
            try
            {
                AssetDatabase.ImportAsset(configPath.SourceAssetDatabasePath, ImportAssetOptions.ForceSynchronousImport);
                AssetDatabase.CopyAsset(configPath.SourceAssetDatabasePath, configPath.DestinationAssetDatabasePath);
                Logging.LogInfo($"Copied config file from {configPath.SourceAssetDatabasePath} to {configPath.DestinationAssetDatabasePath}");
            }
            catch (Exception e)
            {
                Logging.LogException("Error copying config", e);
                ClearSettings();
                return;
            }
            ReloadConfig();
        }
        /// 
        /// Copy the specific environment's config file to the special Mobile package folder that gets deployed to a device.
        /// 
        /// The game's alias, ex: "mygame".
        /// The environment to copy the config from, ex: "dev".
        internal void CopyConfigToMobileAssets(string gameAlias, string environmentCode)
        {
            Debug.Log($"SessionManagerWrapper::CopyConfigToMobileAssets({nameof(gameAlias)}={gameAlias}, {nameof(environmentCode)}={environmentCode})");
            ConfigPath configPath = new ConfigPath(gameAlias, environmentCode);
            if (!File.Exists(configPath.SourceAbsolutePath))
            {
                Logging.LogError($"Source config file does not exist: {configPath.SourceAbsolutePath}");
                return;
            }
#if UNITY_ANDROID
            try
            {
                string androidLibDirFullPath = Path.Combine(GameKitPaths.Get().ASSETS_FULL_PATH, "Plugins", "Android", "GameKitConfig.androidlib").Replace("\\", "/");
                string androidLibRawAssetDirFullPath = Path.Combine(androidLibDirFullPath, "assets", "raw").Replace("\\", "/");
                string androidLibDirRelativePath = Path.Combine(GameKitPaths.Get().ASSETS_RELATIVE_PATH, "Plugins", "Android", "GameKitConfig.androidlib").Replace("\\", "/");
                string androidLibRawAssetDirRelativePath = Path.Combine(androidLibDirRelativePath, "assets", "raw").Replace("\\", "/");
                string androidLibRawAssetConfigPath = Path.Combine(androidLibRawAssetDirRelativePath, ConfigPath.DESTINATION_NAME).Replace("\\", "/");
                AssetDatabase.CopyAsset(configPath.DestinationAssetDatabasePath, androidLibRawAssetConfigPath);
                Logging.LogInfo($"Copied Android config file from {configPath.SourceAssetDatabasePath} to {androidLibRawAssetConfigPath}");
            }
            catch (Exception e)
            {
                Logging.LogException("Error copying Mobile config", e);
                ClearSettings();
                return;
            }
#endif
        }
        /// 
        /// Return true if a config file exists for the environment, false otherwise.
        /// 
        /// The game's alias, ex: "mygame".
        /// The environment to copy the config from, ex: "dev".
        public bool DoesConfigFileExist(string gameAlias, string environmentCode)
        {
            ConfigPath configPath = new ConfigPath(gameAlias, environmentCode);
            return File.Exists(configPath.SourceAbsolutePath);
        }
        /// 
        /// Clear out the currently loaded client config settings.
        /// 
        private void ClearSettings()
        {
            _wrapper.SessionManagerReloadConfigFile(string.Empty);
        }
#endif
        }
}