using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Octokit;

namespace GitHub.Unity
{
    class ApiClient : IApiClient
    {
        public static IApiClient Create(UriString repositoryUrl, IKeychain keychain)
        {
            logger.Trace("Creating ApiClient: {0}", repositoryUrl);

            var credentialStore = keychain.Connect(repositoryUrl);
            var hostAddress = HostAddress.Create(repositoryUrl);

            return new ApiClient(repositoryUrl, keychain,
                new GitHubClient(AppConfiguration.ProductHeader, credentialStore, hostAddress.ApiUri));
        }

        private static readonly Unity.ILogging logger = Unity.Logging.GetLogger<ApiClient>();
        public HostAddress HostAddress { get; }
        public UriString OriginalUrl { get; }

        private readonly IKeychain keychain;
        private readonly IGitHubClient githubClient;
        private readonly ILoginManager loginManager;
        private static readonly SemaphoreSlim sem = new SemaphoreSlim(1);

        IList<Organization> organizationsCache;
        Octokit.User userCache;

        string owner;
        bool? isEnterprise;

        public ApiClient(UriString hostUrl, IKeychain keychain, IGitHubClient githubClient)
        {
            Guard.ArgumentNotNull(hostUrl, nameof(hostUrl));
            Guard.ArgumentNotNull(keychain, nameof(keychain));
            Guard.ArgumentNotNull(githubClient, nameof(githubClient));

            HostAddress = HostAddress.Create(hostUrl);
            OriginalUrl = hostUrl;
            this.keychain = keychain;
            this.githubClient = githubClient;
            loginManager = new LoginManager(keychain, ApplicationInfo.ClientId, ApplicationInfo.ClientSecret);
        }

        public async Task Logout(UriString host)
        {
            await LogoutInternal(host);
        }

        private async Task LogoutInternal(UriString host)
        {
            await loginManager.Logout(host);
        }

        public async Task CreateRepository(NewRepository newRepository, Action<Octokit.Repository, Exception> callback, string organization = null)
        {
            Guard.ArgumentNotNull(callback, "callback");
            await CreateRepositoryInternal(newRepository, callback, organization);
        }

        public async Task GetOrganizations(Action<IList<Organization>> callback)
        {
            Guard.ArgumentNotNull(callback, "callback");
            var organizations = await GetOrganizationInternal();
            callback(organizations);
        }

        public async Task GetCurrentUser(Action<Octokit.User> callback)
        {
            Guard.ArgumentNotNull(callback, "callback");
            var user = await GetCurrentUserInternal();
            callback(user);
        }

        public async Task GetCurrentUserAndOrganizations(Action<Octokit.User, IList<Organization>> callback)
        {
            Guard.ArgumentNotNull(callback, "callback");
            await GetUsersAndOrganizationInternal(callback);
        }

        public async Task Login(string username, string password, Action<LoginResult> need2faCode, Action<bool, string> result)
        {
            Guard.ArgumentNotNull(need2faCode, "need2faCode");
            Guard.ArgumentNotNull(result, "result");

            LoginResultData res = null;
            try
            {
                res = await loginManager.Login(OriginalUrl, githubClient, username, password);
            }
            catch (Exception ex)
            {
                logger.Warning(ex);
                result(false, ex.Message);
                return;
            }

            if (res.Code == LoginResultCodes.CodeRequired)
            {
                var resultCache = new LoginResult(res, result, need2faCode);
                need2faCode(resultCache);
            }
            else
            {
                result(res.Code == LoginResultCodes.Success, res.Message);
            }
        }

        public async Task ContinueLogin(LoginResult loginResult, string code)
        {
            LoginResultData result = null;
            try
            {
                result = await loginManager.ContinueLogin(loginResult.Data, code);
            }
            catch (Exception ex)
            {
                loginResult.Callback(false, ex.Message);
                return;
            }
            if (result.Code == LoginResultCodes.CodeFailed)
            {
                loginResult.TwoFACallback(new LoginResult(result, loginResult.Callback, loginResult.TwoFACallback));
            }
            loginResult.Callback(result.Code == LoginResultCodes.Success, result.Message);
        }

        public async Task<bool> LoginAsync(string username, string password, Func<LoginResult, string> need2faCode)
        {
            Guard.ArgumentNotNull(need2faCode, "need2faCode");

            LoginResultData res = null;
            try
            {
                res = await loginManager.Login(OriginalUrl, githubClient, username, password);
            }
            catch (Exception)
            {
                return false;
            }

            if (res.Code == LoginResultCodes.CodeRequired)
            {
                var resultCache = new LoginResult(res, null, null);
                var code = need2faCode(resultCache);
                return await ContinueLoginAsync(resultCache, need2faCode, code);
            }
            else
            {
                return res.Code == LoginResultCodes.Success;
            }
        }

