
using System;
using System.Collections.Generic;
using BlazorPolicyAuth.Data;
using BlazorPolicyAuth.Models.Entities;
using BlazorPolicyAuth.Models.ViewModels;
using BlazorPolicyAuth.Services.AuthService;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;

namespace BlazorPolicyAuth.Services.AuthService;

public class AuthService(AppDbContext context, IConfiguration configuration, IHttpContextAccessor httpContextAccessor)
	: IAuthService
{
	public async Task<ServiceResponse<int>> Register (UserRegister request)
	{
		if (await UserExists(request.Email))
		{
			return new ServiceResponse<int> { Success = false, Message = "User already exist." };
		}

		CreatePasswordHash(request.Password, out byte[] passwordHash, out byte[] passwordSalt);
		var user = new UserAccount
		{
			Email = request.Email,
			PasswordHash = passwordHash,
			PasswordSalt = passwordSalt
		};

		context.UserAccounts.Add(user);
		await context.SaveChangesAsync();

		return new ServiceResponse<int> { Data = user.Id, Message = "Registration successful" };
	}

	public async Task<bool> UserExists(string email)
	{
		if (await context.UserAccounts.AnyAsync(user => user.Email.ToLower().Equals(email.ToLower())))
		{
			return true;
		}
		return false;
	}

	public async Task<ServiceResponse<string>> Login(string email, string password)
	{
		var response = new ServiceResponse<string>();
		var user = await context.UserAccounts.FirstOrDefaultAsync(u => u.Email.ToLower().Equals(email.ToLower()));
		if (user == null)
		{
			response.Success = false;
			response.Message = "User not found.";
		}
		else if (!VerifyPasswordHash(password, user.PasswordHash, user.PasswordSalt))
		{
			response.Success = false;
			response.Message = "Wrong password.";
		}
		else
		{
			response.Data = CreateToken(user);
		}

		return response;
	}

	public async Task<ServiceResponse<bool>> ChangePassword(int userId, string newPassword)
	{
		var user = await context.UserAccounts.FindAsync(userId);
		if (user == null)
		{
			return new ServiceResponse<bool> { Success = false, Message = "User not found." };
		}

		CreatePasswordHash(newPassword, out byte[] passwordHash, out byte[] passwordSalt);

		user.PasswordHash = passwordHash;
		user.PasswordSalt = passwordSalt;

		await context.SaveChangesAsync();

		return new ServiceResponse<bool> { Data = true, Message = "Password has been changed." };
	}

	public int GetUserId() => int.Parse(httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier));

	public string GetUserEmail() => httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.Name);

	public async Task<UserAccount> GetUserByEmail(string email)
	{
		return await context.UserAccounts.FirstOrDefaultAsync(u => u.Email.Equals(email));
	}

	private static void CreatePasswordHash(string password, out byte[] passwordHash, out byte[] passwordSalt)
	{
		using var hmac = new HMACSHA512();
		passwordSalt = hmac.Key;
		passwordHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(password));
	}

	private static bool VerifyPasswordHash(string password, byte[] passwordHash, byte[] passwordSalt)
	{
		using var hmac = new HMACSHA512(passwordSalt);
		var computedHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(password));
		return computedHash.SequenceEqual(passwordHash);
	}

	private string CreateToken(UserAccount user)
	{
		var claims = new List<Claim>
		{
			new(ClaimTypes.NameIdentifier, user.Id.ToString()),
			new(ClaimTypes.Name, user.Email),
			new(ClaimTypes.Role, user.Role)
		};

		var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration.GetSection("AppSettings:Token").Value));

		var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature);

		var token = new JwtSecurityToken(
			claims: claims,
			expires: DateTime.Now.AddDays(1),
			signingCredentials: creds);

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

		return jwt;
	}
}