using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using DiplomovaPrace.Contracts.Security;
using DiplomovaPrace.Contracts.Security.InputModels;
using DiplomovaPrace.Contracts.Security.ViewModels;
using DiplomovaPrace.DataLayer.Repositories.Security;
using DiplomovaPrace.Model.Security;
using DiplomovaPrace.Services.Encoders;
using DiplomovaPrace.Services.Mailer;
using Grpc.Core;
using Havit.Data.Patterns.DataLoaders;
using Havit.Data.Patterns.UnitOfWorks;
using Havit.Extensions.DependencyInjection.Abstractions;
using Havit.Security;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;

namespace DiplomovaPrace.Services.Security
{
	[Service]
	public class SecurityService : ISecurityService, ISecurityServiceContract
	{
		private readonly ILoginAccountRepository loginAccountRepository;
		private readonly JwtConfiguration jwtConfiguration;
		private readonly IRoleRepository roleRepository;
		private readonly IUnitOfWork unitOfWork;
		private readonly ICryptographyService cryptographyService;
		private readonly IEncoderService encoderService;
		private readonly IMailerService mailerService;
		private readonly IDataLoader dataLoader;

		public SecurityService(
			ILoginAccountRepository loginAccountRepository,
			IOptions<JwtConfiguration> jwtConfiguration,
			IRoleRepository roleRepository,
			IUnitOfWork unitOfWork,
			ICryptographyService cryptographyService,
			IEncoderService encoderService,
			IMailerService mailerService,
			IDataLoader dataLoader)
		{
			this.loginAccountRepository = loginAccountRepository;
			this.jwtConfiguration = jwtConfiguration.Value;
			this.roleRepository = roleRepository;
			this.unitOfWork = unitOfWork;
			this.cryptographyService = cryptographyService;
			this.encoderService = encoderService;
			this.mailerService = mailerService;
			this.dataLoader = dataLoader;
		}

		public async Task<string> AuthenticateUserAsync(Contracts.Customer.Security.InputModels.LoginAccountIM inputModel)
		{
			LoginAccount loginAccount = await loginAccountRepository.GetByEmailAsync(inputModel.Email);
			await dataLoader.LoadAsync(loginAccount, i => i.Role);

			if (loginAccount is not null && loginAccount.Role.Name == "Uživatel")
			{
				// Zkontrolovat, jestli nevadi prazdne jmeno a prijmeni
				return PasswordHashService.VerifySHA512HashString(inputModel.Password, loginAccount.PasswordSalt, loginAccount.PasswordHash)
				? CreateToken(loginAccount.ID.ToString(), "Uživatel", loginAccount.FirstName ?? "", loginAccount.LastName ?? "")
				: null;
			}

			return null;
		}

		/// <summary>
		/// Changes user password
		/// </summary>
		/// <param name="inputModel">Object with old and new password</param>
		/// <returns>Returns true if operation was successful and false if not.</returns>
		public async Task<bool> ChangeUserPasswordAsync(Contracts.Customer.Security.InputModels.PasswordChangeIM inputModel)
		{
			LoginAccount loginAccount = await loginAccountRepository.GetObjectAsync(inputModel.UserID).ConfigureAwait(false);

			if (loginAccount is not null && PasswordHashService.VerifySHA512HashString(inputModel.OldPassword, loginAccount.PasswordSalt, loginAccount.PasswordHash))
			{
				loginAccount.PasswordHash = PasswordHashService.ComputeSHA512HashString(inputModel.Password, loginAccount.PasswordSalt);

				unitOfWork.AddForUpdate(loginAccount);
				await unitOfWork.CommitAsync().ConfigureAwait(false);

				return true;
			}

			return false;
		}

		public async Task<bool> RegisterUserAsync(Contracts.Customer.Security.InputModels.RegisterUserIM inputModel)
		{
			LoginAccount loginAccount = await loginAccountRepository.GetByEmailAsync(inputModel.Email);

			if (loginAccount is not null)
			{
				return false;
			}

			List<Model.Security.Role> roles = await roleRepository.GetAllAsync();
			string salt = PasswordHashService.CreateSalt();

			LoginAccount newLoginAccount = new()
			{
				Email = inputModel.Email,
				PasswordHash = PasswordHashService.ComputeSHA512HashString(inputModel.Password, salt),
				PasswordSalt = salt,
				RoleID = roles.Find(i => i.Name == "Uživatel").ID
			};

			unitOfWork.AddForInsert(newLoginAccount);
			await unitOfWork.CommitAsync();

			return true;
		}

