package io.trustbuilder.mfa.android.demoapp.security;

import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.util.Log;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;

import io.trustbuilder.mfa.lib.generator.crypto.CipherTools;

/**
 * CryptographyManager is a Service implemented with Singleton to handle Encryption / Decryption
 * The encryption used is AES/GCM/NoPadding that represents AES-GCM algorithm (Advanced Encryption Standard - Galois/Counter Mode)
 */
public class CryptographyManager {
    private static final String TAG = CryptographyManager.class.getSimpleName();

    private static final int KEY_SIZE = 256;
    private static final String ANDROID_KEYSTORE = "AndroidKeyStore";
    private static final String ENCRYPTION_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES;
    private static final String ENCRYPTION_BLOCK_MODE = KeyProperties.BLOCK_MODE_GCM;
    private static final String ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_NONE;

    private static CryptographyManager INSTANCE;

    public static CryptographyManager getInstance() {
        // first check to see if instance is already created
        if (INSTANCE == null) {
            // synchronized call to prevent from multi thread
            synchronized (CryptographyManager.class) {
                // second check to see if instance was created with an other concurrent thread
                if (INSTANCE == null) {
                    INSTANCE = new CryptographyManager();
                }
            }
        }
        return INSTANCE;
    }

    private CryptographyManager() {
        // empty constructor to hide the implicit one
    }

    /**
     * Returns a Cipher initialized for Encryption only with a key stored in the AndroidKeyStore
     * @param keyName : the key to be searched in the Android KeyStore
     * @param usingUserAuthentication : if set to true, cipher is used by the Android Biometric API
     * @return a Cipher initialized for Encryption
     */
    public Cipher getInitializedCipherForEncryption(String keyName, boolean usingUserAuthentication) {
        Cipher cipher;
        try {
            CipherTools cipherTools = new CipherTools(ENCRYPTION_ALGORITHM + "/" + ENCRYPTION_BLOCK_MODE + "/" + ENCRYPTION_PADDING);
            // Retrieving the SecretKey stored in the Android KeyStore
            SecretKey secretKey = getOrCreateSecretKey(keyName, usingUserAuthentication);
            // Initializing Cipher in Encrypt Mode
            cipherTools.initCipher(Cipher.ENCRYPT_MODE, secretKey, null);
            cipher = cipherTools.getCipher();
        } catch (
                NoSuchAlgorithmException
                | KeyStoreException
                | CertificateException
                | IOException
                | UnrecoverableKeyException
                | NoSuchProviderException
                | InvalidAlgorithmParameterException
                | InvalidKeyException e
        ) {
            Log.e(TAG, "Error during encryption cypher initialisation");
            throw new RuntimeException(e);
        }
        return cipher;
    }

    /**
     * Returns a Cipher initialized for Decryption only
     * @param keyName : the key to be searched in the Android KeyStore
     * @param initializationVector : the initialisation vector to init the Cipher with
     * @param usingUserAuthentication : if set to true, cipher is used by the Android Biometric API
     * @return the Cipher initialized for Decryption
     */
    public Cipher getInitializedCipherForDecryption(String keyName, byte[] initializationVector, boolean usingUserAuthentication) {
        Cipher cipher;
        try {
            CipherTools cipherTools = new CipherTools(ENCRYPTION_ALGORITHM + "/" + ENCRYPTION_BLOCK_MODE + "/" + ENCRYPTION_PADDING);
            // Retrieving the SecretKey stored in the Android KeyStore
            SecretKey secretKey = getOrCreateSecretKey(keyName, usingUserAuthentication);
            // Initializing Cipher in Decrypt Mode, with GCM Parameter
            cipherTools.initCipher(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, initializationVector));
            cipher = cipherTools.getCipher();
        } catch (
            NoSuchAlgorithmException
            | InvalidAlgorithmParameterException
            | UnrecoverableKeyException
            | CertificateException
            | KeyStoreException
            | IOException
            | NoSuchProviderException
            | InvalidKeyException e
        ) {
            Log.e(TAG, "Error during decryption cypher initialisation");
            throw new RuntimeException(e);
        }
        return cipher;
    }

    /**
     * This method can only be used with a Cipher created from {@link #getInitializedCipherForEncryption(String, boolean)}
     * @param plaintext the data to be encrypted
     * @param cipher the Encryption Cipher
     * @return the Encrypted data and its Initialisation Vector.
     * To be able the decrypt this data, you must use the Initialisation Vector used for encryption
     * @throws CryptographyException : a exception raised during encryption
     */
    public EncryptedData encryptData(String plaintext, Cipher cipher) throws CryptographyException {
        byte[] ciphertext;
        try {
            // encrypting string with a Encrypted initialized Cipher
            ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
        } catch (BadPaddingException | IllegalBlockSizeException e) {
            Log.e(TAG, "Error during data encryption");
            throw new CryptographyException(e);
        }
        // returning the encrypted text and the initialisation vector used for encryption
        return new EncryptedData(ciphertext, cipher.getIV());
    }

    /**
     * This method can only be used with a Cipher created from {@link #getInitializedCipherForDecryption(String, byte[], boolean)}
     * @param ciphertext the encrypted data to be decrypted
     * @param cipher the decryption Cipher
     * @return the Decrypted data
     * The decryption will only work if using the same Initialisation Vector in the Cipher
     * @throws CryptographyException : a exception raised during decryption
     */
    public String decryptData(byte[] ciphertext, Cipher cipher) throws CryptographyException {
        byte[] plaintext;
        try {
            // decrypting encrypted data with a Decrypting initialized Cipher
            plaintext = cipher.doFinal(ciphertext);
        } catch (BadPaddingException | IllegalBlockSizeException e) {
            Log.e(TAG, "Error during data decryption");
            throw new CryptographyException(e);
        }
        return new String(plaintext, StandardCharsets.UTF_8);
    }

    /**
     * Will search for a secret key stored in the AndroidKeyStore
     * If the key does not exist, it means that it has not been created yet, and should be created and stored
     * @param keyName : the key to be searched or created in the Android KeyStore
     * @param usingUserAuthentication : if set to true, SecretKey will only be used by the Android Biometric API
     * @return the SecretKey that will initialize a Cipher
     */
    private SecretKey getOrCreateSecretKey(String keyName, boolean usingUserAuthentication)
            throws KeyStoreException,
            CertificateException,
            IOException,
            NoSuchAlgorithmException,
            UnrecoverableKeyException,
            NoSuchProviderException,
            InvalidAlgorithmParameterException
    {
        // If Secret Key was previously created for that keyName, then grab and return it.
        KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
        keyStore.load(null);
        Key key = keyStore.getKey(keyName, null);
        if (key != null) {
            return (SecretKey) key;
        }

        // if you reach here, then a new SecretKey must be generated for that keyName
        KeyGenParameterSpec.Builder paramsBuilder = new KeyGenParameterSpec.Builder(
                keyName,
                KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT
        );
        paramsBuilder.setBlockModes(ENCRYPTION_BLOCK_MODE)
                .setEncryptionPaddings(ENCRYPTION_PADDING)
                .setKeySize(KEY_SIZE)
                .setUserAuthenticationRequired(usingUserAuthentication);

        KeyGenParameterSpec keyGenParams = paramsBuilder.build();
        KeyGenerator keyGenerator = KeyGenerator.getInstance(
                KeyProperties.KEY_ALGORITHM_AES,
                ANDROID_KEYSTORE
        );
        keyGenerator.init(keyGenParams);
        return keyGenerator.generateKey();
    }
}
