using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Baya.Application.Contracts;
using Baya.Application.Contracts.Common;
using Baya.Application.Contracts.Persistence;
using Baya.Application.Models.Jwt;
using Baya.Domain.Entities.User;
using Baya.Infrastructure.Identity.Identity.Dtos;
using Baya.Infrastructure.Identity.Identity.Manager;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;

namespace Baya.Infrastructure.Identity.Jwt;

public class JwtService : IJwtService
{
    private readonly IdentitySettings _siteSetting;
    private readonly AppUserManager _userManager;
    private IUserClaimsPrincipalFactory<User> _claimsPrincipal;
    private readonly IFieldEncryptor _fieldEncryptor;

    private readonly IUnitOfWork _unitOfWork;

    public JwtService(IOptions<IdentitySettings> siteSetting, AppUserManager userManager, IUserClaimsPrincipalFactory<User> claimsPrincipal, IUnitOfWork unitOfWork, IFieldEncryptor fieldEncryptor)
    {
        _siteSetting = siteSetting.Value;
        _userManager = userManager;
        _claimsPrincipal = claimsPrincipal;
        _unitOfWork = unitOfWork;
        _fieldEncryptor = fieldEncryptor;
    }
    public async Task<AccessToken> GenerateAsync(User user)
    {
        var securityToken = await _createSecurityTokenAsync(user);

        var refreshToken = await _unitOfWork.UserRefreshTokenRepository.CreateToken(user.Id);
        await _unitOfWork.CommitAsync();

        return new AccessToken(securityToken,refreshToken.ToString());
    }

    /// <summary>
    /// Access token only — no <c>UserRefreshTokens</c> row. The REST auth flow (backend-phase-2)
    /// pairs this with its own revocable <c>user_sessions</c> refresh token.
    /// </summary>
    public async Task<JweAccessToken> GenerateAccessTokenAsync(User user)
    {
        var securityToken = await _createSecurityTokenAsync(user);

        return new JweAccessToken(
            new JwtSecurityTokenHandler().WriteToken(securityToken),
            new DateTimeOffset(securityToken.ValidTo, TimeSpan.Zero));
    }

    public Task<ClaimsPrincipal> GetPrincipalFromExpiredToken(string token)
    {
        var tokenValidationParameters = new TokenValidationParameters
        {
            ValidateAudience = false, //you might want to validate the audience and issuer depending on your use case
            ValidateIssuer = false,
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_siteSetting.SecretKey)),
            ValidateLifetime = false,
            TokenDecryptionKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_siteSetting.Encryptkey))
        };

        var tokenHandler = new JwtSecurityTokenHandler();
        SecurityToken securityToken;
        var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out securityToken);
        var jwtSecurityToken = securityToken as JwtSecurityToken;
        if (jwtSecurityToken == null || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
            throw new SecurityTokenException("Invalid token");

        return Task.FromResult(principal);
    }

    public async Task<AccessToken> GenerateByPhoneNumberAsync(string phoneNumber)
    {
        // The phone column is encrypted (non-deterministic); equality goes through the hash.
        var phoneHash = _fieldEncryptor.Hash(phoneNumber);
        var user = await _userManager.Users.AsNoTracking().FirstOrDefaultAsync(u => u.PhoneHash == phoneHash);
        var result = await this.GenerateAsync(user);
        return result;
    }

    public async Task<AccessToken> RefreshToken(Guid refreshTokenId)
    {
        var refreshToken = await _unitOfWork.UserRefreshTokenRepository.GetTokenWithInvalidation(refreshTokenId);

        if (refreshToken is null)
            return null;

        refreshToken.IsValid = false;

        await _unitOfWork.CommitAsync();

        var user = await _unitOfWork.UserRefreshTokenRepository.GetUserByRefreshToken(refreshTokenId);

        if (user is null)
            return null;

        var result = await this.GenerateAsync(user);

        return result;
    }

    private async Task<JwtSecurityToken> _createSecurityTokenAsync(User user)
    {
        var secretKey = Encoding.UTF8.GetBytes(_siteSetting.SecretKey); // longer that 16 character
        var signingCredentials = new SigningCredentials(new SymmetricSecurityKey(secretKey), SecurityAlgorithms.HmacSha256Signature);

        var encryptionkey = Encoding.UTF8.GetBytes(_siteSetting.Encryptkey); //must be 16 character
        var encryptingCredentials = new EncryptingCredentials(new SymmetricSecurityKey(encryptionkey), SecurityAlgorithms.Aes128KW, SecurityAlgorithms.Aes128CbcHmacSha256);

        var claims = await _getClaimsAsync(user);

        var descriptor = new SecurityTokenDescriptor
        {
            Issuer = _siteSetting.Issuer,
            Audience = _siteSetting.Audience,
            IssuedAt = DateTime.Now,
            NotBefore = DateTime.Now.AddMinutes(0),
            Expires = DateTime.Now.AddMinutes(_siteSetting.ExpirationMinutes),
            SigningCredentials = signingCredentials,
            EncryptingCredentials = encryptingCredentials,
            Subject = new ClaimsIdentity(claims)
        };

        var tokenHandler = new JwtSecurityTokenHandler();

        return tokenHandler.CreateJwtSecurityToken(descriptor);
    }

    private async Task<IEnumerable<Claim>> _getClaimsAsync(User user)
    {
        var result = await _claimsPrincipal.CreateAsync(user);
        return result.Claims;
    }
}