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 class CreateTokenResult
    {
        public string Token { get; set; } = default!;
        public UserInfo User { get; set; } = default!;
    }
    public interface IUserService
    {
        /// 
        /// Try to anthenticate with the given username and password.
        /// If success, create a token and return the user info.
        /// 
        /// The username of the user to anthenticate.
        /// The password of the user to anthenticate.
        /// The expired time point. Null then use default. See  for what is default.
        /// An  containing the created token and 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 CreateToken(string username, string password, DateTime? expires = null);
        /// 
        /// Verify the given token.
        /// If success, return the user info.
        /// 
        /// The token to verify.
        /// The user info specified by the token.
        /// Thrown when  is null.
        /// Thrown when the token is of bad format. Thrown by .
        /// Thrown when the user specified by the token does not exist. Usually it has been deleted after the token was issued.
        Task VerifyToken(string token);
        /// 
        /// Get the user info of given username.
        /// 
        /// Username of the user.
        /// The info of the user. Null if the user of given username does not exists.
        /// Thrown when  is null.
        /// Thrown when  is of bad format.
        Task GetUser(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);
    }
    internal class UserCache
    {
        public string Username { get; set; } = default!;
        public bool Administrator { get; set; }
        public long Version { get; set; }
        public UserInfo ToUserInfo()
        {
            return new UserInfo(Username, Administrator);
        }
    }
    public class UserService : IUserService
    {
        private readonly ILogger _logger;
        private readonly IMemoryCache _memoryCache;
        private readonly DatabaseContext _databaseContext;
        private readonly IJwtService _jwtService;
        private readonly IPasswordService _passwordService;
        private readonly UsernameValidator _usernameValidator;
        public UserService(ILogger logger, IMemoryCache memoryCache, DatabaseContext databaseContext, IJwtService jwtService, IPasswordService passwordService)
        {
            _logger = logger;
            _memoryCache = memoryCache;
            _databaseContext = databaseContext;
            _jwtService = jwtService;
            _passwordService = passwordService;
            _usernameValidator = new UsernameValidator();
        }
        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? additionalMessage = null)
        {
            var (result, message) = _usernameValidator.Validate(username);
            if (!result)
            {
                if (additionalMessage == null)
                    throw new UsernameBadFormatException(username, message);
                else
                    throw new UsernameBadFormatException(username, additionalMessage + message);
            }
        }
        public async Task CreateToken(string username, string password, DateTime? expires)
        {
            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);
            var token = _jwtService.GenerateJwtToken(new TokenInfo
            {
                Id = user.Id,
                Version = user.Version
            }, expires);
            return new CreateTokenResult
            {
                Token = token,
                User = UserConvert.CreateUserInfo(user)
            };
        }
        public async Task VerifyToken(string token)
        {
            if (token == null)
                throw new ArgumentNullException(nameof(token));
            TokenInfo tokenInfo;
            tokenInfo = _jwtService.VerifyJwtToken(token);
            var id = tokenInfo.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 = UserConvert.CreateUserCache(user);
                _memoryCache.CreateEntry(key).SetValue(cache);
                _logger.LogInformation(Log.Format(Resources.Services.UserService.LogCacheCreate, ("Key", key)));
            }
            if (tokenInfo.Version != cache.Version)
                throw new JwtVerifyException(new JwtBadVersionException(tokenInfo.Version, cache.Version), JwtVerifyException.ErrorCodes.OldVersion);
            return cache.ToUserInfo();
        }
        public async Task GetUser(string username)
        {
            if (username == null)
                throw new ArgumentNullException(nameof(username));
            CheckUsernameFormat(username);
            return await _databaseContext.Users
                .Where(user => user.Name == username)
                .Select(user => UserConvert.CreateUserInfo(user))
                .SingleOrDefaultAsync();
        }
        public async Task ListUsers()
        {
            return await _databaseContext.Users
                .Select(user => UserConvert.CreateUserInfo(user))
                .ToArrayAsync();
        }
        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 User
                {
                    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);
        }
    }
}