using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
using System.Threading.Tasks;
using Timeline.Entities;
using Timeline.Helpers;
using Timeline.Models;
using Timeline.Models.Validation;
namespace Timeline.Services
{
    public interface IUserService
    {
        /// 
        /// Try to verify the given username and password.
        /// 
        /// The username of the user to verify.
        /// The password of the user to verify.
        /// The user info.
        /// Thrown when  or  is null.
        /// Thrown when username is of bad format.
        /// Thrown when the user with given username does not exist.
        /// Thrown when password is wrong.
        Task VerifyCredential(string username, string password);
        /// 
        /// Try to get a user by id.
        /// 
        /// The id of the user.
        /// The user info.
        /// Thrown when the user with given id does not exist.
        Task GetUserById(long id);
        /// 
        /// Get the user info of given username.
        /// 
        /// Username of the user.
        /// The info of the user.
        /// Thrown when  is null.
        /// Thrown when  is of bad format.
        /// Thrown when the user with given username does not exist.
        Task GetUserByUsername(string username);
        /// 
        /// List all users.
        /// 
        /// The user info of users.
        Task ListUsers();
        /// 
        /// Create or modify a user with given username.
        /// Username must be match with [a-zA-z0-9-_].
        /// 
        /// Username of user.
        /// Password of user.
        /// Whether the user is administrator.
        /// 
        /// Return  if a new user is created.
        /// Return  if a existing user is modified.
        /// 
        /// Thrown when  or  is null.
        /// Thrown when  is of bad format.
        Task PutUser(string username, string password, bool administrator);
        /// 
        /// Partially modify a user of given username.
        /// 
        /// Note that whether actually modified or not, Version of the user will always increase.
        /// 
        /// Username of the user to modify. Can't be null.
        /// New password. Null if not modify.
        /// Whether the user is administrator. Null if not modify.
        /// Thrown if  is null.
        /// Thrown when  is of bad format.
        /// Thrown if the user with given username does not exist.
        Task PatchUser(string username, string? password, bool? administrator);
        /// 
        /// Delete a user of given username.
        /// 
        /// Username of thet user to delete. Can't be null.
        /// Thrown if  is null.
        /// Thrown when  is of bad format.
        /// Thrown if the user with given username does not exist.
        Task DeleteUser(string username);
        /// 
        /// Try to change a user's password with old password.
        /// 
        /// The name of user to change password of.
        /// The user's old password.
        /// The user's new password.
        /// Thrown if  or  or  is null.
        /// Thrown when  is of bad format.
        /// Thrown if the user with given username does not exist.
        /// Thrown if the old password is wrong.
        Task ChangePassword(string username, string oldPassword, string newPassword);
        /// 
        /// Change a user's username.
        /// 
        /// The user's old username.
        /// The new username.
        /// Thrown if  or  is null.
        /// Thrown if the user with old username does not exist.
        /// Thrown if the  or  is of bad format.
        /// Thrown if user with the new username already exists.
        Task ChangeUsername(string oldUsername, string newUsername);
    }
    public class UserService : IUserService
    {
        private readonly ILogger _logger;
        private readonly DatabaseContext _databaseContext;
        private readonly IMemoryCache _memoryCache;
        private readonly IPasswordService _passwordService;
        private readonly UsernameValidator _usernameValidator = new UsernameValidator();
        public UserService(ILogger logger, IMemoryCache memoryCache, DatabaseContext databaseContext, IPasswordService passwordService)
        {
            _logger = logger;
            _memoryCache = memoryCache;
            _databaseContext = databaseContext;
            _passwordService = passwordService;
        }
        private static string GenerateCacheKeyByUserId(long id) => $"user:{id}";
        private void RemoveCache(long id)
        {
            var key = GenerateCacheKeyByUserId(id);
            _memoryCache.Remove(key);
            _logger.LogInformation(Log.Format(Resources.Services.UserService.LogCacheRemove, ("Key", key)));
        }
        private void CheckUsernameFormat(string username, string? message = null)
        {
            var (result, validationMessage) = _usernameValidator.Validate(username);
            if (!result)
            {
                if (message == null)
                    throw new UsernameBadFormatException(username, validationMessage);
                else
                    throw new UsernameBadFormatException(username, validationMessage, message);
            }
        }
        private static UserInfo CreateUserInfoFromEntity(UserEntity user)
        {
            return new UserInfo
            {
                Id = user.Id,
                Username = user.Name,
                Administrator = UserRoleConvert.ToBool(user.RoleString),
                Version = user.Version
            };
        }
        public async Task VerifyCredential(string username, string password)
        {
            if (username == null)
                throw new ArgumentNullException(nameof(username));
            if (password == null)
                throw new ArgumentNullException(nameof(password));
            CheckUsernameFormat(username);
            // We need password info, so always check the database.
            var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
            if (user == null)
                throw new UserNotExistException(username);
            if (!_passwordService.VerifyPassword(user.EncryptedPassword, password))
                throw new BadPasswordException(password);
            return CreateUserInfoFromEntity(user);
        }
        public async Task GetUserById(long id)
        {
            var key = GenerateCacheKeyByUserId(id);
            if (!_memoryCache.TryGetValue(key, out var cache))
            {
                // no cache, check the database
                var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync();
                if (user == null)
                    throw new UserNotExistException(id);
                // create cache
                cache = CreateUserInfoFromEntity(user);
                _memoryCache.CreateEntry(key).SetValue(cache);
                _logger.LogInformation(Log.Format(Resources.Services.UserService.LogCacheCreate, ("Key", key)));
            }
            return cache;
        }
        public async Task GetUserByUsername(string username)
        {
            if (username == null)
                throw new ArgumentNullException(nameof(username));
            CheckUsernameFormat(username);
            var entity = await _databaseContext.Users
                .Where(user => user.Name == username)
                .SingleOrDefaultAsync();
            if (entity == null)
                throw new UserNotExistException(username);
            return CreateUserInfoFromEntity(entity);
        }
        public async Task ListUsers()
        {
            var entities = await _databaseContext.Users.ToArrayAsync();
            return entities.Select(user => CreateUserInfoFromEntity(user)).ToArray();
        }
        public async Task PutUser(string username, string password, bool administrator)
        {
            if (username == null)
                throw new ArgumentNullException(nameof(username));
            if (password == null)
                throw new ArgumentNullException(nameof(password));
            CheckUsernameFormat(username);
            var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
            if (user == null)
            {
                var newUser = new UserEntity
                {
                    Name = username,
                    EncryptedPassword = _passwordService.HashPassword(password),
                    RoleString = UserRoleConvert.ToString(administrator),
                    Avatar = null
                };
                await _databaseContext.AddAsync(newUser);
                await _databaseContext.SaveChangesAsync();
                _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseCreate,
                    ("Id", newUser.Id), ("Username", username), ("Administrator", administrator)));
                return PutResult.Create;
            }
            user.EncryptedPassword = _passwordService.HashPassword(password);
            user.RoleString = UserRoleConvert.ToString(administrator);
            user.Version += 1;
            await _databaseContext.SaveChangesAsync();
            _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseUpdate,
                ("Id", user.Id), ("Username", username), ("Administrator", administrator)));
            //clear cache
            RemoveCache(user.Id);
            return PutResult.Modify;
        }
        public async Task PatchUser(string username, string? password, bool? administrator)
        {
            if (username == null)
                throw new ArgumentNullException(nameof(username));
            CheckUsernameFormat(username);
            var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
            if (user == null)
                throw new UserNotExistException(username);
            if (password != null)
            {
                user.EncryptedPassword = _passwordService.HashPassword(password);
            }
            if (administrator != null)
            {
                user.RoleString = UserRoleConvert.ToString(administrator.Value);
            }
            user.Version += 1;
            await _databaseContext.SaveChangesAsync();
            _logger.LogInformation(Resources.Services.UserService.LogDatabaseUpdate, ("Id", user.Id));
            //clear cache
            RemoveCache(user.Id);
        }
        public async Task DeleteUser(string username)
        {
            if (username == null)
                throw new ArgumentNullException(nameof(username));
            CheckUsernameFormat(username);
            var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
            if (user == null)
                throw new UserNotExistException(username);
            _databaseContext.Users.Remove(user);
            await _databaseContext.SaveChangesAsync();
            _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseRemove,
                ("Id", user.Id)));
            //clear cache
            RemoveCache(user.Id);
        }
        public async Task ChangePassword(string username, string oldPassword, string newPassword)
        {
            if (username == null)
                throw new ArgumentNullException(nameof(username));
            if (oldPassword == null)
                throw new ArgumentNullException(nameof(oldPassword));
            if (newPassword == null)
                throw new ArgumentNullException(nameof(newPassword));
            CheckUsernameFormat(username);
            var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
            if (user == null)
                throw new UserNotExistException(username);
            var verifyResult = _passwordService.VerifyPassword(user.EncryptedPassword, oldPassword);
            if (!verifyResult)
                throw new BadPasswordException(oldPassword);
            user.EncryptedPassword = _passwordService.HashPassword(newPassword);
            user.Version += 1;
            await _databaseContext.SaveChangesAsync();
            _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseUpdate,
                ("Id", user.Id), ("Operation", "Change password")));
            //clear cache
            RemoveCache(user.Id);
        }
        public async Task ChangeUsername(string oldUsername, string newUsername)
        {
            if (oldUsername == null)
                throw new ArgumentNullException(nameof(oldUsername));
            if (newUsername == null)
                throw new ArgumentNullException(nameof(newUsername));
            CheckUsernameFormat(oldUsername, Resources.Services.UserService.ExceptionOldUsernameBadFormat);
            CheckUsernameFormat(newUsername, Resources.Services.UserService.ExceptionNewUsernameBadFormat);
            var user = await _databaseContext.Users.Where(u => u.Name == oldUsername).SingleOrDefaultAsync();
            if (user == null)
                throw new UserNotExistException(oldUsername);
            var conflictUser = await _databaseContext.Users.Where(u => u.Name == newUsername).SingleOrDefaultAsync();
            if (conflictUser != null)
                throw new UsernameConfictException(newUsername);
            user.Name = newUsername;
            user.Version += 1;
            await _databaseContext.SaveChangesAsync();
            _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseUpdate,
                ("Id", user.Id), ("Old Username", oldUsername), ("New Username", newUsername)));
            RemoveCache(user.Id);
        }
    }
}