using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Timeline.Entities;
using Timeline.Helpers;
using Timeline.Models;
using Timeline.Models.Validation;
using Timeline.Services.Exceptions;
using static Timeline.Resources.Services.UserService;
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 and auth info.
        /// Thrown when  or  is null.
        /// Thrown when  is of bad format or  is empty.
        /// 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);
        /// 
        /// Get the user id of given username.
        /// 
        /// Username of the user.
        /// The id of the user.
        /// Thrown when  is null.
        /// Thrown when  is of bad format.
        /// Thrown when the user with given username does not exist.
        Task GetUserIdByUsername(string username);
        /// 
        /// List all users.
        /// 
        /// The user info of users.
        Task GetUsers();
        /// 
        /// Create a user with given info.
        /// 
        /// The info of new user.
        /// The password, can't be null or empty.
        /// The the new user.
        /// Thrown when is null.
        /// Thrown when some fields in  is bad.
        /// Thrown when a user with given username already exists.
        /// 
        ///  must not be null and must be a valid username.
        ///  must not be null or empty.
        ///  is false by default (null).
        ///  must be a valid nickname if set. It is empty by default.
        /// Other fields are ignored.
        /// 
        Task CreateUser(User info);
        /// 
        /// Modify a user's info.
        /// 
        /// The id of the user.
        /// The new info. May be null.
        /// The new user info.
        /// Thrown when some fields in  is bad.
        /// Thrown when user with given id does not exist.
        /// 
        /// Only , ,  and  will be used.
        /// If null, then not change.
        /// Other fields are ignored.
        /// Version will increase if password is changed.
        /// 
        ///  must be a valid username if set.
        ///  can't be empty if set.
        ///  must be a valid nickname if set.
        /// 
        /// 
        /// 
        Task ModifyUser(long id, User? info);
        /// 
        /// Modify a user's info.
        /// 
        /// The username of the user.
        /// The new info. May be null.
        /// The new user info.
        /// Thrown when  is null.
        /// Thrown when  is of bad format or some fields in  is bad.
        /// Thrown when user with given id does not exist.
        /// Thrown when user with the newusername already exist.
        /// 
        /// Only ,  and  will be used.
        /// If null, then not change.
        /// Other fields are ignored.
        /// After modified, even if nothing is changed, version will increase.
        /// 
        ///  must be a valid username if set.
        ///  can't be empty if set.
        ///  must be a valid nickname if set.
        /// 
        /// Note: Whether  is set or not, version will increase and not set to the specified value if there is one.
        /// 
        /// 
        Task ModifyUser(string username, User? info);
        /// 
        /// Delete a user of given id.
        /// 
        /// Id of the user to delete.
        /// True if user is deleted, false if user not exist.
        Task DeleteUser(long id);
        /// 
        /// Delete a user of given username.
        /// 
        /// Username of the user to delete. Can't be null.
        /// True if user is deleted, false if user not exist.
        /// Thrown if  is null.
        /// Thrown when  is of bad format.
        Task DeleteUser(string username);
        /// 
        /// Try to change a user's password with old password.
        /// 
        /// The id of user to change password of.
        /// Old password.
        /// New password.
        /// Thrown if  or  is null.
        /// Thrown if  or  is empty.
        /// Thrown if the user with given username does not exist.
        /// Thrown if the old password is wrong.
        Task ChangePassword(long id, string oldPassword, string newPassword);
    }
    public class UserService : IUserService
    {
        private readonly ILogger _logger;
        private readonly DatabaseContext _databaseContext;
        private readonly IPasswordService _passwordService;
        private readonly UsernameValidator _usernameValidator = new UsernameValidator();
        private readonly NicknameValidator _nicknameValidator = new NicknameValidator();
        public UserService(ILogger logger, DatabaseContext databaseContext, IPasswordService passwordService)
        {
            _logger = logger;
            _databaseContext = databaseContext;
            _passwordService = passwordService;
        }
        private void CheckUsernameFormat(string username, string? paramName)
        {
            if (!_usernameValidator.Validate(username, out var message))
            {
                throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionUsernameBadFormat, message), paramName);
            }
        }
        private static void CheckPasswordFormat(string password, string? paramName)
        {
            if (password.Length == 0)
            {
                throw new ArgumentException(ExceptionPasswordEmpty, paramName);
            }
        }
        private void CheckNicknameFormat(string nickname, string? paramName)
        {
            if (!_nicknameValidator.Validate(nickname, out var message))
            {
                throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionNicknameBadFormat, message), paramName);
            }
        }
        private static void ThrowUsernameConflict()
        {
            throw new EntityAlreadyExistException(EntityNames.User, ExceptionUsernameConflict);
        }
        private static User CreateUserFromEntity(UserEntity entity)
        {
            return new User
            {
                Username = entity.Username,
                Administrator = UserRoleConvert.ToBool(entity.Roles),
                Nickname = string.IsNullOrEmpty(entity.Nickname) ? entity.Username : entity.Nickname,
                Id = entity.Id,
                Version = entity.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, nameof(username));
            CheckPasswordFormat(password, nameof(password));
            var entity = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync();
            if (entity == null)
                throw new UserNotExistException(username);
            if (!_passwordService.VerifyPassword(entity.Password, password))
                throw new BadPasswordException(password);
            return CreateUserFromEntity(entity);
        }
        public async Task GetUserById(long id)
        {
            var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync();
            if (user == null)
                throw new UserNotExistException(id);
            return CreateUserFromEntity(user);
        }
        public async Task GetUserByUsername(string username)
        {
            if (username == null)
                throw new ArgumentNullException(nameof(username));
            CheckUsernameFormat(username, nameof(username));
            var entity = await _databaseContext.Users.Where(user => user.Username == username).SingleOrDefaultAsync();
            if (entity == null)
                throw new UserNotExistException(username);
            return CreateUserFromEntity(entity);
        }
        public async Task GetUserIdByUsername(string username)
        {
            if (username == null)
                throw new ArgumentNullException(nameof(username));
            CheckUsernameFormat(username, nameof(username));
            var entity = await _databaseContext.Users.Where(user => user.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
            if (entity == null)
                throw new UserNotExistException(username);
            return entity.Id;
        }
        public async Task GetUsers()
        {
            var entities = await _databaseContext.Users.ToArrayAsync();
            return entities.Select(user => CreateUserFromEntity(user)).ToArray();
        }
        public async Task CreateUser(User info)
        {
            if (info == null)
                throw new ArgumentNullException(nameof(info));
            if (info.Username == null)
                throw new ArgumentException(ExceptionUsernameNull, nameof(info));
            CheckUsernameFormat(info.Username, nameof(info));
            if (info.Password == null)
                throw new ArgumentException(ExceptionPasswordNull, nameof(info));
            CheckPasswordFormat(info.Password, nameof(info));
            if (info.Nickname != null)
                CheckNicknameFormat(info.Nickname, nameof(info));
            var username = info.Username;
            var conflict = await _databaseContext.Users.AnyAsync(u => u.Username == username);
            if (conflict)
                ThrowUsernameConflict();
            var administrator = info.Administrator ?? false;
            var password = info.Password;
            var nickname = info.Nickname;
            var newEntity = new UserEntity
            {
                Username = username,
                Password = _passwordService.HashPassword(password),
                Roles = UserRoleConvert.ToString(administrator),
                Nickname = nickname,
                Version = 1
            };
            _databaseContext.Users.Add(newEntity);
            await _databaseContext.SaveChangesAsync();
            _logger.LogInformation(Log.Format(LogDatabaseCreate,
                ("Id", newEntity.Id), ("Username", username), ("Administrator", administrator)));
            return CreateUserFromEntity(newEntity);
        }
        private void ValidateModifyUserInfo(User? info)
        {
            if (info != null)
            {
                if (info.Username != null)
                    CheckUsernameFormat(info.Username, nameof(info));
                if (info.Password != null)
                    CheckPasswordFormat(info.Password, nameof(info));
                if (info.Nickname != null)
                    CheckNicknameFormat(info.Nickname, nameof(info));
            }
        }
        private async Task UpdateUserEntity(UserEntity entity, User? info)
        {
            if (info != null)
            {
                var username = info.Username;
                if (username != null)
                {
                    var conflict = await _databaseContext.Users.AnyAsync(u => u.Username == username);
                    if (conflict)
                        ThrowUsernameConflict();
                    entity.Username = username;
                }
                var password = info.Password;
                if (password != null)
                {
                    entity.Password = _passwordService.HashPassword(password);
                    entity.Version += 1;
                }
                var administrator = info.Administrator;
                if (administrator.HasValue)
                {
                    entity.Roles = UserRoleConvert.ToString(administrator.Value);
                }
                var nickname = info.Nickname;
                if (nickname != null)
                {
                    entity.Nickname = nickname;
                }
            }
        }
        public async Task ModifyUser(long id, User? info)
        {
            ValidateModifyUserInfo(info);
            var entity = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync();
            if (entity == null)
                throw new UserNotExistException(id);
            await UpdateUserEntity(entity, info);
            await _databaseContext.SaveChangesAsync();
            _logger.LogInformation(LogDatabaseUpdate, ("Id", id));
            return CreateUserFromEntity(entity);
        }
        public async Task ModifyUser(string username, User? info)
        {
            if (username == null)
                throw new ArgumentNullException(nameof(username));
            CheckUsernameFormat(username, nameof(username));
            ValidateModifyUserInfo(info);
            var entity = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync();
            if (entity == null)
                throw new UserNotExistException(username);
            await UpdateUserEntity(entity, info);
            await _databaseContext.SaveChangesAsync();
            _logger.LogInformation(LogDatabaseUpdate, ("Username", username));
            return CreateUserFromEntity(entity);
        }
        public async Task DeleteUser(long id)
        {
            var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync();
            if (user == null)
                return false;
            _databaseContext.Users.Remove(user);
            await _databaseContext.SaveChangesAsync();
            _logger.LogInformation(Log.Format(LogDatabaseRemove, ("Id", id), ("Username", user.Username)));
            return true;
        }
        public async Task DeleteUser(string username)
        {
            if (username == null)
                throw new ArgumentNullException(nameof(username));
            CheckUsernameFormat(username, nameof(username));
            var user = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync();
            if (user == null)
                return false;
            _databaseContext.Users.Remove(user);
            await _databaseContext.SaveChangesAsync();
            _logger.LogInformation(Log.Format(LogDatabaseRemove, ("Id", user.Id), ("Username", username)));
            return true;
        }
        public async Task ChangePassword(long id, string oldPassword, string newPassword)
        {
            if (oldPassword == null)
                throw new ArgumentNullException(nameof(oldPassword));
            if (newPassword == null)
                throw new ArgumentNullException(nameof(newPassword));
            CheckPasswordFormat(oldPassword, nameof(oldPassword));
            CheckPasswordFormat(newPassword, nameof(newPassword));
            var entity = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync();
            if (entity == null)
                throw new UserNotExistException(id);
            if (!_passwordService.VerifyPassword(entity.Password, oldPassword))
                throw new BadPasswordException(oldPassword);
            entity.Password = _passwordService.HashPassword(newPassword);
            entity.Version += 1;
            await _databaseContext.SaveChangesAsync();
            _logger.LogInformation(Log.Format(LogDatabaseUpdate, ("Id", id), ("Operation", "Change password")));
        }
    }
}