// Copyright © Selmo Technology GmbH. All Rights Reserved

using Microsoft.AspNetCore.Identity;
using Microsoft.IdentityModel.Tokens;
using Novus.Shared.Types.Maybe;
using SelmoStudio.Hmi.Plugin.Model.RestApi;
using SelmoStudio.Hmi.Plugin.Model.User;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Text.Json;

namespace SelmoStudio.Hmi.Server.Db.Authentication.Service
{
    public class SelmoAuthenticationService : ISelmoAuthenticationService
    {
        private readonly UserManager<ApplicationUser> _userManager;
        private readonly RoleManager<ApplicationRole> _roleManager;
        private readonly SignInManager<ApplicationUser> _signInManager;
        private readonly IPasswordValidator<ApplicationUser> _passwordValidator;
        private readonly IPasswordHasher<ApplicationUser> _passwordHasher;

        private readonly IConfiguration _configuration;

        public SelmoAuthenticationService(UserManager<ApplicationUser> userManager, RoleManager<ApplicationRole> roleManager,
            SignInManager<ApplicationUser> signInManager, IConfiguration configuration,
            IPasswordValidator<ApplicationUser> passwordValidator, IPasswordHasher<ApplicationUser> passwordHasher)
        {
            _userManager = userManager;
            _roleManager = roleManager;
            _signInManager = signInManager;
            _configuration = configuration;
            _passwordValidator = passwordValidator;
            _passwordHasher = passwordHasher;
        }

        public async Task<List<ApplicationUser>> GetAll()
        {
            var allUsers = _userManager.Users.ToList();

            return allUsers;
        }

        public async Task<bool> Exists(string username)
        {
            var userByUsername = await Find(username);

            return userByUsername.HasValue;
        }

        public async Task<Maybe<ApplicationUser>> Find(string username)
        {
            var userByUsername = await _userManager.FindByNameAsync(username);
            if (userByUsername is null)
            {
                return Maybe.Nothing<ApplicationUser>();
            }

            return Maybe.From(userByUsername);
        }

        public async Task<ApplicationUser> GetByName(string username)
        {
            var userByUsername = await _userManager.FindByNameAsync(username);
            if (userByUsername is null)
            {
                throw new ArgumentException($"User with  username {username} does not exists.");
            }

            return userByUsername;
        }

        public async Task<Maybe<IdentityResult>> Delete(string username)
        {
            var userByUsername = await _userManager.FindByNameAsync(username);
            if (userByUsername is null)
            {
                return Maybe.Nothing<IdentityResult>();
            }

            var result = await _userManager.DeleteAsync(userByUsername);

            return result;
        }

        public async Task<string> Logout(string username, HttpContext context)
        {
            await _signInManager.SignOutAsync();
            return "";
        }

        public async Task<string> Register(RegisterRequest request, HttpContext context)
        {
            var callerUser = await _userManager.GetUserAsync(context.User);
            var roles = _roleManager.Roles.ToList();

            var userByEmail = await _userManager.FindByEmailAsync(request.Email);
            var userByUsername = await _userManager.FindByNameAsync(request.Username);
            if (userByEmail is not null || userByUsername is not null)
            {
                throw new ArgumentException($"User with email {request.Email} or username {request.Username} already exists.");
            }

            ApplicationUser user = new()
            {
                Email = request.Email,
                UserName = request.Username,
                SecurityStamp = Guid.NewGuid().ToString()
            };

            var result = await _userManager.CreateAsync(user, request.Password);

            var claims = request.Permissions.Select(GetClaimForPermission).ToList();
            await _userManager.AddClaimsAsync(user, claims);

            if (!result.Succeeded)
            {
                throw new ArgumentException($"Unable to register user {request.Username} errors: {GetErrorsText(result.Errors)}");
            }

            return $"Try to create user {request.Username}; Result = {result}";
        }