        public async Task<bool> ContinueLoginAsync(LoginResult loginResult, Func<LoginResult, string> need2faCode, string code)
        {
            LoginResultData result = null;
            try
            {
                result = await loginManager.ContinueLogin(loginResult.Data, code);
            }
            catch (Exception)
            {
                return false;
            }

            if (result.Code == LoginResultCodes.CodeFailed)
            {
                var resultCache = new LoginResult(result, null, null);
                code = need2faCode(resultCache);
                if (String.IsNullOrEmpty(code))
                    return false;
                return await ContinueLoginAsync(resultCache, need2faCode, code);
            }
            return result.Code == LoginResultCodes.Success;
        }

        private async Task CreateRepositoryInternal(NewRepository newRepository, Action<Octokit.Repository, Exception> callback, string organization)
        {
            try
            {
                logger.Trace("Creating repository");

                if (!await EnsureKeychainLoaded())
                {
                    callback(null, new Exception("Keychain Not Loaded"));
                    return;
                }

                Octokit.Repository repository;
                if (!string.IsNullOrEmpty(organization))
                {
                    logger.Trace("Creating repository for organization");

                    repository = await githubClient.Repository.Create(organization, newRepository);
                }
                else
                {
                    logger.Trace("Creating repository for user");

                    repository = await githubClient.Repository.Create(newRepository);
                }

                logger.Trace("Created Repository");

                callback(repository, null);
            }
            catch (Exception ex)
            {
                logger.Error(ex, "Error Creating Repository");
                callback(null, ex);
            }
        }

        private async Task<IList<Organization>> GetOrganizationInternal()
        {
            try
            {
                logger.Trace("Getting Organizations");

                if (!await EnsureKeychainLoaded())
                {
                    return null;
                }

                var organizations = await githubClient.Organization.GetAllForCurrent();

                logger.Trace("Obtained {0} Organizations", organizations?.Count.ToString() ?? "NULL");

                if (organizations != null)
                {
                    organizationsCache = organizations.ToArray();
                }
            }
            catch(Exception ex)
            {
                logger.Error(ex, "Error Getting Organizations");
                throw;
            }

            return organizationsCache;
        }

        private async Task<Octokit.User> GetCurrentUserInternal()
        {
            try
            {
                logger.Trace("Getting Organizations");

                if (!await EnsureKeychainLoaded())
                {
                    return null;
                }

                userCache = await githubClient.User.Current();
            }
            catch(Exception ex)
            {
                logger.Error(ex, "Error Getting Current User");
                throw;
            }

            return userCache;
        }

        private async Task GetUsersAndOrganizationInternal(Action<Octokit.User, IList<Organization>> callback)
        {
            if (!await EnsureKeychainLoaded())
            {
                callback(null, null);
                return;
            }

            var currentUserInternal = GetCurrentUserInternal();
            var organizationInternal = GetOrganizationInternal();

            currentUserInternal.Start(TaskScheduler.Current);
            organizationInternal.Start(TaskScheduler.Current);

            callback(await currentUserInternal,await organizationInternal);
        }

        private async Task<bool> EnsureKeychainLoaded()
        {
            logger.Trace("EnsureKeychainLoaded");

            if (keychain.HasKeys)
            {
                if (!keychain.NeedsLoad)
                {
                    logger.Trace("EnsureKeychainLoaded: Has keys does not need load");
                    return true;
                }

                logger.Trace("EnsureKeychainLoaded: Loading");

                var uriString = keychain.Connections.First().Host;
                var keychainAdapter = await keychain.Load(uriString);

                return keychainAdapter.OctokitCredentials != Credentials.Anonymous;
            }

            logger.Trace("EnsureKeychainLoaded: No keys to load");

            return false;
        }

        public async Task<bool> ValidateCredentials()
        {
            try
            {
                var store = keychain.Connect(OriginalUrl);

                if (store.OctokitCredentials != Credentials.Anonymous)
                {
                    var credential = store.Credential;
                    await githubClient.Authorization.CheckApplicationAuthentication(ApplicationInfo.ClientId, credential.Token);
                }
            }
            catch
            {
                return false;
            }
            return true;
        }
    }
}