package com.docusign.esign.client.auth;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.StringReader;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Security;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.EncodedKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Date;

import com.auth0.jwt.JWTCreator;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTCreationException;

public class JWTUtils {


	/**
	 * Helper method to create a JWT token for the JWT flow
	 * @param rsaPrivateKey the byte contents of the RSA private key
	 * @param oAuthBasePath DocuSign OAuth base path (account-d.docusign.com for the developer sandbox
	and account.docusign.com for the production platform)
	 * @param clientId DocuSign OAuth Client Id (AKA Integrator Key)
	 * @param userId DocuSign user Id to be impersonated (This is a UUID)
	 * @param expiresIn number of seconds remaining before the JWT assertion is considered as invalid
	 * @param scopes space-separated string that represents the list of scopes to grant to the OAuth token.
	 * @return a fresh JWT token
	 * @throws IllegalArgumentException if one of the arguments is invalid
	 * @throws JWTCreationException if not able to create a JWT token from the input parameters
	 * @throws IOException if there is an issue with either the public or private file
	 */
	public static String generateJWTAssertionFromByteArray(byte[] rsaPrivateKey, String oAuthBasePath, String clientId, String userId, long expiresIn, String scopes) throws IllegalArgumentException, JWTCreationException, IOException {
		if (expiresIn <= 0L) {
			throw new IllegalArgumentException("expiresIn should be a non-negative value");
		}
		if (rsaPrivateKey == null || rsaPrivateKey.length == 0) {
			throw new IllegalArgumentException("rsaPrivateKey byte array is empty");
		}
		if (oAuthBasePath == null || "".equals(oAuthBasePath) || clientId == null || "".equals(clientId)) {
			throw new IllegalArgumentException("One of the arguments is null or empty");
		}
		
		RSAPrivateKey privateKey = readPrivateKeyFromByteArray(rsaPrivateKey, "RSA");
		Algorithm algorithm = Algorithm.RSA256(null, privateKey);
		long now = System.currentTimeMillis();
		JWTCreator.Builder builder = JWT.create()
				.withIssuer(clientId)
				.withAudience(oAuthBasePath)
				.withIssuedAt(new Date(now))
				.withClaim("scope", scopes)
				.withExpiresAt(new Date(now + expiresIn * 1000));
		if (userId != null && userId != "") {
			builder = builder.withSubject(userId);
		}
		return builder.sign(algorithm);
	}
	  /**
	   * Helper method to create a JWT token for the JWT flow
	   * @param publicKeyFilename the filename of the RSA public key
	   * @param privateKeyFilename the filename of the RSA private key
	   * @param oAuthBasePath DocuSign OAuth base path (account-d.docusign.com for the developer sandbox
	 			and account.docusign.com for the production platform)
	   * @param clientId DocuSign OAuth Client Id (AKA Integrator Key)
	   * @param userId DocuSign user Id to be impersonated (This is a UUID)
	   * @param expiresIn number of seconds remaining before the JWT assertion is considered as invalid
	   * @return a fresh JWT token
	   * @throws JWTCreationException if not able to create a JWT token from the input parameters
	   * @throws IOException if there is an issue with either the public or private file
	   */
	  public static String generateJWTAssertion(String publicKeyFilename, String privateKeyFilename, String oAuthBasePath, String clientId, String userId, long expiresIn) throws JWTCreationException, IOException {
		  String token = null;
		  if (expiresIn <= 0L) {
				throw new IllegalArgumentException("expiresIn should be a non-negative value");
		  }
		  if (publicKeyFilename == null || "".equals(publicKeyFilename) || privateKeyFilename == null || "".equals(privateKeyFilename) || oAuthBasePath == null || "".equals(oAuthBasePath) || clientId == null || "".equals(clientId) || userId == null || "".equals(userId)) {
				throw new IllegalArgumentException("One of the arguments is null or empty");
		  }

		  try {
			  RSAPublicKey publicKey = readPublicKeyFromFile(publicKeyFilename, "RSA");
			  RSAPrivateKey privateKey = readPrivateKeyFromFile(privateKeyFilename, "RSA");
			  Algorithm algorithm = Algorithm.RSA256(publicKey, privateKey);
			  long now = System.currentTimeMillis();
			  token = JWT.create()
					  .withIssuer(clientId)
					  .withSubject(userId)
					  .withAudience(oAuthBasePath)
					  .withNotBefore(new Date(now))
					  .withExpiresAt(new Date(now + expiresIn * 1000))
					  .withClaim("scope", "signature")
					  .sign(algorithm);
		  } catch (JWTCreationException e){
			  throw e;
		  } catch (IOException e) {
			  throw e;
		  }

		  return token;
	  }
	  
