package com.ffii.core.utils;

import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
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.annotation.Value;
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.fpsms.model.RefreshToken;

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

import jakarta.annotation.PostConstruct;

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

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

  private static final long serialVersionUID = -2550185165626007488L;

  public static final String AES_SECRET = "ffii";
  public static final String TOKEN_SEPARATOR = "@@";

  @Value("${jwt.expiration-minutes:14400}")
  private long expirationMinutes = 14400;

  @Value("${jwt.refresh-expiration-days:30}")
  private int refreshExpirationDays = 30;

  /** Allow up to this many seconds of clock skew so tokens that just expired still parse (reduces boundary failures). */
  @Value("${jwt.clock-skew-seconds:30}")
  private long clockSkewSeconds = 30;

  /**
   * Stable signing key (HS512). Must be derived from configuration so tokens survive restarts.
   * Previously {@code Keys.secretKeyFor(HS512)} generated a new key every JVM start and broke all existing JWTs.
   */
  @Value("${jwt.secret}")
  private String jwtSecret;

  private Key secretKey;

  @PostConstruct
  void initSigningKey() {
    try {
      byte[] keyBytes = MessageDigest.getInstance("SHA-512")
          .digest(jwtSecret.getBytes(StandardCharsets.UTF_8));
      this.secretKey = Keys.hmacShaKeyFor(keyBytes);
    } catch (NoSuchAlgorithmException e) {
      throw new IllegalStateException("Cannot initialize JWT signing key", e);
    }
  }

  // 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)
        .setAllowedClockSkewSeconds(clockSkewSeconds)
        .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) {
    Map<String, Object> claims = new HashMap<>();
    return doGenerateToken(claims, userDetails.getUsername());
  }

  // 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 expirationMs = expirationMinutes * 60 * 1000;
    logger.info((new Date(System.currentTimeMillis() + expirationMs)).toString());
    return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
        .setExpiration(new Date(System.currentTimeMillis() + expirationMs))
        .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) {
    long refreshExpirationMs = (long) refreshExpirationDays * 24 * 60 * 60 * 1000;
    RefreshToken refreshToken = new RefreshToken();
    refreshToken.setUserName(username);
    refreshToken.setExpiryDate(Instant.now().plusMillis(refreshExpirationMs));
    long instantNum = Instant.now().plusMillis(refreshExpirationMs).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);
  }
}
