using AsbCloudApp.Data.User;
using AsbCloudApp.Exceptions;
using AsbCloudApp.Repositories;
using AsbCloudApp.Services;
using AsbCloudDb.Model;
using Mapster;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;


namespace AsbCloudInfrastructure.Services
{
    /// <inheritdoc/>
    public class AuthService : IAuthService
    {
        private readonly IAsbCloudDbContext db;
        private readonly IUserRepository userRepository;

        public const string issuer = "a";
        public const string audience = "a";
        public static readonly SymmetricSecurityKey securityKey = new(Encoding.ASCII.GetBytes("супер секретный ключ для шифрования"));
        private const string algorithms = SecurityAlgorithms.HmacSha256;

        private static readonly TimeSpan expiresTimespan = TimeSpan.FromDays(365.25);
        private static readonly Encoding encoding = Encoding.UTF8;
        private const int PasswordSaltLength = 5;
        private const string claimIdUser = "id";
        private const string claimNameIdCompany = "idCompany";
        private readonly HashAlgorithm hashAlgorithm;
        private readonly Random rnd;

        public AuthService(IAsbCloudDbContext db, IUserRepository userRepository)
        {
            this.db = db;
            this.userRepository = userRepository;
            hashAlgorithm = SHA384.Create();
            rnd = new Random((int)(DateTime.Now.Ticks % 2147480161));
        }

        /// <inheritdoc/>
        public async Task<UserTokenDto?> LoginAsync(string login, string password,
            CancellationToken token)
        {
            var user = await GetUserByLoginAsync(login, token);
            if (user is null)
                return null;

            if (!CheckPassword(user.PasswordHash, password))
                return null;

            return await MakeUserTokenDto(user, token);
        }

        /// <inheritdoc/>
        public async Task<UserTokenDto?> RefreshAsync(ClaimsPrincipal identity,
            CancellationToken token)
        {
            var login = identity.FindFirst(ClaimsIdentity.DefaultNameClaimType)?.Value;
            if (string.IsNullOrEmpty(login))
                return null;

            var user = await GetUserByLoginAsync(login, token);
            if (user is null)
                return null;

            var dto = await MakeUserTokenDto(user, token);
            return dto;
        }

        /// <inheritdoc/>
        public void Register(UserRegistrationDto userDto)
        {
            var user = db.Users.FirstOrDefault(u => u.Login == userDto.Login) 
                ?? throw new ArgumentInvalidException(nameof(userDto.Login), "Логин уже занят");

            var salt = GenerateSalt();

            var newUser = new User
            {
                IdCompany = userDto.IdCompany,
                IdState = 0,
                Name = userDto.Name,
                Surname = userDto.Surname,
                Patronymic = userDto.Patronymic,
                Email = userDto.Email,
                Phone = userDto.Phone,
                Position = userDto.Position,
                Login = userDto.Login,
                PasswordHash = salt + ComputeHash(salt, userDto.Password),
            };

            db.Users.Add(newUser);
            db.SaveChanges();
            db.RelationUserUserRoles.Add(new RelationUserUserRole
            {
                IdUser = newUser.Id,
                IdUserRole = 2
            });
            db.SaveChanges();
        }

        /// <inheritdoc/>
        public void ChangePassword(string userLogin, string newPassword)
        {
            var user = db.Users.FirstOrDefault(u => u.Login == userLogin) 
                ?? throw new ArgumentInvalidException(nameof(userLogin), "Логин не зарегистрирован");

            var salt = GenerateSalt();
            user.PasswordHash = salt + ComputeHash(salt, newPassword);
            db.SaveChanges();
        }

        /// <inheritdoc/>
        public void ChangePassword(int idUser, string newPassword)
        {
            var user = db.Users.FirstOrDefault(u => u.Id == idUser) 
                ?? throw new ArgumentInvalidException(nameof(idUser), $"Пользователь с idUser:{idUser} не зарегистрирован");

            var salt = GenerateSalt();
            user.PasswordHash = salt + ComputeHash(salt, newPassword);
            db.SaveChanges();
        }

        private async Task<UserTokenDto?> MakeUserTokenDto(User user, CancellationToken token)
        {
            var identity = MakeClaims(user);
            if (identity is null || user.IdState == 0)
                return null;

            var userDto = await userRepository.GetOrDefaultAsync(user.Id, token);
            if (userDto is null)
                return null;

            var dto = userDto.Adapt<UserTokenDto>();
            dto.Permissions = userRepository.GetNestedPermissions(userDto.Id);
            dto.Token = MakeToken(identity.Claims);
            return dto;
        }

        private static string MakeToken(IEnumerable<Claim> claims)
        {
            var now = DateTime.Now;

            var jwt = new JwtSecurityToken(
                    issuer,
                    audience,
                    notBefore: now,
                    claims: claims,
                    expires: now.Add(expiresTimespan),
                    signingCredentials: new SigningCredentials(securityKey, algorithms));

            return new JwtSecurityTokenHandler().WriteToken(jwt);
        }

        private async Task<User?> GetUserByLoginAsync(string login, CancellationToken token = default)
        {
            var user = await db.Users
                .Include(e => e.Company)
                .Where(e => e.Login == login)
                .AsNoTracking()
                .FirstOrDefaultAsync(token)
                .ConfigureAwait(false);
            return user;
        }

        private ClaimsIdentity MakeClaims(User user)
        {
            var claims = new List<Claim>
                {
                    new (claimIdUser, user.Id.ToString()),
                    new (ClaimsIdentity.DefaultNameClaimType, user.Login),
                    new (claimNameIdCompany, user.IdCompany.ToString()),
                };
            var roles = userRepository.GetRolesByIdUser(user.Id);
            if (roles is not null)
                foreach (var role in roles)
                    claims.Add(new Claim(ClaimsIdentity.DefaultRoleClaimType, role.Caption));

            var claimsIdentity = new ClaimsIdentity(claims, "Token", ClaimsIdentity.DefaultNameClaimType, ClaimsIdentity.DefaultRoleClaimType);
            return claimsIdentity;
        }

        private bool CheckPassword(string passwordHash, string password)
        {
            if (passwordHash?.Length == 0 && password.Length == 0)
                return true;

            if (passwordHash?.Length < PasswordSaltLength)
                return false;

            if (passwordHash is null)
                return false;

            var salt = passwordHash[0..PasswordSaltLength];
            var hashDb = passwordHash[PasswordSaltLength..];

            return hashDb == ComputeHash(salt, password);
        }

        private string ComputeHash(string salt, string password)
        {
            var hashBytes = hashAlgorithm.ComputeHash(encoding.GetBytes(salt + password));
            var hashString = BitConverter.ToString(hashBytes)
                    .Replace("-", "")
                    .ToLower();
            return hashString;
        }

        private string GenerateSalt()
        {
            const string saltChars = "sHwiaX7kZT1QRp0cPILGUuK2Sz=9q8lmejDNfoYCE3B_WtgyVv6M5OxAJ4Frbhnd";
            var salt = "";
            for (var i = 0; i < PasswordSaltLength - 1; i++)
                salt += saltChars[rnd.Next(0, saltChars.Length)];
            salt += "|";
            return salt;
        }
    }
}