	  private static RSAPublicKey readPublicKeyFromFile(String filepath, String algorithm) throws IOException {
		  File pemFile = new File(filepath);
		  if (!pemFile.isFile() || !pemFile.exists()) {
	          throw new FileNotFoundException(String.format("The file '%s' doesn't exist.", pemFile.getAbsolutePath()));
	      }
	      PemReader reader = new PemReader(new FileReader(pemFile));
	      try {
		      PemObject pemObject = reader.readPemObject();
			  byte[] bytes = pemObject.getContent();
			  RSAPublicKey publicKey = null;
		      try {
		          KeyFactory kf = KeyFactory.getInstance(algorithm);
		          EncodedKeySpec keySpec = new X509EncodedKeySpec(bytes);
		          publicKey = (RSAPublicKey) kf.generatePublic(keySpec);
		      } catch (NoSuchAlgorithmException e) {
		          System.out.println("Could not reconstruct the public key, the given algorithm could not be found.");
		      } catch (InvalidKeySpecException e) {
		          System.out.println("Could not reconstruct the public key");
		      }
		
		      return publicKey;
	      } finally {
	    	  reader.close();
	      }
	  }

	  private static RSAPrivateKey readPrivateKeyFromFile(String filepath, String algorithm) throws IOException {
		  File pemFile = new File(filepath);
		  if (!pemFile.isFile() || !pemFile.exists()) {
	          throw new FileNotFoundException(String.format("The file '%s' doesn't exist.", pemFile.getAbsolutePath()));
	      }
	      PemReader reader = new PemReader(new FileReader(pemFile));
	      try {
		      PemObject pemObject = reader.readPemObject();
			  byte[] bytes = pemObject.getContent();
			  RSAPrivateKey privateKey = null;
		      try {
		    	  Security.addProvider(new BouncyCastleProvider());
		          KeyFactory kf = KeyFactory.getInstance(algorithm, "BC");
		          EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes);
		          privateKey = (RSAPrivateKey) kf.generatePrivate(keySpec);
		      } catch (NoSuchAlgorithmException e) {
		          System.out.println("Could not reconstruct the private key, the given algorithm could not be found.");
		      } catch (InvalidKeySpecException e) {
		          System.out.println("Could not reconstruct the private key");
		      } catch (NoSuchProviderException e) {
		          System.out.println("Could not reconstruct the private key, invalid provider.");
			  }
		
		      return privateKey;
	      } finally {
	    	  reader.close();
	      }
	  }

	  private static RSAPrivateKey readPrivateKeyFromByteArray(byte[] privateKeyBytes, String algorithm) throws IOException {
		PemReader reader = new PemReader(new StringReader(new String(privateKeyBytes)));
	    try {
	    	PemObject pemObject = reader.readPemObject();
	    	byte[] bytes = pemObject.getContent();
			RSAPrivateKey privateKey = null;
			try {
				Security.addProvider(new BouncyCastleProvider());
				KeyFactory kf = KeyFactory.getInstance(algorithm, "BC");
				EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes);
				privateKey = (RSAPrivateKey) kf.generatePrivate(keySpec);
			} catch (NoSuchAlgorithmException e) {
				System.out.println("Could not reconstruct the private key, the given algorithm could not be found.");
			} catch (InvalidKeySpecException e) {
				System.out.println("Could not reconstruct the private key");
			} catch (NoSuchProviderException e) {
				System.out.println("Could not reconstruct the private key, invalid provider.");
			}
	
			return privateKey;
	    } finally {
	    	reader.close();
	    }
	}
}