package info.knacki.pass.io.pgp;

import android.content.Context;
import android.support.annotation.NonNull;

import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
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.Iterator;
import java.util.NoSuchElementException;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

import info.knacki.pass.io.FileUtils;
import info.knacki.pass.io.PathUtils;

public class GPGStorage {
    private static final Logger log = Logger.getLogger(GPGStorage.class.getName());
    private GPGStorage(){}

    private final static String KEY_NAME = GPGStorage.class.getName()+"_PGP_PRIVATE";
    private final static String KEY_PASS = GPGStorage.class.getName()+"___PASSWORD";

    static SecretKey FindSecretKey(@NonNull byte[] in) throws IOException {
        final PGPSecretKeyRingCollection pgpSec;

        try {
            pgpSec = new PGPSecretKeyRingCollection(in, new JcaKeyFingerprintCalculator());
        } catch (PGPException e) {
            throw new GPGUtil.MalformedKeyException(e);
        }
        Iterator<PGPSecretKeyRing> rIt = pgpSec.getKeyRings();
        if (rIt.hasNext()) {
            PGPSecretKey bcSecKey = rIt.next().getSecretKey();
            return new SecretKeySpec(bcSecKey.getEncoded(), "AES");
        }
        throw new NoSuchElementException("GPG key not found");
    }

    private static KeyStore GetKeystore(Context ctx, boolean read) {
        KeyStore ks;

        try {
            ks = KeyStore.getInstance("UBER", "BC");
            if (read) {
                File gpgKeyFile = new File(PathUtils.GetGPGKeyFile(ctx));
                if (!gpgKeyFile.exists())
                    throw new FileNotFoundException(gpgKeyFile.getName());
                ks.load(new FileInputStream(gpgKeyFile), KEY_PASS.toCharArray());
            } else {
                ks.load(null, null);
            }
        } catch (KeyStoreException | NoSuchProviderException | IOException | CertificateException | NoSuchAlgorithmException e) {
            log.log(Level.SEVERE, "Cannot retreive KeyStore", e);
            ks = null;
        }
        return ks;
    }

    private static void SaveKeystore(Context ctx, KeyStore ks) {
        try {
            File f = new File(PathUtils.GetGPGKeyFile(ctx));
            if (!f.exists() && !f.createNewFile())
                throw new FileNotFoundException(f.getName());
            ks.store(new FileOutputStream(f), KEY_PASS.toCharArray());
        }
        catch (IOException | CertificateException | NoSuchAlgorithmException | KeyStoreException e) {
            log.log(Level.SEVERE, "Cannot save keystore", e);
        }
    }

    public static boolean HasGPGKey(Context ctx) {
        try {
            KeyStore ks = GetKeystore(ctx, true);
            return ks != null && ks.containsAlias(KEY_NAME);
        } catch (KeyStoreException e) {
            log.log(Level.SEVERE, "KeyStore Exception: " +e.getMessage(), e);
            return false;
        }
    }

    public static boolean SetGPGKeyContent(Context ctx, InputStream content) {
        KeyStore ks = GetKeystore(ctx, false);
        if (ks == null)
            return false;
        try {
            SecretKey secret = FindSecretKey(FileUtils.ReadAllStream(content));
            KeyStore.Entry e = new KeyStore.SecretKeyEntry(secret);
            ks.setEntry(KEY_NAME, e, new KeyStore.PasswordProtection(null));
            SaveKeystore(ctx, ks);
        } catch (KeyStoreException | IOException e) {
            log.log(Level.SEVERE, "Cannot set GPG key content", e);
            return false;
        }
        return true;
    }

    private static @NonNull InputStream GetGPGKeyContent(Context ctx) throws FileNotFoundException {
        if (!HasGPGKey(ctx))
            throw new FileNotFoundException("GPG key");
        KeyStore ks = GetKeystore(ctx, true);
        if (ks == null)
            throw new FileNotFoundException("GPG key store");
        try {
            return new ByteArrayInputStream(ks.getKey(KEY_NAME, null).getEncoded());
        } catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) {
            log.log(Level.SEVERE, "KeyStore Exception: " +e.getMessage(), e);
            throw new FileNotFoundException("GPG key store");
        }
    }

    public static void ExportGPGKeyContent(Context ctx, OutputStream out) throws IOException {
        FileUtils.pipe(GetGPGKeyContent(ctx), out);
    }

    public static @NonNull PGPSecretKeyRing GetGPGKeyRing(Context ctx) throws IOException {
        final PGPSecretKeyRingCollection pgpSec;

        try {
            pgpSec = new PGPSecretKeyRingCollection(PGPUtil.getDecoderStream(GetGPGKeyContent(ctx)), new JcaKeyFingerprintCalculator());
        } catch (PGPException e) {
            throw new GPGUtil.MalformedKeyException(e);
        }
        Iterator<PGPSecretKeyRing> rIt = pgpSec.getKeyRings();
        if (rIt.hasNext())
            return rIt.next();
        throw new NoSuchElementException("GPG key not found");
    }

    public static @NonNull PGPSecretKey GetGPGSecretKey(Context ctx) throws IOException {
        return GetGPGKeyRing(ctx).getSecretKey();
    }

    public static void RemoveGpgKey(Context ctx) {
        File f = new File(PathUtils.GetGPGKeyFile(ctx));
        if (f.exists() && !f.delete()) {
            log.severe("Cannot remove GPG file");
        }
    }
}
