using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.AddressableAssets;
using System.Text;
using System.Net.Http;

#if (ENABLE_CCD && UNITY_2019_4_OR_NEWER)
using Unity.Services.Core;
using Unity.Services.Ccd.Management;
using Unity.Services.Ccd.Management.Http;
using Unity.Services.Ccd.Management.Models;
#endif

namespace UnityEditor.AddressableAssets.Settings
{
    /// <summary>
    /// Scriptable Object that holds data source setting information for the profile data source dropdown window
    /// </summary>
    public class ProfileDataSourceSettings : ScriptableObject, ISerializationCallbackReceiver
    {
        const string DEFAULT_PATH = "Assets/AddressableAssetsData";
        const string DEFAULT_NAME = "ProfileDataSourceSettings";
        const string CONTENT_RANGE_HEADER = "Content-Range";
        static string DEFAULT_SETTING_PATH = $"{DEFAULT_PATH}/{DEFAULT_NAME}.asset";

#if ENABLE_CCD
        internal const string MANAGED_ENVIRONMENT = "ManagedEnvironment";
        internal const string MANAGED_BUCKET = "ManagedBucket";
        internal const string MANAGED_BADGE = "ManagedBadge";
#endif

        // paths
        internal static string m_CloudEnvironment = "production";
        internal static string m_GenesisBasePath = "https://api-staging.unity.com";
        internal static string m_CcdClientBasePath = ".client-api-stg.unity3dusercontent.com";
        internal static string m_DashboardBasePath = "https://staging.dashboard.unity3d.com";
        internal static string m_ServicesBasePath = "https://staging.services.unity.com";

        [InitializeOnLoadMethod]
        internal static void InitializeCloudEnvironment()
        {
            var cloudEnvironment = GetCloudEnvironment();
            switch (cloudEnvironment)
            {
                case "staging":
#if ENABLE_CCD
                    CcdManagement.SetBasePath("https://staging.services.unity.com");
#endif
                    m_GenesisBasePath = "https://api-staging.unity.com";
                    m_CcdClientBasePath = ".client-api-stg.unity3dusercontent.com";
                    m_DashboardBasePath = "https://staging.dashboard.unity3d.com";
                    m_ServicesBasePath = "https://staging.services.unity.com";
                    break;
                default:
#if ENABLE_CCD
                    CcdManagement.SetBasePath("https://services.unity.com");
#endif
                    m_GenesisBasePath = "https://api.unity.com";
                    m_CcdClientBasePath = ".client-api.unity3dusercontent.com";
                    m_DashboardBasePath = "https://dashboard.unity3d.com";
                    m_ServicesBasePath = "https://services.unity.com";
                    break;
            }
        }

        const string EnvironmentArg = "-cloudEnvironment";

        internal static string GetCloudEnvironment()
        {
            try
            {
                var commandLineArgs = System.Environment.GetCommandLineArgs();
                var cloudEnvironmentIndex = Array.IndexOf(commandLineArgs, EnvironmentArg);

                if (cloudEnvironmentIndex >= 0 && cloudEnvironmentIndex <= commandLineArgs.Length - 2)
                {
                    return commandLineArgs[cloudEnvironmentIndex + 1];
                }
            }
            catch (Exception e)
            {
                Debug.LogError(e);
            }

            return null;
        }


        /// <summary>
        /// Group types that exist within the settings object
        /// </summary>
        [SerializeField]
        public List<ProfileGroupType> profileGroupTypes = new List<ProfileGroupType>();

        [SerializeField]
        internal List<Environment> environments = new List<Environment>();

        [SerializeField]
        internal Environment currentEnvironment;

