package com.ffii.core.utils;

import java.io.Serializable;
import java.security.Key;
import java.time.Instant;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import com.ffii.lioner.model.RefreshToken;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;

@Component
@Scope(value = ConfigurableBeanFactory. SCOPE_SINGLETON)
public class JwtTokenUtil implements Serializable {

  Logger logger = LoggerFactory.getLogger(JwtTokenUtil.class);

  private static final long serialVersionUID = -2550185165626007488L;

  // * 60000 = 1 Min
  public static final long JWT_REFRESH_TOKEN_EXPIRED_TIME = 60000 * 30;
  public static final String AES_SECRET = "ffii";
  public static final String TOKEN_SEPARATOR = "@@";

  // @Value("${jwt.secret}")
  // private String secret;

  private static final Key secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS512);

  // retrieve username from jwt token
  public String getUsernameFromToken(String token) {
    return getClaimFromToken(token, Claims::getSubject);
  }

  // retrieve expiration date from jwt token
  public Date getExpirationDateFromToken(String token) {
    return getClaimFromToken(token, Claims::getExpiration);
  }

  public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
    final Claims claims = getAllClaimsFromToken(token);
    return claimsResolver.apply(claims);
  }

  // for retrieveing any information from token we will need the secret key
  private Claims getAllClaimsFromToken(String token) {
    return Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token).getBody();
  }

  // check if the token has expired
  private Boolean isTokenExpired(String token) {
    final Date expiration = getExpirationDateFromToken(token);
    return expiration.before(new Date());
  }

  // generate token for user
  public String generateToken(UserDetails userDetails, long accessTokenExpiry) {
    Map<String, Object> claims = new HashMap<>();
    return doGenerateToken(claims, userDetails.getUsername(), accessTokenExpiry);
  }

  // while creating the token -
  // 1. Define claims of the token, like Issuer, Expiration, Subject, and the ID
  // 2. Sign the JWT using the HS512 algorithm and secret key.
  // 3. According to JWS Compact
  // Serialization(https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#section-3.1)
  // compaction of the JWT to a URL-safe string
  private String doGenerateToken(Map<String, Object> claims, String subject, long accessTokenExpiry) {
    //logger.info((new Date(System.currentTimeMillis() + accessTokenExpiry)).toString());
    return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
        .setExpiration(new Date(System.currentTimeMillis() + accessTokenExpiry))
        .signWith(secretKey).compact();
  }

  // validate token
  public Boolean validateToken(String token, UserDetails userDetails) {
    final String username = getUsernameFromToken(token);
    return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
  }

  public RefreshToken createRefreshToken(String username) {
    RefreshToken refreshToken = new RefreshToken();
    refreshToken.setUserName(username);
    refreshToken.setExpiryDate(Instant.now().plusMillis(JWT_REFRESH_TOKEN_EXPIRED_TIME));
    long instantNum = Instant.now().plusMillis(JWT_REFRESH_TOKEN_EXPIRED_TIME).toEpochMilli();
    refreshToken.setToken(AES.encrypt(username + TOKEN_SEPARATOR + instantNum, AES_SECRET));
    return refreshToken;
  }

  public boolean verifyExpiration(RefreshToken token) throws Exception {
    if (token.getExpiryDate().compareTo(Instant.now()) < 0) {
      return false;
    }

    return true;
  }

  public String getUsernameFromRefreshToken(String refreshToken) {
    return AES.decrypt(refreshToken, AES_SECRET);
  }
}
