using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Cryptography;

namespace Emby.Server.Implementations.Library
{
    public class DefaultAuthenticationProvider : IAuthenticationProvider, IRequiresResolvedUser
    {
        private readonly ICryptoProvider _cryptographyProvider;
        public DefaultAuthenticationProvider(ICryptoProvider crypto)
        {
            _cryptographyProvider = crypto;
        }

        public string Name => "Default";

        public bool IsEnabled => true;
        
        // This is dumb and an artifact of the backwards way auth providers were designed.
        // This version of authenticate was never meant to be called, but needs to be here for interface compat
        // Only the providers that don't provide local user support use this
        public Task<ProviderAuthenticationResult> Authenticate(string username, string password)
        {
            throw new NotImplementedException();
        }
        
        // This is the verson that we need to use for local users. Because reasons.
        public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser)
        {
            bool success = false;
            if (resolvedUser == null)
            {
                throw new Exception("Invalid username or password");
            }

            // As long as jellyfin supports passwordless users, we need this little block here to accomodate
            if (IsPasswordEmpty(resolvedUser, password))
            {
                return Task.FromResult(new ProviderAuthenticationResult
                {
                    Username = username
                });
            }

            ConvertPasswordFormat(resolvedUser);
            byte[] passwordbytes = Encoding.UTF8.GetBytes(password);

            PasswordHash readyHash = new PasswordHash(resolvedUser.Password);
            byte[] calculatedHash;
            string calculatedHashString;
            if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id) || _cryptographyProvider.DefaultHashMethod == readyHash.Id)
            {
                if (string.IsNullOrEmpty(readyHash.Salt))
                {
                    calculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes);
                    calculatedHashString = BitConverter.ToString(calculatedHash).Replace("-", string.Empty);
                }
                else
                {
                    calculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes, readyHash.SaltBytes);
                    calculatedHashString = BitConverter.ToString(calculatedHash).Replace("-", string.Empty);
                }

                if (calculatedHashString == readyHash.Hash)
                {
                    success = true;
                    // throw new Exception("Invalid username or password");
                }
            }
            else
            {
                throw new Exception(string.Format($"Requested crypto method not available in provider: {readyHash.Id}"));
            }

            // var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase);

            if (!success)
            {
                throw new Exception("Invalid username or password");
            }

            return Task.FromResult(new ProviderAuthenticationResult
            {
                Username = username
            });
        }

        // This allows us to move passwords forward to the newformat without breaking. They are still insecure, unsalted, and dumb before a password change
        // but at least they are in the new format.
        private void ConvertPasswordFormat(User user)
        {
            if (string.IsNullOrEmpty(user.Password))
            {
                return;
            }

            if (!user.Password.Contains("$"))
            {
                string hash = user.Password;
                user.Password = string.Format("$SHA1${0}", hash);
            }
            
            if (user.EasyPassword != null && !user.EasyPassword.Contains("$"))
            {
                string hash = user.EasyPassword;
                user.EasyPassword = string.Format("$SHA1${0}", hash);
            }
        }

        public Task<bool> HasPassword(User user)
        {
            var hasConfiguredPassword = !IsPasswordEmpty(user, GetPasswordHash(user));
            return Task.FromResult(hasConfiguredPassword);
        }

        private bool IsPasswordEmpty(User user, string password)
        {
            return (string.IsNullOrEmpty(user.Password) && string.IsNullOrEmpty(password));
        }

        public Task ChangePassword(User user, string newPassword)
        {
            ConvertPasswordFormat(user);
            // This is needed to support changing a no password user to a password user
            if (string.IsNullOrEmpty(user.Password))
            {
                PasswordHash newPasswordHash = new PasswordHash(_cryptographyProvider);
                newPasswordHash.SaltBytes = _cryptographyProvider.GenerateSalt();
                newPasswordHash.Salt = PasswordHash.ConvertToByteString(newPasswordHash.SaltBytes);
                newPasswordHash.Id = _cryptographyProvider.DefaultHashMethod;
                newPasswordHash.Hash = GetHashedStringChangeAuth(newPassword, newPasswordHash);
                user.Password = newPasswordHash.ToString();
                return Task.CompletedTask;
            }

            PasswordHash passwordHash = new PasswordHash(user.Password);
            if (passwordHash.Id == "SHA1" && string.IsNullOrEmpty(passwordHash.Salt))
            {
                passwordHash.SaltBytes = _cryptographyProvider.GenerateSalt();
                passwordHash.Salt = PasswordHash.ConvertToByteString(passwordHash.SaltBytes);
                passwordHash.Id = _cryptographyProvider.DefaultHashMethod;
                passwordHash.Hash = GetHashedStringChangeAuth(newPassword, passwordHash);
            }
            else if (newPassword != null)
            {
                passwordHash.Hash = GetHashedString(user, newPassword);
            }

            if (string.IsNullOrWhiteSpace(passwordHash.Hash))
            {
                throw new ArgumentNullException(nameof(passwordHash.Hash));
            }

            user.Password = passwordHash.ToString();

            return Task.CompletedTask;
        }

        public string GetPasswordHash(User user)
        {
            return user.Password;
        }

        public string GetHashedStringChangeAuth(string newPassword, PasswordHash passwordHash)
        {
            passwordHash.HashBytes = Encoding.UTF8.GetBytes(newPassword);
            return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash));
        }

        /// <summary>
        /// Gets the hashed string.
        /// </summary>
        public string GetHashedString(User user, string str)
        {
            PasswordHash passwordHash;
            if (string.IsNullOrEmpty(user.Password))
            {
                passwordHash = new PasswordHash(_cryptographyProvider);
            }
            else
            {
                ConvertPasswordFormat(user);
                passwordHash = new PasswordHash(user.Password);
            }

            if (passwordHash.SaltBytes != null)
            {
                // the password is modern format with PBKDF and we should take advantage of that
                passwordHash.HashBytes = Encoding.UTF8.GetBytes(str);
                return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash));
            }
            else
            {
                // the password has no salt and should be called with the older method for safety
                return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash.Id, Encoding.UTF8.GetBytes(str)));
            }
        }
    }
}