        /// <summary>
        /// Creates, if needed, and returns the profile data source settings for the project
        /// </summary>
        /// <param name="path">Desired path to put settings</param>
        /// <param name="settingName">Desired name for settings</param>
        /// <returns></returns>
        public static ProfileDataSourceSettings Create(string path = null, string settingName = null)
        {
            ProfileDataSourceSettings aa;
            var assetPath = DEFAULT_SETTING_PATH;

            if (path != null && settingName != null)
            {
                assetPath = $"{path}/{settingName}.asset";
            }

            aa = AssetDatabase.LoadAssetAtPath<ProfileDataSourceSettings>(assetPath);
            if (aa == null)
            {
                Directory.CreateDirectory(path != null ? path : DEFAULT_PATH);
                aa = CreateInstance<ProfileDataSourceSettings>();
                AssetDatabase.CreateAsset(aa, assetPath);
                aa = AssetDatabase.LoadAssetAtPath<ProfileDataSourceSettings>(assetPath);
                aa.profileGroupTypes = CreateDefaultGroupTypes();
                EditorUtility.SetDirty(aa);
            }

            return aa;
        }

        /// <summary>
        /// Gets the profile data source settings for the project
        /// </summary>
        /// <param name="path"></param>
        /// <param name="settingName"></param>
        /// <returns></returns>
        public static ProfileDataSourceSettings GetSettings(string path = null, string settingName = null)
        {
            ProfileDataSourceSettings aa;
            var assetPath = DEFAULT_SETTING_PATH;

            if (path != null && settingName != null)
            {
                assetPath = $"{path}/{settingName}.asset";
            }

            aa = AssetDatabase.LoadAssetAtPath<ProfileDataSourceSettings>(assetPath);
            if (aa == null)
                return Create();
            return aa;
        }

        /// <summary>
        /// Creates a list of default group types that are automatically added on ProfileDataSourceSettings object creation
        /// </summary>
        /// <returns>List of ProfileGroupTypes: Built-In and Editor Hosted</returns>
        public static List<ProfileGroupType> CreateDefaultGroupTypes() => new List<ProfileGroupType>
        {
            CreateBuiltInGroupType(),
            CreateEditorHostedGroupType(),
#if ENABLE_CCD
            CreateCcdManagerGroupType()
#endif
        };

        static ProfileGroupType CreateBuiltInGroupType()
        {
            ProfileGroupType defaultBuiltIn = new ProfileGroupType(AddressableAssetSettings.LocalGroupTypePrefix);
            defaultBuiltIn.AddVariable(new ProfileGroupType.GroupTypeVariable(AddressableAssetSettings.kBuildPath, AddressableAssetSettings.kLocalBuildPathValue));
            defaultBuiltIn.AddVariable(new ProfileGroupType.GroupTypeVariable(AddressableAssetSettings.kLoadPath, AddressableAssetSettings.kLocalLoadPathValue));
            return defaultBuiltIn;
        }

        static ProfileGroupType CreateEditorHostedGroupType()
        {
            ProfileGroupType defaultRemote = new ProfileGroupType(AddressableAssetSettings.EditorHostedGroupTypePrefix);
            defaultRemote.AddVariable(new ProfileGroupType.GroupTypeVariable(AddressableAssetSettings.kBuildPath, AddressableAssetSettings.kRemoteBuildPathValue));
            defaultRemote.AddVariable(new ProfileGroupType.GroupTypeVariable(AddressableAssetSettings.kLoadPath, AddressableAssetSettings.RemoteLoadPathValue));
            return defaultRemote;
        }

#if ENABLE_CCD
        static ProfileGroupType CreateCcdManagerGroupType()
        {
            string buildPath = $"{AddressableAssetSettings.kCCDBuildDataPath}/{MANAGED_ENVIRONMENT}/{MANAGED_BUCKET}/{MANAGED_BADGE}";
            string loadPath =
 $"https://{CloudProjectSettings.projectId}{m_CcdClientBasePath}/client_api/v1/environments/{{CcdManager.EnvironmentName}}/buckets/{{CcdManager.BucketId}}/release_by_badge/{{CcdManager.Badge}}/entry_by_path/content/?path=";
            ProfileGroupType defaultCcdManager = new ProfileGroupType(AddressableAssetSettings.CcdManagerGroupTypePrefix);
            defaultCcdManager.AddVariable(new ProfileGroupType.GroupTypeVariable(AddressableAssetSettings.kBuildPath, buildPath));
            defaultCcdManager.AddVariable(new ProfileGroupType.GroupTypeVariable(AddressableAssetSettings.kLoadPath, loadPath));
            return defaultCcdManager;
        }
#endif

