package co.infinum.goldfinger;

import android.content.Context;
import android.os.Build;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.StringRes;
import androidx.biometric.BiometricManager;
import androidx.biometric.BiometricPrompt;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import co.infinum.goldfinger.crypto.CipherCrypter;
import co.infinum.goldfinger.crypto.CipherFactory;
import co.infinum.goldfinger.crypto.MacCrypter;
import co.infinum.goldfinger.crypto.MacFactory;
import co.infinum.goldfinger.crypto.SignatureCrypter;
import co.infinum.goldfinger.crypto.SignatureFactory;
import co.infinum.goldfinger.crypto.impl.AesCipherFactory;
import co.infinum.goldfinger.crypto.impl.Base64CipherCrypter;

@SuppressWarnings("unused")
public interface Goldfinger {

    /**
     * @deprecated Use {@link #hasFingerprintHardware(int)} instead.
     */
    @Deprecated
    boolean hasFingerprintHardware();

    /**
     * Returns true if user has fingerprint hardware, false otherwise.
     */
    boolean hasFingerprintHardware(int authenticators);

    /**
     * @deprecated Use {@link #hasEnrolledFingerprint(int)} instead.
     */
    @Deprecated
    boolean hasEnrolledFingerprint();

    /**
     * Returns true if user has enrolled fingerprint, false otherwise.
     */
    boolean hasEnrolledFingerprint(int authenticators);

    /**
     * @deprecated Use {@link #canAuthenticate(int)} instead.
     */
    @Deprecated
    boolean canAuthenticate();

    /**
     * @see BiometricManager#canAuthenticate(int)
     */
    boolean canAuthenticate(int authenticators);

    /**
     * Authenticate user via Fingerprint.
     * <p>
     * Example - Process payment after successful fingerprint authentication.
     *
     * @see PromptParams
     */
    void authenticate(@NonNull PromptParams params, @NonNull Callback callback);

    /**
     * Authenticate user via Fingerprint. If user is successfully authenticated,
     * {@link CrypterProxy} implementation is used to automatically encrypt given value.
     * <p>
     * Use it when saving some data that should not be saved as plain text (e.g. password).
     * To decrypt the value use {@link Goldfinger#decrypt} method.
     * <p>
     * Example - Allow auto-login via Fingerprint.
     *
     * @param params   parameters used to build {@link BiometricPrompt} instance
     * @param key      unique key identifier, used to store cipher IV internally
     * @param value    String value which will be encrypted if user successfully authenticates
     * @param callback callback
     * @see Goldfinger.Callback
     */
    void encrypt(
        @NonNull PromptParams params,
        @NonNull String key,
        @NonNull String value,
        @NonNull Callback callback
    );

    /**
     * Authenticate user via Fingerprint. If user is successfully authenticated,
     * {@link CrypterProxy} implementation is used to automatically decrypt given value.
     * <p>
     * Should be used together with {@link Goldfinger#encrypt} to decrypt saved data.
     *
     * @param key   unique key identifier, used to load Cipher IV internally
     * @param value String value which will be decrypted if user successfully authenticates
     */
    void decrypt(
        @NonNull PromptParams params,
        @NonNull String key,
        @NonNull String value,
        @NonNull Callback callback
    );

    /**
     * Cancel current active Fingerprint authentication.
     */
    void cancel();

    /**
     * Become Bob the builder.
     */
    @SuppressWarnings("UnusedReturnValue")
    class Builder {

        @NonNull private final Context context;
        @Nullable private CipherFactory cipherFactory;
        @Nullable private MacFactory macFactory;
        @Nullable private SignatureFactory signatureFactory;
        @Nullable private CipherCrypter cipherCrypter;
        @Nullable private MacCrypter macCrypter;
        @Nullable private SignatureCrypter signatureCrypter;
        @Nullable private String key;
        @Nullable private String value;

        public Builder(@NonNull Context context) {
            this.context = context;
        }

        @NonNull
        public Goldfinger build() {
            ensureParamsValid();

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                return buildGoldfinger();
            } else {
                return new GoldfingerMock();
            }
        }

        @NonNull
        public Builder cipherCrypter(@Nullable CipherCrypter cipherCrypter) {
            this.cipherCrypter = cipherCrypter;
            return this;
        }

        @NonNull
        public Builder cipherFactory(@Nullable CipherFactory cipherFactory) {
            this.cipherFactory = cipherFactory;
            return this;
        }

        @NonNull
        public Builder logEnabled(boolean logEnabled) {
            LogUtils.setEnabled(logEnabled);
            return this;
        }

