package in.gov.digilocker.uidai.services.resources;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PublicKey;
import java.security.Security;
import java.security.UnrecoverableEntryException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Enumeration;
import java.util.TimeZone;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;

import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.engines.AESEngine;
import org.bouncycastle.crypto.modes.GCMBlockCipher;
import org.bouncycastle.crypto.paddings.PKCS7Padding;
import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher;
import org.bouncycastle.crypto.params.AEADParameters;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class RDEncrypter {
	private static final Logger logger = LoggerFactory.getLogger(RDEncrypter.class);
	private PublicKey publicKey;
	private Date certExpiryDate;
	X509Certificate cert;
	public static final int IV_SIZE_BITS = 96;
	public static final int AUTH_TAG_SIZE_BITS = 128;
	public static final int AAD_SIZE_BITS = 128;
	private String algorithm = "SHA-256";

	@SuppressWarnings("unused")
	private int HMAC_SIZE = 32;

	static {
		Security.addProvider(new BouncyCastleProvider());
	}

	public RDEncrypter(String publicKeyFileName) throws RuntimeException {
		FileInputStream fis = null;
		try {
			Security.addProvider(new BouncyCastleProvider());			
			CertificateFactory certFactory = CertificateFactory.getInstance("X.509", "BC");
			fis = new FileInputStream(new File(RDEncrypter.class.getResource(publicKeyFileName).getFile()));
			this.cert = ((X509Certificate) certFactory.generateCertificate(fis));
			fis.close();
			this.publicKey = this.cert.getPublicKey();
			this.certExpiryDate = this.cert.getNotAfter();
		} catch (Exception e) {
			logger.info("RDEncrypter:--- "+ e.getLocalizedMessage());
			e.printStackTrace();
			throw new RuntimeException("Could not intialize encryption module", e);
		} finally {
			if (fis != null) {
				try {
					fis.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}

	public KeyStore.PrivateKeyEntry getKeyFromKeyStore(String digilockerP12FilePath, String digilockerP12FilePassword, String digilockerP12FileAlias) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException, UnrecoverableEntryException {
		findAliases(digilockerP12FilePath, digilockerP12FilePassword);
		FileInputStream keyFileStream = null;
		KeyStore ks = KeyStore.getInstance("PKCS12");
		keyFileStream = new FileInputStream(new File(RDEncrypter.class.getResource(digilockerP12FilePath).getFile()));
		ks.load(keyFileStream, digilockerP12FilePassword.toCharArray());
		KeyStore.PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry) ks.getEntry(digilockerP12FileAlias, new KeyStore.PasswordProtection(digilockerP12FilePassword.toCharArray()));
		keyFileStream.close();
		return entry;
	}  

	public static void findAliases(String digilockerP12FilePath, String digilockerP12FilePassword) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException {
		FileInputStream keyFileStream = null;
		KeyStore ks = KeyStore.getInstance("PKCS12");
		File file = new File(RDEncrypter.class.getResource(digilockerP12FilePath).getFile());
		keyFileStream = new FileInputStream(file);
		ks.load(keyFileStream, digilockerP12FilePassword.toCharArray());
		Enumeration<String> aliases = ks.aliases();
		while (aliases.hasMoreElements()) {
			String alias = aliases.nextElement();
			@SuppressWarnings("unused")	Certificate cert  = ks.getCertificate(alias);			
		}
	}

	public X509Certificate getCertificateChain() {
		return this.cert;
	}

	public byte[] generateSessionKey() throws NoSuchAlgorithmException, NoSuchProviderException {
		KeyGenerator kgen = KeyGenerator.getInstance("AES", "BC");
		kgen.init(256);
		SecretKey key = kgen.generateKey();
		byte[] symmKey = key.getEncoded();
		return symmKey;
	}

	public byte[] decrypt(byte[] inputData, byte[] sessionKey, byte[] encSrcHash)
			throws IllegalStateException, InvalidCipherTextException, Exception {
		byte[] bytesTs = Arrays.copyOfRange(inputData, 0, 19);
		String ts = new String(bytesTs);
		byte[] cipherData = Arrays.copyOfRange(inputData, bytesTs.length, inputData.length);
		byte[] iv = generateIv(ts);
		byte[] aad = generateAad(ts);
		byte[] plainText = encryptDecryptUsingSessionKey(false, sessionKey, iv, aad, cipherData);
		byte[] srcHash = encryptDecryptUsingSessionKey(false, sessionKey, iv, aad, encSrcHash);
		System.out.println("Decrypted HAsh in cipher text: " + byteArrayToHexString(srcHash));
		boolean result = validateHash(srcHash, plainText);
		if (!result) {
			throw new Exception(
					"Integrity Validation Failed : The original data at client side and the decrypted data at server side is not identical");
		}
		System.out.println("Hash Validation is Successful!!!!!");
		return plainText;
	}

	private boolean validateHash(byte[] srcHash, byte[] plainTextWithTS) throws Exception {
		byte[] actualHash = generateHash(plainTextWithTS);
		System.out.println("Hash of actual plain text in cipher hex:--->" + byteArrayToHexString(actualHash));
		if (new String(srcHash, "UTF-8").equals(new String(actualHash, "UTF-8"))) {
			return true;
		}
		return false;
	}

	private static String byteArrayToHexString(byte[] bytes) {
		StringBuffer result = new StringBuffer();
		for (int i = 0; i < bytes.length; i++) {
			result.append(Integer.toString((bytes[i] & 0xFF) + 256, 16).substring(1));
		}
		return result.toString();
	}

	public byte[] generateIv(String ts) throws UnsupportedEncodingException {
		return getLastBits(ts, 12);
	}

	private byte[] getLastBits(String ts, int bits) throws UnsupportedEncodingException {
		byte[] tsInBytes = ts.getBytes("UTF-8");
		return Arrays.copyOfRange(tsInBytes, tsInBytes.length - bits, tsInBytes.length);
	}

	public byte[] encrypt(byte[] inputData, byte[] sessionKey, String ts)
			throws IllegalStateException, InvalidCipherTextException, Exception {
		byte[] iv = generateIv(ts);
		byte[] aad = generateAad(ts);
		byte[] cipherText = encryptDecryptUsingSessionKey(true, sessionKey, iv, aad, inputData);
		byte[] tsInBytes = ts.getBytes("UTF-8");
		byte[] packedCipherData = new byte[cipherText.length + tsInBytes.length];
		System.arraycopy(tsInBytes, 0, packedCipherData, 0, tsInBytes.length);
		System.arraycopy(cipherText, 0, packedCipherData, tsInBytes.length, cipherText.length);
		return packedCipherData;
	}

	public byte[] encryptDecryptUsingSessionKey(boolean cipherOperation, byte[] skey, byte[] iv, byte[] aad,
			byte[] data) throws IllegalStateException, InvalidCipherTextException {
		AEADParameters aeadParam = new AEADParameters(new KeyParameter(skey), 128, iv, aad);
		GCMBlockCipher gcmb = new GCMBlockCipher(new AESEngine());
		gcmb.init(cipherOperation, aeadParam);
		int outputSize = gcmb.getOutputSize(data.length);
		byte[] result = new byte[outputSize];
		int processLen = gcmb.processBytes(data, 0, data.length, result, 0);
		gcmb.doFinal(result, processLen);
		return result;
	}

	public byte[] generateHash(byte[] message) throws Exception {
		byte[] hash = null;
		try {
			MessageDigest digest = MessageDigest.getInstance(this.algorithm, "BC");
			digest.reset();
			this.HMAC_SIZE = digest.getDigestLength();
			hash = digest.digest(message);
		} catch (GeneralSecurityException e) {
			throw new Exception("SHA-256 Hashing algorithm not available");
		}
		return hash;
	}

	public byte[] generateAad(String ts) throws UnsupportedEncodingException {
		return getLastBits(ts, 16);
	}

	public byte[] encryptUsingPublicKey(byte[] data) throws IOException, GeneralSecurityException {
		Cipher pkCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "BC");
		pkCipher.init(1, this.publicKey);
		byte[] encSessionKey = pkCipher.doFinal(data);
		return encSessionKey;
	}

	public byte[] encryptUsingSessionKey(byte[] skey, byte[] data) throws InvalidCipherTextException {
		PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new AESEngine(), new PKCS7Padding());
		cipher.init(true, new KeyParameter(skey));
		int outputSize = cipher.getOutputSize(data.length);
		byte[] tempOP = new byte[outputSize];
		int processLen = cipher.processBytes(data, 0, data.length, tempOP, 0);
		int outputLen = cipher.doFinal(tempOP, processLen);
		byte[] result = new byte[processLen + outputLen];
		System.arraycopy(tempOP, 0, result, 0, result.length);
		return result;
	}

	public String getCertificateIdentifier() {
		String certificateIdentifier = null;
		try {
			SimpleDateFormat ciDateFormat = new SimpleDateFormat("yyyyMMdd");
			ciDateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
			certificateIdentifier = ciDateFormat.format(this.certExpiryDate);
		} catch (Exception localException) {
		}
		return certificateIdentifier;
	}

	public Date getCertExpiryDate() {
		return this.certExpiryDate;
	}

	public String returnSha256() {
		String hashValue = "";
		try {

		} catch (Exception e) {
			e.printStackTrace();
		}
		return hashValue;
	}
}