        /// <summary>
        /// Given a valid profileGroupType, searches the settings and returns, if exists, the profile group type
        /// </summary>
        /// <param name="groupType"></param>
        /// <returns>ProfileGroupType if found, null otherwise</returns>
        public ProfileGroupType FindGroupType(ProfileGroupType groupType)
        {
            ProfileGroupType result = null;
            if (!groupType.IsValidGroupType())
            {
                throw new ArgumentException("Group Type is not valid. Group Type must include a build path and load path variables");
            }

            var buildPath = groupType.GetVariableBySuffix(AddressableAssetSettings.kBuildPath);
            var loadPath = groupType.GetVariableBySuffix(AddressableAssetSettings.kLoadPath);
            foreach (ProfileGroupType settingsGroupType in profileGroupTypes)
            {
                var foundBuildPath = settingsGroupType.ContainsVariable(buildPath);
                var foundLoadPath = settingsGroupType.ContainsVariable(loadPath);
                if (foundBuildPath && foundLoadPath)
                {
                    result = settingsGroupType;
                    break;
                }
            }

            return result;
        }

        /// <summary>
        /// Retrieves a list of ProfileGroupType that matches the given prefix
        /// </summary>
        /// <param name="prefix">prefix to search by</param>
        /// <returns>List of ProfileGroupType</returns>
        public List<ProfileGroupType> GetGroupTypesByPrefix(string prefix)
        {
            return profileGroupTypes.Where((groupType) => groupType.GroupTypePrefix.StartsWith(prefix, StringComparison.Ordinal)).ToList();
        }

#if (ENABLE_CCD && UNITY_2019_4_OR_NEWER)
        /// <summary>
        /// Updates the CCD buckets and badges with the data source settings
        /// </summary>
        /// <param name="projectId">Project Id connected to Unity Services</param>
        /// <param name="showInfoLog">Whether or not to show debug logs or not</param>
        /// <returns>List of ProfileGroupType</returns>
        public static async Task<List<ProfileGroupType>> UpdateCCDDataSourcesAsync(string projectId, bool showInfoLog)
        {
            var settings = GetSettings();

            if (showInfoLog)
            {
                Addressables.Log("Syncing CCD Environments, Buckets, and Badges.");
            }
            
            var profileGroupTypes = new List<ProfileGroupType>();

            var environments = await GetEnvironments();

            if (showInfoLog)
            {
                EditorUtility.DisplayProgressBar("Syncing Profile Data Sources", "Fetching Environments", 0);
                Addressables.Log($"Successfully fetched {environments.Count} environments.");
            }
            profileGroupTypes.AddRange(CreateDefaultGroupTypes());

            try
            {
                var envProgress = 1;
                foreach (var environment in environments)
                {
                    var bucketDictionary = await GetAllBucketsAsync(environment.id);
                    var bucketProgress = 1;
                    foreach (var kvp in bucketDictionary)
                    {

                        var bucket = kvp.Value;
                        if (showInfoLog)
                        {
                            EditorUtility.DisplayProgressBar($"Syncing Environment: {environment.name} ({envProgress} of {environments.Count})", $"Loading {bucket.Name}", (bucketProgress / (float)bucketDictionary.Count));
                        }

                        var badges = await GetAllBadgesAsync(bucket.Id.ToString());
                        if (badges.Count == 0) badges.Add(new CcdBadge(name: "latest"));
                        foreach (var badge in badges)
                        {
                            var groupType =
 new ProfileGroupType($"CCD{ProfileGroupType.k_PrefixSeparator}{projectId}{ProfileGroupType.k_PrefixSeparator}{environment.id}{ProfileGroupType.k_PrefixSeparator}{bucket.Id}{ProfileGroupType.k_PrefixSeparator}{badge.Name}");
                            groupType.AddVariable(new ProfileGroupType.GroupTypeVariable($"{nameof(CcdBucket)}{nameof(CcdBucket.Name)}", bucket.Name));
                            groupType.AddVariable(new ProfileGroupType.GroupTypeVariable($"{nameof(CcdBucket)}{nameof(CcdBucket.Id)}", bucket.Id.ToString()));
                            groupType.AddVariable(new ProfileGroupType.GroupTypeVariable($"{nameof(CcdBadge)}{nameof(CcdBadge.Name)}", badge.Name));
                            groupType.AddVariable(new ProfileGroupType.GroupTypeVariable(nameof(CcdBucket.Attributes.PromoteOnly), bucket.Attributes.PromoteOnly.ToString()));

                            //Adding environment stub here
                            groupType.AddVariable(new ProfileGroupType.GroupTypeVariable($"{nameof(Environment)}{nameof(Environment.name)}", environment.name));
                            groupType.AddVariable(new ProfileGroupType.GroupTypeVariable($"{nameof(Environment)}{nameof(Environment.id)}", environment.id));

                            string buildPath = $"{AddressableAssetSettings.kCCDBuildDataPath}/{environment.id}/{bucket.Id}/{badge.Name}";
                            groupType.AddVariable(new ProfileGroupType.GroupTypeVariable(AddressableAssetSettings.kBuildPath, buildPath));

                            string loadPath =
$"https://{projectId}{m_CcdClientBasePath}/client_api/v1/environments/{environment.name}/buckets/{bucket.Id}/release_by_badge/{badge.Name}/entry_by_path/content/?path=";
                            groupType.AddVariable(new ProfileGroupType.GroupTypeVariable(AddressableAssetSettings.kLoadPath, loadPath));

                            profileGroupTypes.Add(groupType);
                        }
                        bucketProgress++;
                    }
                    envProgress++;
                }

                settings.profileGroupTypes = profileGroupTypes;
                settings.environments = environments.ToList();
                if (showInfoLog) Addressables.Log("Successfully synced CCD Buckets and Badges.");
                EditorUtility.SetDirty(settings);
                AddressableAssetUtility.OpenAssetIfUsingVCIntegration(settings);
            }
            catch (CcdManagementException e)
            {
                throw e;
            }
            finally
            {
                EditorUtility.ClearProgressBar();
            }

            return settings.profileGroupTypes;
        }