        @NonNull
        public Builder macCrypter(@Nullable MacCrypter macCrypter) {
            this.macCrypter = macCrypter;
            return this;
        }

        @NonNull
        public Builder macFactory(@Nullable MacFactory macFactory) {
            this.macFactory = macFactory;
            return this;
        }

        @NonNull
        public Builder signatureCrypter(@Nullable SignatureCrypter signatureCrypter) {
            this.signatureCrypter = signatureCrypter;
            return this;
        }

        @NonNull
        public Builder signatureFactory(@Nullable SignatureFactory signatureFactory) {
            this.signatureFactory = signatureFactory;
            return this;
        }

        @NonNull
        @RequiresApi(Build.VERSION_CODES.M)
        private Goldfinger buildGoldfinger() {
            if (macCrypter == null && signatureCrypter == null && cipherCrypter == null) {
                this.cipherCrypter = new Base64CipherCrypter();
            }
            if (macFactory == null && signatureFactory == null && cipherFactory == null) {
                this.cipherFactory = new AesCipherFactory(context);
            }
            AsyncCryptoObjectFactory asyncFactory = new AsyncCryptoObjectFactory(
                new CryptoObjectFactory(cipherFactory, macFactory, signatureFactory)
            );
            CrypterProxy cryptoProxy = new CrypterProxy(cipherCrypter, macCrypter, signatureCrypter);

            return new GoldfingerImpl(context, asyncFactory, cryptoProxy);
        }

