package info.knacki.pass.ui.passwordPicker;

import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.support.annotation.RequiresApi;
import android.support.v4.hardware.fingerprint.FingerprintManagerCompat;
import android.support.v4.os.CancellationSignal;
import android.util.Base64;
import android.util.Base64DataException;
import android.widget.Toast;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
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 java.util.ArrayDeque;
import java.util.Queue;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;

import info.knacki.pass.R;
import info.knacki.pass.io.FileInterfaceFactory;
import info.knacki.pass.io.FileUtils;
import info.knacki.gitdroid.callback.OnErrorListener;
import info.knacki.gitdroid.callback.OnResponseListener;
import info.knacki.pass.io.PathUtils;
import info.knacki.pass.ui.alertPrompt.AlertPrompt;
import info.knacki.pass.ui.alertPrompt.AlertPromptGenerator;
import info.knacki.pass.ui.alertPrompt.views.FingerprintView;

@RequiresApi(api = Build.VERSION_CODES.M)
class FingerprintPicker extends PasswordPicker {
    private static final Logger log = Logger.getLogger(FingerprintPicker.class.getName());
    private static final String KEY_STORE_PROVIDER = "AndroidKeyStore";
    private static final String KEY_NAME = "AndroidPassFingerprintKey";
    private static final String KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES;
    private static final String KEY_BLOCK_MODE = KeyProperties.BLOCK_MODE_CBC;
    private static final String KEY_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7;

    private final File fFile;
    private IvAndPayload fEncrypted;
    private Queue<String> fPassword;
    private Queue<String> fPasswordIterator;
    private AlertPrompt fDisplayingPrompt;

    private static final class IvAndPayload {
        static final char SEPARATOR = '-';
        final byte[] fIV;
        final byte[] fPayload;

        IvAndPayload(Cipher c, byte[] payload) {
            fIV = c.getIV();
            fPayload = payload;
        }

        IvAndPayload(String raw) throws Base64DataException {
            int sep = raw.indexOf(SEPARATOR);
            if (sep < 0)
                throw new Base64DataException("Cannot find separator");
            fIV = Base64.decode(raw.substring(0, sep), Base64.NO_WRAP);
            fPayload = Base64.decode(raw.substring(sep + 1), Base64.NO_WRAP);
        }

        public String toString() {
            return Base64.encodeToString(fIV, Base64.NO_WRAP) + SEPARATOR + Base64.encodeToString(fPayload, Base64.NO_WRAP);
        }
    }

    FingerprintPicker(Context ctx, AlertPromptGenerator alertFactory) {
        super(ctx, alertFactory);
        fFile = new File(PathUtils.GetFingerprintFile(ctx));
        fPassword = fPasswordIterator = null;
    }

    private FingerprintManagerCompat GetFingerprintManager() {
        return FingerprintManagerCompat.from(fContext);
    }