        public async Task<string> Update(UpdateRequest request, HttpContext context)
        {
            var userByEmail = await _userManager.FindByEmailAsync(request.Email);
            var userByUsername = await _userManager.FindByNameAsync(request.OldUsername);

            var foundUser = userByEmail is not null ? userByEmail : userByUsername;
            if (userByEmail is not null || userByUsername is not null)
            {
                if (!string.IsNullOrEmpty(request.Password))
                {
                    var result = await _passwordValidator.ValidateAsync(_userManager, foundUser, request.Password);

                    if (!result.Succeeded)
                    {
                        throw new ArgumentException($"Unable to update user {request.NewUsername} errors: {GetErrorsText(result.Errors)}");
                    }

                    foundUser.PasswordHash = _passwordHasher.HashPassword(foundUser, request.Password);
                }

                foundUser.Email = request.Email;
                foundUser.UserName = request.NewUsername;

                await _userManager.UpdateAsync(foundUser);

                var existingClaims = await _userManager.GetClaimsAsync(foundUser);
                await _userManager.RemoveClaimsAsync(foundUser, existingClaims);
                var newClaims = request.Permissions.Select(GetClaimForPermission).ToList();
                await _userManager.AddClaimsAsync(foundUser, newClaims);
            }

            return "Result = Succeeded";
        }

        private Claim GetClaimForPermission(UserPermission userPermission) => userPermission.PolicyKey switch
        {
            WellKnownPermissions.KeySystemManage => new Claim(WellKnownClaimTypes.Permission, WellKnownPermissionClaims.SystemManaging),
            WellKnownPermissions.KeyWriteProcessValue => new Claim(WellKnownClaimTypes.Permission, WellKnownPermissionClaims.WriteProcessValue),
            WellKnownPermissions.KeyReadProcessValue => new Claim(WellKnownClaimTypes.Permission, WellKnownPermissionClaims.ReadProcessValue),
            WellKnownPermissions.KeyWriteHistoryValue => new Claim(WellKnownClaimTypes.Permission, WellKnownPermissionClaims.WriteHistoryValue),
            WellKnownPermissions.KeyReadHistoryValue => new Claim(WellKnownClaimTypes.Permission, WellKnownPermissionClaims.ReadHistoryValue),
            WellKnownPermissions.KeyWriteSettings => new Claim(WellKnownClaimTypes.Permission, WellKnownPermissionClaims.WriteSettings),
            WellKnownPermissions.KeyReadSettings => new Claim(WellKnownClaimTypes.Permission, WellKnownPermissionClaims.ReadSettings),
            _ => throw new NotSupportedException($"The {userPermission.PolicyKey} is not supported!")
        };

        public async Task<IList<Claim>> GetClaims(ApplicationUser user)
        {
            var claims = await _userManager.GetClaimsAsync(user);
            return claims;
        }

        public async Task<string> Login(LoginRequest request, HttpContext context)
        {
            var user = await _userManager.FindByNameAsync(request.Username);

            if (user is null)
            {
                user = await _userManager.FindByEmailAsync(request.Username);
            }

            if (user is null || !await _userManager.CheckPasswordAsync(user, request.Password))
            {
                throw new ArgumentException($"Unable to authenticate user {request.Username}");
            }

            _signInManager.Context = context;

            var authClaims = await _userManager.GetClaimsAsync(user);

            authClaims.Add(new Claim(ClaimTypes.Name, user.UserName));
            authClaims.Add(new(ClaimTypes.Email, user.Email));
            authClaims.Add(new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()));

            await _signInManager.SignInWithClaimsAsync(user, false, authClaims);

            var token = GetToken(authClaims);

            var tokenAsText = new JwtSecurityTokenHandler().WriteToken(token);

            return tokenAsText;
        }

        public static IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
        {
            var payload = jwt.Split('.')[1];
            var jsonBytes = ParseBase64WithoutPadding(payload);
            var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);
            return keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString()));
        }

        private static byte[] ParseBase64WithoutPadding(string base64)
        {
            switch (base64.Length % 4)
            {
                case 2: base64 += "=="; break;
                case 3: base64 += "="; break;
            }
            return Convert.FromBase64String(base64);
        }

        private JwtSecurityToken GetToken(IEnumerable<Claim> authClaims)
        {
            var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWT:Secret"]));

            var token = new JwtSecurityToken(
                issuer: _configuration["JWT:ValidIssuer"],
                audience: _configuration["JWT:ValidAudience"],
                expires: DateTime.Now.AddHours(15),
                claims: authClaims,
                signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256));

            return token;
        }

        private string GetErrorsText(IEnumerable<IdentityError> errors)
        {
            return string.Join(", ", errors.Select(error => error.Description).ToArray());
        }
    }
}