        private void ensureParamsValid() {
            if (macFactory != null && macCrypter == null || macFactory == null && macCrypter != null) {
                throw new RuntimeException(
                    "To use CryptoObject with MacObject you must provide both MacFactory and "
                        + "MacCrypter implementation. Use Goldfinger.Builder#macFactory(MacFactory) and "
                        + "Goldfinger.Builder#macCrypter(MacCrypter) methods to set values."
                );
            }

            if (signatureFactory != null && signatureCrypter == null || signatureFactory == null && signatureCrypter != null) {
                throw new RuntimeException(
                    "To use CryptoObject with SignatureObject you must provide both SignatureFactory and "
                        + "SignatureCrypter implementation. Use Goldfinger.Builder#signatureFactory(SignatureFactory) and "
                        + "Goldfinger.Builder#signatureCrypter(SignatureCrypter) methods to set values."
                );
            }
        }
    }

    @SuppressWarnings("WeakerAccess")
    class PromptParams {

        @NonNull private final Object dialogOwner;
        @Nullable private final String description;
        @Nullable private final String negativeButtonText;
        @Nullable private final String subtitle;
        @Nullable private final String title;
        private final boolean confirmationRequired;
        private final boolean deviceCredentialsAllowed;
        private final int allowedAuthenticators;

        private PromptParams(
            @NonNull Object dialogOwner,
            @Nullable String title,
            @Nullable String description,
            @Nullable String negativeButtonText,
            @Nullable String subtitle,
            boolean confirmationRequired,
            boolean deviceCredentialsAllowed,
            int allowedAuthenticators
        ) {
            this.dialogOwner = dialogOwner;
            this.title = title;
            this.description = description;
            this.negativeButtonText = negativeButtonText;
            this.subtitle = subtitle;
            this.confirmationRequired = confirmationRequired;
            this.deviceCredentialsAllowed = deviceCredentialsAllowed;
            this.allowedAuthenticators = allowedAuthenticators;
        }

        @NonNull
        public Object dialogOwner() {
            return dialogOwner;
        }

        @Nullable
        public String title() {
            return title;
        }

        @Nullable
        public String description() {
            return description;
        }

        @Nullable
        public String negativeButtonText() {
            return negativeButtonText;
        }

        @Nullable
        public String subtitle() {
            return subtitle;
        }

        public boolean confirmationRequired() {
            return confirmationRequired;
        }

        public boolean deviceCredentialsAllowed() {
            return deviceCredentialsAllowed;
        }

        public int allowedAuthenticators() {
            return allowedAuthenticators;
        }

        /**
         * Create new {@link BiometricPrompt.PromptInfo} instance. Parameter
         * validation is done earlier in the code so we can trust the data at
         * this step.
         */
        @SuppressWarnings("ConstantConditions")
        @NonNull
        BiometricPrompt.PromptInfo buildPromptInfo() {
            BiometricPrompt.PromptInfo.Builder builder = new BiometricPrompt.PromptInfo.Builder()
                .setTitle(title)
                .setSubtitle(subtitle)
                .setDescription(description)
                .setAllowedAuthenticators(allowedAuthenticators)
                .setNegativeButtonText(negativeButtonText)
                .setConfirmationRequired(confirmationRequired);

            return builder.build();
        }

        public static class Builder {

            /* Dialog dialogOwner can be either Fragment or FragmentActivity */
            @NonNull private Object dialogOwner;
            @Nullable private String description;
            @Nullable private String negativeButtonText;
            @Nullable private String subtitle;
            @Nullable private String title;
            private boolean confirmationRequired;
            private int allowedAuthenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK;

            public Builder(@NonNull FragmentActivity activity) {
                this.dialogOwner = activity;
            }

            public Builder(@NonNull Fragment fragment) {
                this.dialogOwner = fragment;
            }

            @NonNull
            public PromptParams build() {
                boolean deviceCredentialAllowed = (allowedAuthenticators & BiometricManager.Authenticators.DEVICE_CREDENTIAL) != 0;

                return new PromptParams(
                    dialogOwner,
                    title,
                    description,
                    negativeButtonText,
                    subtitle,
                    confirmationRequired,
                    deviceCredentialAllowed,
                    allowedAuthenticators
                );
            }

            /**
             * @see BiometricPrompt.PromptInfo.Builder#setTitle
             */
            @NonNull
            public Builder title(@NonNull String title) {
                this.title = title;
                return this;
            }

            /**
             * @see BiometricPrompt.PromptInfo.Builder#setTitle
             */
            @NonNull
            public Builder title(@StringRes int resId) {
                this.title = getString(resId);
                return this;
            }

            /**
             * @see BiometricPrompt.PromptInfo.Builder#setDescription
             */
            @NonNull
            public Builder description(@Nullable String description) {
                this.description = description;
                return this;
            }

            /**
             * @see BiometricPrompt.PromptInfo.Builder#setDescription
             */
            @NonNull
            public Builder description(@StringRes int resId) {
                this.description = getString(resId);
                return this;
            }

            /**
             * @see BiometricPrompt.PromptInfo.Builder#setNegativeButtonText
             */
            @NonNull
            public Builder negativeButtonText(@NonNull String negativeButtonText) {
                this.negativeButtonText = negativeButtonText;
                return this;
            }

            /**
             * @see BiometricPrompt.PromptInfo.Builder#setNegativeButtonText
             */
            @NonNull
            public Builder negativeButtonText(@StringRes int resId) {
                this.negativeButtonText = getString(resId);
                return this;
            }

            /**
             * @see BiometricPrompt.PromptInfo.Builder#setSubtitle
             */
            @NonNull
            public Builder subtitle(@Nullable String subtitle) {
                this.subtitle = subtitle;
                return this;
            }

            /**
             * @see BiometricPrompt.PromptInfo.Builder#setSubtitle
             */
            @NonNull
            public Builder subtitle(@StringRes int resId) {
                this.subtitle = getString(resId);
                return this;
            }

            /**
             * @see BiometricPrompt.PromptInfo.Builder#setConfirmationRequired
             */
            @NonNull
            public Builder confirmationRequired(boolean confirmationRequired) {
                this.confirmationRequired = confirmationRequired;
                return this;
            }

            /**
             * @see BiometricPrompt.PromptInfo.Builder#setAllowedAuthenticators(int)
             */
            @NonNull
            public Builder allowedAuthenticators(int allowedAuthenticators) {
                this.allowedAuthenticators = allowedAuthenticators;
                return this;
            }

            @Nullable
            private String getString(@StringRes int resId) {
                if (dialogOwner instanceof Fragment) {
                    return ((Fragment) dialogOwner).getString(resId);
                }

                if (dialogOwner instanceof FragmentActivity) {
                    return ((FragmentActivity) dialogOwner).getString(resId);
                }

                return null;
            }
        }
    }

    /**
     * Result wrapper class containing all the useful information about
     * fingerprint authentication and value for encryption/decryption operations.
     */
    class Result {

        /**
         * @see Goldfinger.Type
         */
        @NonNull private final Goldfinger.Type type;

        /**
         * @see Goldfinger.Reason
         */
        @NonNull private final Goldfinger.Reason reason;

        /**
         * Authentication value. If standard {@link Goldfinger#authenticate} method is used,
         * returned value is null.
         * <p>
         * IFF {@link Goldfinger#encrypt} or {@link Goldfinger#decrypt}
         * is used, the value contains encrypted or decrypted String.
         * <p>
         * In all other cases, the value is null.
         */
        @Nullable private final String value;

        /**
         * System message returned by {@link BiometricPrompt.AuthenticationCallback}.
         * A human-readable error string that can be shown in UI.
         */
        @Nullable private final String message;

        Result(@NonNull Type type, @NonNull Reason reason) {
            this(type, reason, null, null);
        }

        Result(@NonNull Type type, @NonNull Reason reason, @Nullable String value, @Nullable String message) {
            this.type = type;
            this.reason = reason;
            this.value = value;
            this.message = message;
        }

        @Nullable
        public String message() {
            return message;
        }

        @NonNull
        public Reason reason() {
            return reason;
        }

        @NonNull
        public Type type() {
            return type;
        }

        @Nullable
        public String value() {
            return value;
        }
    }

    /**
     * Callback used to receive Goldfinger results.
     */
    interface Callback {

        /**
         * Returns fingerprint result and will be invoked multiple times during
         * fingerprint authentication as not all fingerprint results complete
         * the authentication.
         * <p>
         * Result callback invoked for every fingerprint result (success, error or info).
         * It can be invoked multiple times during single fingerprint authentication.
         *
         * @param result contains fingerprint result information
         * @see Goldfinger.Result
         */
        void onResult(@NonNull Goldfinger.Result result);

        /**
         * Critical error happened and fingerprint authentication is stopped.
         */
        void onError(@NonNull Exception e);
    }

    /**
     * Describes in detail why {@link Callback#onResult} is dispatched.
     */
    enum Reason {
        /**
         * @see BiometricPrompt#ERROR_HW_UNAVAILABLE
         */
        HARDWARE_UNAVAILABLE,

        /**
         * @see BiometricPrompt#ERROR_UNABLE_TO_PROCESS
         */
        UNABLE_TO_PROCESS,

        /**
         * @see BiometricPrompt#ERROR_TIMEOUT
         */
        TIMEOUT,

        /**
         * @see BiometricPrompt#ERROR_NO_SPACE
         */
        NO_SPACE,

        /**
         * @see BiometricPrompt#ERROR_CANCELED
         */
        CANCELED,

        /**
         * @see BiometricPrompt#ERROR_LOCKOUT
         */
        LOCKOUT,

        /**
         * @see BiometricPrompt#ERROR_VENDOR
         */
        VENDOR,

        /**
         * @see BiometricPrompt#ERROR_LOCKOUT_PERMANENT
         */
        LOCKOUT_PERMANENT,

        /**
         * @see BiometricPrompt#ERROR_USER_CANCELED
         */
        USER_CANCELED,

        /**
         * @see BiometricPrompt#ERROR_NO_BIOMETRICS
         */
        NO_BIOMETRICS,

        /**
         * @see BiometricPrompt#ERROR_HW_NOT_PRESENT
         */
        HW_NOT_PRESENT,

        /**
         * @see BiometricPrompt#ERROR_NEGATIVE_BUTTON
         */
        NEGATIVE_BUTTON,

        /**
         * @see BiometricPrompt#ERROR_NO_DEVICE_CREDENTIAL
         */
        NO_DEVICE_CREDENTIAL,

        /**
         * @see BiometricPrompt#ERROR_SECURITY_UPDATE_REQUIRED
         */
        SECURITY_UPDATE_REQUIRED,

        /**
         * Dispatched when Fingerprint authentication is initialized correctly and
         * just before actual authentication is started. Can be used to update UI if necessary.
         */
        AUTHENTICATION_START,

        /**
         * @see BiometricPrompt.AuthenticationCallback#onAuthenticationSucceeded
         */
        AUTHENTICATION_SUCCESS,

        /**
         * @see BiometricPrompt.AuthenticationCallback#onAuthenticationFailed
         */
        AUTHENTICATION_FAIL,

        /**
         * Unknown reason.
         */
        UNKNOWN
    }

    /**
     * Describes the type of the result received in {@link Callback#onResult}
     */
    enum Type {

        /**
         * Fingerprint authentication is successfully finished. {@link Goldfinger.Result}
         * will contain value in case of {@link PromptParams.Builder#decrypt} or
         * {@link PromptParams.Builder#encrypt} invocation.
         */
        SUCCESS,

        /**
         * Fingerprint authentication is still active. {@link Goldfinger.Result} contains
         * additional information about currently active fingerprint authentication.
         */
        INFO,

        /**
         * Fingerprint authentication is unsuccessfully finished. {@link Goldfinger.Result}
         * contains the reason why the authentication failed.
         */
        ERROR
    }
}