        internal static async Task<Dictionary<Guid, CcdBucket>> GetAllBucketsAsync(string environment)
        {
            CcdManagement.SetEnvironmentId(environment);
            int page = 1;
            bool loop = true;
            List<CcdBucket> buckets = new List<CcdBucket>();
            do
            {
                try
                {
                    var listBuckets = await CcdManagement.Instance.ListBucketsAsync(new PageOptions()
                    {
                        Page = page
                    });
                    buckets.AddRange(listBuckets);
                    page++;
                }
                catch (CcdManagementException e)
                {
                    if (e.ErrorCode == CcdManagementErrorCodes.OutOfRange)
                    {
                        loop = false;
                    }
                    else if (e.ErrorCode == CommonErrorCodes.Forbidden)
                    {
                        throw new CcdManagementException(e.ErrorCode, "Unactivated Org. Please activate your organization via the Unity Dashboard");
                    }
                    else
                    {
                        throw e;
                    }
                }
            } while (loop);
            return buckets.ToDictionary(kvp => kvp.Id, kvp => kvp);
        }

        internal static async Task<List<CcdBadge>> GetAllBadgesAsync(string bucketId)
        {
            int page = 1;
            bool loop = true;
            List<CcdBadge> badges = new List<CcdBadge>();
            do
            {
                try
                {
                    var listBadges = await CcdManagement.Instance.ListBadgesAsync(Guid.Parse(bucketId), new PageOptions()
                    {
                        Page = page
                    });
                    badges.AddRange(listBadges);
                    page++;
                }
                catch (CcdManagementException e)
                {
                    if (e.ErrorCode == CcdManagementErrorCodes.OutOfRange)
                    {
                        loop = false;
                    }
                    else
                    {
                        throw e;
                    }
                }
            } while (loop);
            return badges;
        }