    private Key GetKey(KeyStore keyStore) {
        try {
            return keyStore.getKey(KEY_NAME, null);
        }
        catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) {
            log.log(Level.SEVERE, e.getMessage(), e);
            return null;
        }
    }

    private Key TryGetKey(OnErrorListener resp) {
        KeyStore keyStore;
        try {
            keyStore = KeyStore.getInstance(KEY_STORE_PROVIDER);
            keyStore.load(null);
        } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e) {
            final String msg = "Cannot get android keystore";
            resp.OnError(msg, e);
            log.log(Level.SEVERE, msg, e);
            return null;
        }
        return GetKey(keyStore);
    }

    private boolean GenerateKey(OnErrorListener resp) {
        log.info("Generating new key");
        try {
            KeyGenerator keyGenerator;
            keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM, KEY_STORE_PROVIDER);
            keyGenerator.init(new KeyGenParameterSpec.Builder(KEY_NAME, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                    .setBlockModes(KEY_BLOCK_MODE)
                    .setEncryptionPaddings(KEY_PADDING)
                    .build());
            keyGenerator.generateKey();
        } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
            final String msg = "Cannot create Key generator";
            resp.OnError(msg, e);
            log.log(Level.SEVERE, msg, e);
            return false;
        }
        return true;
    }

    private FingerprintManagerCompat.CryptoObject InitCryptoObject(IvAndPayload ivForDecoding, OnErrorListener resp) {
        Key key = TryGetKey(resp);
        if (key == null) {
            if (!GenerateKey(resp)) {
                // Unrecoverable failure, error already handled
                return null;
            }
            key = TryGetKey(resp);
            if (key == null) {
                // Unrecoverable failure
                final String msg = "Cannot get android keystore";
                log.log(Level.SEVERE, msg);
                resp.OnError(msg, null);
                return null;
            }
        }

        Cipher c;
        try {
            c = InitCipher(key, ivForDecoding);
        }
        catch (InvalidKeyException e) {
            return null;
        }
        catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidAlgorithmParameterException e) {
            final String msg = "Cannot create Cipher";
            resp.OnError(msg, e);
            log.log(Level.SEVERE, msg, e);
            return null;
        }
        return new FingerprintManagerCompat.CryptoObject(c);
    }

    private static Cipher InitCipher(Key key, IvAndPayload ivForDecoding) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
        Cipher c = Cipher.getInstance(KEY_ALGORITHM +"/" +KEY_BLOCK_MODE +"/" +KEY_PADDING);
        if (ivForDecoding == null)
            c.init(Cipher.ENCRYPT_MODE, key);
        else
            c.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(ivForDecoding.fIV));
        return c;
    }

    private void DoWriteFile(FingerprintManagerCompat.CryptoObject cryptoObject, String content) {
        try {
            Cipher cipher = cryptoObject.getCipher();
            if (cipher == null) {
                log.severe("Cannot write content: cipher is null");
                return;
            }
            byte[] encrypted = cipher.doFinal(content.getBytes());
            Writer writer = new FileWriter(fFile);
            writer.write((new IvAndPayload(cipher, encrypted)).toString());
            writer.close();
        }
        catch (IOException | BadPaddingException | IllegalBlockSizeException | NullPointerException e) {
            log.log(Level.SEVERE, e.getMessage(), e);
        }
    }

    private void DisplayPrompt(IvAndPayload ivForDecoding, OnResponseListener<FingerprintManagerCompat.CryptoObject> resp) {
        final CancellationSignal signal = new CancellationSignal();
        final FingerprintView fingerprintView = new FingerprintView(fContext);
        fDisplayingPrompt = fAlertFactory.Generate(fContext)
                .setView(fingerprintView)
                .setTitle(R.string.push_finger)
                .setNegativeButton(R.string.cancel, (dialogInterface, view) -> {
                    fDisplayingPrompt = null;
                    signal.cancel();
                    resp.OnError(fContext.getString(R.string.cancelled), null);
                })
                .show();
        final FingerprintManagerCompat.CryptoObject cryptoObject = InitCryptoObject(ivForDecoding, resp);
        if (cryptoObject == null)
            return; // Error already handled

        GetFingerprintManager().authenticate(cryptoObject, 0, signal, new FingerprintManagerCompat.AuthenticationCallback() {
            @Override
            public void onAuthenticationError(int errMsgId, CharSequence errString) {
                ClosePrompt();
                if (errMsgId != 5) // cancelled
                    resp.OnError(errString.toString(), null);
            }

            @Override
            public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
                log.severe("Authentication help " +helpString);
                super.onAuthenticationHelp(helpMsgId, helpString);
            }

            @Override
            public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) {
                ClosePrompt();
                resp.OnResponse(cryptoObject);
            }

            @Override
            public void onAuthenticationFailed() {
                ((FingerprintView)(fDisplayingPrompt.getView())).onAuthFailed();
            }
        }, null);
    }

    private void ClosePrompt() {
        if (fDisplayingPrompt != null) {
            fDisplayingPrompt.close();
            fDisplayingPrompt = null;
        }
    }

    private void ReadAllPasswords(final FileInterfaceFactory.OnPasswordEnteredListener onResp) {
        try {
            String raw = FileUtils.ReadAllFile(fFile);
            fEncrypted = new IvAndPayload(raw);
        } catch (IOException e) {
            // Fallback password
            SuperGetPassword(true, onResp);
            return;
        }

        DisplayPrompt(fEncrypted, new OnResponseListener<FingerprintManagerCompat.CryptoObject>() {
            @Override
            public void OnResponse(FingerprintManagerCompat.CryptoObject cryptoObject) {
                try {
                    Cipher cipher = cryptoObject.getCipher();
                    if (cipher == null) {
                        log.severe("Cannot read content: cipher is null");
                        Toast.makeText(fContext, R.string.fingerprint_init_error, Toast.LENGTH_LONG).show();
                        SuperGetPassword(false, onResp);
                        return;
                    }
                    byte[] decoded = cipher.doFinal(fEncrypted.fPayload);
                    String[] passwords = (new String(decoded, "UTF-8")).split("\n");
                    fPassword = new ArrayDeque<>();
                    fPasswordIterator = new ArrayDeque<>();
                    // Try all password in keystore
                    for (String i: passwords)
                        if (i.length() > 0)
                            fPassword.add(i);
                    fPasswordIterator.addAll(fPassword);
                    if (fPasswordIterator.isEmpty())
                        SuperGetPassword(true, onResp);
                    else
                        onResp.OnResponse(fPasswordIterator.remove());
                }
                catch (BadPaddingException | IllegalBlockSizeException | UnsupportedEncodingException e) {
                    OnError(e.getMessage(), e);
                }
            }

            @Override
            public void OnError(String msg, Throwable e) {
                new Handler(Looper.getMainLooper()).post(() -> Toast.makeText(fContext, msg, Toast.LENGTH_LONG).show());
                // Fallback password
                fPassword = new ArrayDeque<>();
                fPasswordIterator = new ArrayDeque<>();
                SuperGetPassword(false, onResp);
            }
        });
    }

    private void SuperGetPassword(boolean savePassword, FileInterfaceFactory.OnPasswordEnteredListener onResp) {
        ClosePrompt();
        FingerprintPicker.super.GetPassword(savePassword ? new FileInterfaceFactory.OnPasswordEnteredListener() {
            @Override
            public boolean OnResponse(String result) {
                if (result != null) {
                    AddPassword(result);
                }
                return onResp.OnResponse(result);
            }

            @Override
            public void OnError(String msg, Throwable e) {
                onResp.OnError(msg, e);
            }
        } : onResp);
    }

    @Override
    public FingerprintPicker GetPassword(FileInterfaceFactory.OnPasswordEnteredListener onResp) {
        if (fPassword == null)
            ReadAllPasswords(onResp);
        else if (fPasswordIterator.isEmpty())
            SuperGetPassword(true, onResp);
        else
            onResp.OnResponse(fPasswordIterator.remove());
        return this;
    }

    @Override
    public void WrongPassword() {

    }

    private void AddPassword(String newPass) {
        fEncrypted = null;
        DisplayPrompt(null, new OnResponseListener<FingerprintManagerCompat.CryptoObject>() {
            @Override
            public void OnResponse(FingerprintManagerCompat.CryptoObject cryptoObject) {
                StringBuilder prev = new StringBuilder();
                if (fPassword != null)
                    for (String i: fPassword)
                        prev.append(i).append('\n');
                prev.append(newPass);
                DoWriteFile(cryptoObject, prev.toString());
            }

            @Override
            public void OnError(String msg, Throwable e) {
            }
        });
    }
}
