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

import android.app.AlertDialog;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.security.keystore.KeyPermanentlyInvalidatedException;
import android.security.keystore.KeyProperties;
import android.text.Editable;
import android.util.Base64;
import android.util.Log;
import android.widget.EditText;

import androidx.annotation.NonNull;
import androidx.biometric.BiometricManager;
import androidx.biometric.BiometricPrompt;
import androidx.core.content.ContextCompat;

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStoreException;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;

import io.trustbuilder.mfa.android.demoapp.security.CryptoUtils;
import io.trustbuilder.mfa.android.demoapp.security.KeystoreService;
import io.trustbuilder.mfa.android.demoapp.utils.PinEditable;
import io.trustbuilder.mfa.android.demoapp.utils.UiUtils;
import io.trustbuilder.mfa.lib.definition.CredTypeEnum;
import io.trustbuilder.mfa.lib.generator.crypto.CipherTools;
import io.trustbuilder.mfa.lib.generator.crypto.GeneratorCryptoTools;
import io.trustbuilder.mfa.lib.usecase.IUseCase;
import io.trustbuilder.mfa.lib.usecase.definition.CredMode;
import io.trustbuilder.mfa.lib.usecase.definition.CredValue;
import io.trustbuilder.mfa.lib.usecase.definition.biokeyregistration.BiokeyRegistration;
import io.trustbuilder.mfa.lib.usecase.definition.biokeyregistration.BiokeyRegistrationExecuteParams;

public abstract class TrustBuilderAuthenticationActivity extends TrustBuilderActivity {

    protected abstract <T extends IUseCase> void executeUseCase(CredValue cred, T usecase) throws Exception;

    public <T extends IUseCase> void displayCredentials(CredTypeEnum credType, CredMode credMode, byte[] bioChallenge , T usecase) {
        if (credType == CredTypeEnum.BIO) {
            authBio(bioChallenge, credType, usecase);
        } else {
            displaySecretCode(credType,credMode , usecase);
        }
    }

    private <T extends IUseCase> void displaySecretCode(CredTypeEnum credType, CredMode credMode, T usecase){
        AlertDialog.Builder builder;

        if (credMode == CredMode.EXISTING) {
            builder = UiUtils.createSecretCodeValidationAlertDialogBuilder(this);
        } else {
            builder = UiUtils.createSecretCodeDefinitionAlertDialogBuilder(this);
        }
        EditText secretCodeInput = UiUtils.createSecretCodeEditText(this);
        secretCodeInput.setEditableFactory(new Editable.Factory() {
            public Editable newEditable(CharSequence source) {
                PinEditable pinEditable = new PinEditable();
                pinEditable.append(source);
                return pinEditable;
            }
        });
        builder.setView(secretCodeInput);
        builder.setPositiveButton("Validate", (dialog, which) -> {
            try {
                byte[] secretCode = PinEditable.getBytesFromTextview(secretCodeInput);
                CredValue cred = CredValue.builder()
                        .value(secretCode)
                        .credType(credType).build();
                executeUseCase(cred, usecase);
            } catch (Exception e) {
                displayException(e);
            }
        });
        builder.setNegativeButton(CANCEL_BUTTON, (dialog, which) -> dialog.cancel());
        builder.show();
    }

    public <T extends IUseCase> void authBio(byte[] bioChallenge, CredTypeEnum credType, T usecase) {
        BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
                .setTitle("Biometric login for my app")
                .setSubtitle("Log in using your biometric credential")
                .setNegativeButtonText(CANCEL_BUTTON)
                .build();
        CipherTools cipherTools = new CipherTools(KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7);
        authBioCallback = createAuthenticationCallback(false, credType, null, usecase, bioChallenge, null, cipherTools);

        initializeCrypto(bioChallenge);
        KeystoreService keystoreService = new KeystoreService();
        SecretKey secretKey = getSecretKey(keystoreService);
        if (secretKey != null && initCipherForDecrypt(keystoreService, secretKey, cipherTools)) {
            authenticateWithBiometric(promptInfo, authBioCallback, cipherTools);
        } else {
            displayJsonResult("Unable to decrypt biometrics object");
        }
    }