		/// <summary>
		/// User password recovery
		/// </summary>
		/// <param name="inputModel">Input model with user email address</param>
		/// <returns></returns>
		public async Task RecoverPasswordAsync(Contracts.Customer.Security.InputModels.PasswordRecoveryIM inputModel)
		{
			LoginAccount loginAccount = await loginAccountRepository.GetByEmailAsync(inputModel.Email);
			await dataLoader.LoadAsync(loginAccount, i => i.Role);

			if (loginAccount is not null && loginAccount.Role.Name == "Uživatel")
			{
				byte[] token = GeneratePasswordRecoveryToken(loginAccount);

				await mailerService.SendPasswordRecoveryEmailAsync(loginAccount.Email, $"{loginAccount.FirstName} {loginAccount.LastName}", token, "Uživatel").ConfigureAwait(false);
			}
		}

		/// <summary>
		/// User jwt token refresh
		/// </summary>
		/// <param name="userID">User ID</param>
		/// <returns>New jwt token(string) or null</returns>
		public async Task<string> RefreshJwtTokenAsync(int userID)
		{
			LoginAccount loginAccount = await loginAccountRepository.GetObjectAsync(userID);

			if (loginAccount is not null && loginAccount.Role.Name == "Uživatel")
			{
				return CreateToken(loginAccount.ID.ToString(), "Uživatel", loginAccount.FirstName, loginAccount.LastName);
			}

			return null;
		}

		public async Task<AuthenticationResult> AuthenticateAdminAsync(LoginAccountIM inputModel)
		{
			LoginAccount loginAccount = await loginAccountRepository.GetByEmailAsync(inputModel.Email).ConfigureAwait(false);

			if (loginAccount is not null && PasswordHashService.VerifySHA512HashString(inputModel.Password, loginAccount.PasswordSalt, loginAccount.PasswordHash))
			{
				return new AuthenticationResult
				{
					Token = CreateToken(loginAccount.ID.ToString(), Contracts.Security.ViewModels.Role.Admin, loginAccount.FirstName, loginAccount.LastName)
				};
			}

			return new AuthenticationResult();
		}

		public async Task RecoverForgottenPasswordAsync(ForgottenPasswordIM inputModel)
		{
			LoginAccount loginAccount = await loginAccountRepository.GetByEmailAsync(inputModel.Email).ConfigureAwait(false);
			await dataLoader.LoadAsync(loginAccount, i => i.Role).ConfigureAwait(false);

			if (loginAccount is not null)
			{
				byte[] token = GeneratePasswordRecoveryToken(loginAccount);

				if (loginAccount.Role.Name == Contracts.Security.ViewModels.Role.Admin)
				{
					await mailerService.SendPasswordRecoveryEmailAsync(loginAccount.Email, $"{loginAccount.FirstName} {loginAccount.LastName}", token, Contracts.Security.ViewModels.Role.Admin).ConfigureAwait(false);
				}
			}
		}

		public async Task<AuthenticationResult> RefreshTokenAsync(ProtoBuf.Grpc.CallContext context)
		{
			ClaimsPrincipal user = context.ServerCallContext.GetHttpContext().User;
			string role = user.FindFirst(ClaimTypes.Role).Value;
			int userID = int.Parse(user.Identity.Name);

			if (role == Contracts.Security.ViewModels.Role.Admin)
			{
				LoginAccount loginAccount = await loginAccountRepository.GetObjectAsync(userID).ConfigureAwait(false);

				if (loginAccount is not null)
				{
					string ID, firstName, lastName;

					ID = loginAccount.ID.ToString();
					firstName = loginAccount.FirstName;
					lastName = loginAccount.LastName;

					return new AuthenticationResult { Token = CreateToken(ID, role, firstName, lastName) };
				}
			}

			return new AuthenticationResult();
		}

		public async Task RegisterAccountAsync(RegisterAccountIM inputModel)
		{
			LoginAccount loginAccount = await loginAccountRepository.GetByEmailAsync(inputModel.Email).ConfigureAwait(false);

			// Účet se zadaným emailem již existuje
			if (loginAccount is not null)
			{
				return;
			}

			List<Model.Security.Role> roles = await roleRepository.GetAllAsync().ConfigureAwait(false);
			string salt = PasswordHashService.CreateSalt();

			LoginAccount newLoginAccount = new()
			{
				FirstName = inputModel.FirstName,
				LastName = inputModel.LastName,
				Email = inputModel.Email,
				RoleID = roles.Find(i => i.Name == nameof(Model.Security.Role.Entry.Admin))?.ID,
				PasswordHash = PasswordHashService.ComputeSHA512HashString(inputModel.Password, salt),
				PasswordSalt = salt
			};

			unitOfWork.AddForInsert(newLoginAccount);
			await unitOfWork.CommitAsync().ConfigureAwait(false);
		}

