package com.mobify.astro.security;

import android.content.Context;
import android.util.Log;

import java.security.Provider;
import java.security.Security;
import java.util.ArrayList;
import java.util.Set;

/**
 * CryptoManager
 *
 * Creates a public/private key pair that we use to encrypt a symmetric key we generated.
 * The key pair is stored in the Android KeyStore.
 *
 * The symmetric key we generated is store in a sharePref file that is only accessible by
 * this application. This key is encrypted using the public key before writing into the
 * sharePref file. We use the symmetric key to perform data encryption and decryption.
 *
 * CryptoManager uses AES/GCM symmetric encryption when available. A lot of older devices do
 * not support this encryption algorithm. If AES/GCM is not supported then the crypto manager
 * falls back to using AES/CBC with HMAC symmetric encryption.
 */
public class CryptoManager {
    private final static String TAG = CryptoManager.class.getName();
    private final static String SHARE_PREF_SYMMETRIC_KEY = "sharePrefSymmetricKey";
    private final static String SHARE_PREF_SYMMETRIC_HMAC_KEY = "sharePrefSymmetricHmacKey";

    private final Context context;
    private final SymmetricKeyService symmetricKeyService;
    private final SymmetricCryptosystem symmetricCryptosystem;

    private final boolean isUsingAesGcmForSymmetricEncryption;

    private final CryptoManagerListener callback;

    public interface CryptoManagerListener {
        void onKeysDestroy();
    }

    public CryptoManager(Context context, CryptoManagerListener cryptoManagerListener) throws Exception {
        this.context = context;
        callback = cryptoManagerListener;
        isUsingAesGcmForSymmetricEncryption = isAesGcmAvailable();

        ArrayList<String> sharedPrefKeys = new ArrayList<>();
        sharedPrefKeys.add(SHARE_PREF_SYMMETRIC_KEY);

        if (!isUsingAesGcmForSymmetricEncryption) {
            sharedPrefKeys.add(SHARE_PREF_SYMMETRIC_HMAC_KEY);
        }
        symmetricKeyService = new SymmetricKeyService(context, sharedPrefKeys,
                new SymmetricKeyService.SymmetricKeyServiceListener() {
                    @Override
                    public void onKeysDestroy() {
                        callback.onKeysDestroy();
                    }
        });

        if (isUsingAesGcmForSymmetricEncryption) {
            symmetricCryptosystem = new AesGcmSymmetricCryptosystem(symmetricKeyService, SHARE_PREF_SYMMETRIC_KEY);
        } else {
            symmetricCryptosystem = new AesCbcHmacSymmetricCryptosystem(symmetricKeyService, SHARE_PREF_SYMMETRIC_KEY, SHARE_PREF_SYMMETRIC_HMAC_KEY);
        }
    }

    protected static boolean isAesGcmAvailable() {
        Provider[] providers = Security.getProviders();
        for (Provider provider : providers) {
            Set<Provider.Service> services = provider.getServices();
            for (Provider.Service service : services) {
                if (AesGcmSymmetricCryptosystem.AES_GCM_CIPHER_ALGORITHM.contentEquals(service.getAlgorithm())) {
                    Log.i(TAG, AesGcmSymmetricCryptosystem.AES_GCM_CIPHER_ALGORITHM + " found!");
                    return true;
                }
            }
        }

        Log.i(TAG, AesGcmSymmetricCryptosystem.AES_GCM_CIPHER_ALGORITHM + " not found!");
        return false;
    }

    public String encrypt(String keyToAssociate, String valueToEncrypt) throws Exception {
        try {
            return symmetricCryptosystem.encrypt(keyToAssociate, valueToEncrypt);
        } catch (Exception e) {
            Log.e(TAG, Log.getStackTraceString(e));
            symmetricKeyService.resetKeys(e);
            return "";
        }
    }

    public String decrypt(String keyToAssociate, String valueToDecrypt) throws Exception {
        try {
            return symmetricCryptosystem.decrypt(keyToAssociate, valueToDecrypt);
        } catch (Exception e) {
            Log.e(TAG, Log.getStackTraceString(e));
            symmetricKeyService.resetKeys(e);
            return null;
        }
    }
}