    protected  void authenticateWithBiometric(BiometricPrompt.PromptInfo promptInfo, BiometricPrompt.AuthenticationCallback callback, CipherTools cipherTools){
        BiometricManager biometricManager = BiometricManager.from(this);
        BiometricPrompt biometricPrompt = new BiometricPrompt(this, ContextCompat.getMainExecutor(this), callback);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            switch (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL)) {
                case BiometricManager.BIOMETRIC_SUCCESS:
                    Log.d(TAG, "App can authenticate using biometrics.");
                    biometricPrompt.authenticate(promptInfo, new BiometricPrompt.CryptoObject(cipherTools.getCipher()));
                    break;
                case BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE:
                    Log.e(TAG, "No biometric features available on this device.");
                    displayJsonResult("No biometric features available on this device.");
                    break;
                case BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE:
                    Log.e(TAG, "Biometric features are currently unavailable.");
                    displayJsonResult("Biometric features are currently unavailable.");
                    break;
                case BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED:
                    Log.e(TAG, "BIOMETRIC_ERROR_NONE_ENROLLED.");
                    displayJsonResult("You need to allow bio on your device");
                    break;
                default:
                    Log.e(TAG, "Unknown biometrics error");
                    break;
            }
        } else {
            displayJsonResult("Biometric not allowed on your device");
        }
    }

    protected BiometricPrompt.AuthenticationCallback authBioCallback;

    protected <T extends IUseCase> BiometricPrompt.AuthenticationCallback createAuthenticationCallback(boolean registerBio, CredTypeEnum credType, byte[] credValue, T usecase, byte[] bioChallenge, byte[] disclaimer, CipherTools cipherTools) {
        return new BiometricPrompt.AuthenticationCallback() {
            @Override
            public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
                super.onAuthenticationError(errorCode, errString);
                Log.e(TAG, "Biometric authentication error : " + errString);
            }

            @Override
            public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
                super.onAuthenticationSucceeded(result);
                try {
                    GeneratorCryptoTools cryptoTools = new GeneratorCryptoTools();
                    KeystoreService keystoreService = new KeystoreService();
                    if (registerBio) {
                        CredValue cred = CredValue.builder()
                                .value(credValue)
                                .credType(credType).build();
                        BiokeyRegistration biokeyRegistration = (BiokeyRegistration) usecase;
                        byte[] bioPubkey = null;
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                            bioPubkey = CryptoUtils.generateRsaKeyForBio(tknAlias.getText().toString(), keystoreService);
                        } else {
                            bioPubkey = CryptoUtils.generateRsaKeyForBioForOldAndroid(tknAlias.getText().toString(), keystoreService);
                        }
                        byte[] signbioChallenge = cryptoTools.sign(keystoreService.getPrivateKey(tknAlias.getText().toString()), bioChallenge);
                        BiokeyRegistrationExecuteParams biokeyRegistrationExecuteParams =
                                BiokeyRegistrationExecuteParams.builder().credValue(cred).disclaimer(disclaimer).bioPubKey(bioPubkey)
                                        .signedBioChallenge(signbioChallenge).build();
                        Object obj = biokeyRegistration.execute(biokeyRegistrationExecuteParams);
                        displayJsonResult(obj);
                        accessValueInKeyStoreForEncrypt(cipherTools);
                    } else {
                        byte[] signbioChallenge = cryptoTools.sign(keystoreService.getPrivateKey(tknAlias.getText().toString()), bioChallenge);
                        CredValue cred = CredValue.builder()
                                .value(signbioChallenge)
                                .credType(credType).build();
                        executeUseCase(cred, usecase);
                    }
                } catch (GeneralSecurityException | IOException e) {
                    Log.d(TAG, "Error during biometrics authentication" , e);
                    displayJsonResult("Error during biometrics authentication : " + e.getMessage());
                } catch (Exception e) {
                    Log.d(TAG, "Unexpected error during biometrics authentication" , e);
                    displayJsonResult("Unexpected error during biometrics authentication : " + e.getMessage());
                }
            }

            @Override
            public void onAuthenticationFailed() {
                super.onAuthenticationFailed();
                Log.e(TAG, "Biometric authentication failed");
            }
        };
    }

    private boolean initCipherForDecrypt(KeystoreService keystoreService, SecretKey secretKey, CipherTools cipherTools) {
        boolean initCipher = false;
        try {
            SharedPreferences sharedPreferences = this.getSharedPreferences(BIO_SHARED_PREFS_KEY_NAME, Context.MODE_PRIVATE);
            byte[] ivBytes =
                    Base64.decode(sharedPreferences.getString("bio_tb" + tknAlias.getText().toString(), ""), Base64.DEFAULT);
            cipherTools.initCipher(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(ivBytes));

            initCipher = true;
        } catch (KeyPermanentlyInvalidatedException e) {
            Log.e(TAG, "Error initiating cipher to decrypt: key has been invalidated", e);
            removePermanentlyInvalidatedKey(keystoreService);
        } catch (GeneralSecurityException e) {
            Log.e(TAG, "Error initiating cipher to decrypt", e);
        }
        return initCipher;
    }

    private void accessValueInKeyStoreForEncrypt(CipherTools cipherTools) {
        // If the user has authenticated with fingerprint, verify that using cryptography and
        // then return the encrypted token
        try {
            cipherTools.doFinal(challengeToSignForBio);
            SharedPreferences sharedPreferences = this.getSharedPreferences(BIO_SHARED_PREFS_KEY_NAME, Context.MODE_PRIVATE);
            SharedPreferences.Editor editor = sharedPreferences.edit();
            editor.putString("bio_tb" + tknAlias.getText().toString(), Base64.encodeToString(cipherTools.getCipher().getIV(), Base64.DEFAULT));
            editor.apply();
            resetToEncrypt();
        } catch (GeneralSecurityException e) {
            Log.e(TAG, "Error getting key from shared prefs", e);
        }
    }

    protected SecretKey getSecretKey(KeystoreService keystoreService) {
        SecretKey key = null;
        try {
            key = keystoreService.getKey(CLIENT_ID, ANDROID_KEY_STORE);
        } catch (GeneralSecurityException | IOException e) {
            Log.e(TAG, "Error getting secret key from keystore", e);
        }
        return key;
    }

    protected void removePermanentlyInvalidatedKey(KeystoreService keystoreService) {
        try {
            keystoreService.deleteEntry(CLIENT_ID, ANDROID_KEY_STORE);
        } catch (KeyStoreException e) {
            Log.e(TAG, "Error deleting key in keystore", e);
        }
    }

    protected static void initializeCrypto(byte[] bioChallenge) {
        challengeToSignForBio = bioChallenge;
    }

    protected static void resetToEncrypt() {
        challengeToSignForBio = null;
    }

}