		/// <summary>
		/// Common token validation method for Admin and User
		/// </summary>
		/// <param name="inputModel"></param>
		/// <returns></returns>
		public async Task<Contracts.Security.ViewModels.TokenValidationResult> ValidatePasswordResetTokenAsync(PasswordResetTokenIM inputModel)
		{
			byte[] token = encoderService.Decode(inputModel.Token);
			int? userID = await DecriptPasswordResetTokenAsync(token).ConfigureAwait(false);

			return new Contracts.Security.ViewModels.TokenValidationResult { IsValid = userID != null };
		}

		/// <summary>
		/// Common password reset method for Admin and User
		/// </summary>
		/// <param name="inputModel"></param>
		/// <returns></returns>
		public async Task PasswordResetAsync(PasswordResetIM inputModel)
		{
			byte[] token = encoderService.Decode(inputModel.Token);
			int? userID = await DecriptPasswordResetTokenAsync(token).ConfigureAwait(false);

			if (userID != null)
			{
				LoginAccount loginAccount = await loginAccountRepository.GetObjectAsync((int)userID).ConfigureAwait(false);

				if (loginAccount is not null)
				{
					loginAccount.PasswordHash = PasswordHashService.ComputeSHA512HashString(inputModel.Password, loginAccount.PasswordSalt);

					unitOfWork.AddForUpdate(loginAccount);
					await unitOfWork.CommitAsync().ConfigureAwait(false);
				}
			}
		}

		public async Task<PasswordChangeResult> PasswordChangeAsync(PasswordChangeIM inputModel, ProtoBuf.Grpc.CallContext context = default)
		{
			ClaimsPrincipal user = context.ServerCallContext.GetHttpContext().User;
			LoginAccount loginAccount = await loginAccountRepository.GetObjectAsync(int.Parse(user.Identity.Name)).ConfigureAwait(false);

			if ((loginAccount is not null) && PasswordHashService.VerifySHA512HashString(inputModel.OldPassword, loginAccount.PasswordSalt, loginAccount.PasswordHash))
			{
				loginAccount.PasswordHash = PasswordHashService.ComputeSHA512HashString(inputModel.NewPassword, loginAccount.PasswordSalt);

				unitOfWork.AddForUpdate(loginAccount);
				await unitOfWork.CommitAsync().ConfigureAwait(false);

				return new PasswordChangeResult { IsSuccessful = true };
			}

			return new PasswordChangeResult { IsSuccessful = false };
		}

		private string CreateToken(string name, string role, string firstName, string lastName)
		{
			byte[] key = Encoding.ASCII.GetBytes(jwtConfiguration.Key);
			SymmetricSecurityKey securityKey = new(key);

			JwtSecurityToken token = new(
				null,
				null,
				new[]
				{
					new Claim(ClaimTypes.Name, name),
					new Claim(ClaimTypes.Role, role),
					new Claim("firstName", firstName),
					new Claim("lastName", lastName),
				},
				expires: DateTime.Now.AddHours(8), // TODO: Znížit dobu platnosti tokenu
				signingCredentials: new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256));

			JwtSecurityTokenHandler tokenHandler = new();

			return tokenHandler.WriteToken(token);
		}

		private byte[] GeneratePasswordRecoveryToken(LoginAccount loginAccount)
		{
			if (loginAccount is not null)
			{
				return cryptographyService.Encrypt(loginAccount.ID + "|" + DateTime.Now.AddHours(1));
			}

			return null;
		}

		private async Task<int?> DecriptPasswordResetTokenAsync(byte[] token)
		{
			string[] separators = new string[] { "|" };
			string[] decriptedToken = cryptographyService.Decript(token).Split(separators, StringSplitOptions.RemoveEmptyEntries);

			if (decriptedToken.Length != 2)
			{
				return null;
			}

			if (!DateTime.TryParse(decriptedToken[1], out DateTime tokenValidity) || !int.TryParse(decriptedToken[0], out int id))
			{
				return null;
			}

			if (tokenValidity < DateTime.Now)
			{
				return null;
			}

			LoginAccount loginAccount = await loginAccountRepository.GetObjectAsync(id).ConfigureAwait(false);

			return loginAccount.ID!;
		}
	}
}