		internal static async Task<List<Environment>> GetEnvironments()
        {

            var projectId = CloudProjectSettings.projectId;
            var authToken = await GetAuthToken();
            using (System.Net.Http.HttpClient client = new System.Net.Http.HttpClient())
            {
                client.DefaultRequestHeaders.Add("Authorization", authToken);
                var response = await client.GetAsync(String.Format("{0}/api/unity/legacy/v1/projects/{1}/environments", m_ServicesBasePath, projectId));
                if (!response.IsSuccessStatusCode)
                {
                    throw new Exception(response.ReasonPhrase);
                }
                var data = await response.Content.ReadAsStringAsync();
                var envs = JsonUtility.FromJson<Environments>(data);
                var envList = envs.results.ToList();
                return envList;
            }
        }

        private static async Task<string> GetAuthToken()
        {
            using (System.Net.Http.HttpClient client = new System.Net.Http.HttpClient())
            {
                var jsonString = JsonUtility.ToJson(new Token() { token = CloudProjectSettings.accessToken });
                var url = $"{m_ServicesBasePath}/api/auth/v1/genesis-token-exchange/unity/";
                var clientResponse = await client.PostAsync(url, new StringContent(jsonString, Encoding.UTF8, "application/json"));
                if (!clientResponse.IsSuccessStatusCode)
                {
                    throw new Exception(clientResponse.ReasonPhrase);
                }
                var token = JsonUtility.FromJson<Token>(await clientResponse.Content.ReadAsStringAsync()).token;
                return $"Bearer {token}";
            }
        }

        internal void SetEnvironmentById(string id)
        {
            Environment env = environments.Where(x => x.id == id).FirstOrDefault();
            if (env != null)
            {
                currentEnvironment = env;
            }
            else
            {
                throw new Exception("Unable to find environment by id");
            }
        }
#endif
        void ISerializationCallbackReceiver.OnBeforeSerialize()
        {
        }

        void ISerializationCallbackReceiver.OnAfterDeserialize()
        {
            // Ensure static Group types have the correct string
            // Local
            var types = GetGroupTypesByPrefix(AddressableAssetSettings.LocalGroupTypePrefix);
            if (types == null || types.Count == 0)
                profileGroupTypes.Add(CreateBuiltInGroupType());
            else
            {
                types[0].AddOrUpdateVariable(new ProfileGroupType.GroupTypeVariable(AddressableAssetSettings.kBuildPath,
                    AddressableAssetSettings.kLocalBuildPathValue));
                types[0].AddOrUpdateVariable(new ProfileGroupType.GroupTypeVariable(AddressableAssetSettings.kLoadPath,
                    AddressableAssetSettings.kLocalLoadPathValue));
            }

            // Editor Hosted
            types = GetGroupTypesByPrefix(AddressableAssetSettings.EditorHostedGroupTypePrefix);
            if (types.Count == 0)
                profileGroupTypes.Add(CreateEditorHostedGroupType());
            else
            {
                types[0].AddOrUpdateVariable(new ProfileGroupType.GroupTypeVariable(AddressableAssetSettings.kBuildPath,
                    AddressableAssetSettings.kRemoteBuildPathValue));
                types[0].AddOrUpdateVariable(new ProfileGroupType.GroupTypeVariable(AddressableAssetSettings.kLoadPath,
                    AddressableAssetSettings.RemoteLoadPathValue));
            }
        }

        /// <summary>
        /// Access Token
        /// </summary>
        private class Token
        {
            [SerializeField]
            public string token;
        }

        /// <summary>
        /// Environment Wrapper Object
        /// </summary>
        internal class Environments
        {
            [SerializeField]
            public List<Environment> results;
        }

        /// <summary>
        /// Identity API Environment Object
        /// </summary>
        [Serializable]
        internal class Environment
        {
            [SerializeField]
            public string id;

            [SerializeField]
            public string projectId;

            [SerializeField]
            public string projectGenesisId;

            [SerializeField]
            public string name;

            [SerializeField]
            public bool isDefault;
        }
    }
}