package cz.tomasdvorak.eet.client.security;

import cz.tomasdvorak.eet.client.exceptions.DataSigningException;
import cz.tomasdvorak.eet.client.exceptions.InvalidKeystoreException;
import cz.tomasdvorak.eet.client.utils.CertExpirationChecker;
import cz.tomasdvorak.eet.client.utils.CertificateUtils;
import cz.tomasdvorak.eet.client.utils.IOUtils;
import org.apache.wss4j.common.crypto.Crypto;
import org.apache.wss4j.common.crypto.Merlin;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.Enumeration;
import java.util.concurrent.TimeUnit;

/**
 * Representation of the client private key and public certificate pair. The public certificate is attached to every
 * signed request, signature is computed from the private key.
 */
public class ClientKey {

    private static final Logger logger = LoggerFactory.getLogger(ClientKey.class);

    private final KeyStore keyStore;
    private final String password;
    private final String alias;
    private final ClientPasswordCallback clientPasswordCallback;

    /**
     * Create new ClientKey instance based on data provided in the stream together with the password
     * @deprecated use
     * @param inputStream expects a stream to the pk12 keystore with one pair of key/cert. Will be closed automatically
     */
    public ClientKey(final InputStream inputStream, final String password) throws InvalidKeystoreException {

        if(inputStream == null) {
            throw new InvalidKeystoreException("Input stream of ClientKey cannot be NULL");
        }

        JavaCryptographyExtension.validateInstallation();

        this.password = password;
        String tempAlias = null;
        final KeyStore keystore = getKeyStore(inputStream, password);
        final Enumeration<String> aliases = getAliases(keystore);
        while (aliases.hasMoreElements()) {
            final String alias = aliases.nextElement();
            try {
				if (keystore.isKeyEntry(alias)) {
					tempAlias = alias;
                    String certificateInfo = CertificateUtils.getCertificateInfo(keystore, alias);
                    logger.info(certificateInfo);
                    CertExpirationChecker.of(keystore, alias)
                            .whenExpiresIn(30, TimeUnit.DAYS)
                            .printWarningTo(logger);
                }
			} catch (final KeyStoreException e) {
				logger.error(String.format("cannot check isKeyEntry(%s) - %s : %s", alias, e.getClass().getName(), e.getMessage()));
			}
        }
        if (tempAlias == null) {
            throw new InvalidKeystoreException("Keystore doesn't contain any keys!");
        }
        this.alias = tempAlias;
        this.keyStore = keystore;
        this.clientPasswordCallback = new ClientPasswordCallback(alias, password);
    }

    /**
     * @since 3.0
     */
    public static ClientKey fromInputStream(final InputStream inputStream, final String password) throws InvalidKeystoreException {
        return new ClientKey(inputStream, password);
    }

    /**
     * @since 3.0
     */
    public static ClientKey fromFile(final String filePath, final String password) throws InvalidKeystoreException {
        try {
            return new ClientKey(new FileInputStream(filePath), password);
        } catch (FileNotFoundException e) {
            throw new InvalidKeystoreException(e);
        }
    }

    private Enumeration<String> getAliases(final KeyStore keystore) throws InvalidKeystoreException {
        try {
            return keystore.aliases();
        } catch (final KeyStoreException e) {
            throw new InvalidKeystoreException(e);
        }
    }

    private KeyStore getKeyStore(final InputStream inputStream, final String password) throws InvalidKeystoreException {
        try {
            final KeyStore keystore = KeyStore.getInstance("pkcs12", new BouncyCastleProvider());
            keystore.load(inputStream, password.toCharArray());
            inputStream.close();
            return keystore;
        } catch (final CertificateException e) {
            throw new InvalidKeystoreException(e);
        } catch (final NoSuchAlgorithmException e) {
            throw new InvalidKeystoreException(e);
        } catch (final KeyStoreException e) {
            throw new InvalidKeystoreException(e);
        } catch (final IOException e) {
            throw new InvalidKeystoreException(e);
        } finally {
            IOUtils.closeQuietly(inputStream);
        }
    }

    /**
     * Sign provided text with SHA256withRSA initialized by the private key
     */
    public byte[] sign(final String text) throws DataSigningException {
        try {
            final Signature signature = Signature.getInstance("SHA256withRSA");
            signature.initSign(getPrivateKey());
            signature.update(text.getBytes("UTF-8"));
            return signature.sign();
        } catch (final NoSuchAlgorithmException e) {
            throw new DataSigningException(e);
        } catch (final UnrecoverableKeyException e) {
            throw new DataSigningException(e);
        } catch (final InvalidKeyException e) {
            throw new DataSigningException(e);
        } catch (final SignatureException e) {
            throw new DataSigningException(e);
        } catch (final UnsupportedEncodingException e) {
            throw new DataSigningException(e);
        } catch (final KeyStoreException e) {
            throw new DataSigningException(e);
        }
    }

    private PrivateKey getPrivateKey() throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException {
        return (PrivateKey) this.keyStore.getKey(this.alias, this.password.toCharArray());
    }

    /**
     * Crypto implementation used to sign WS requests
     */
    public Crypto getCrypto() {
        final Merlin merlin = new Merlin();
        merlin.setKeyStore(this.keyStore);
        return merlin;
    }

    /**
     * Get the first (and hopefully the only one) alias included in the keystore bundle
     */
    public String getAlias() {
        return alias;
    }

    /**
     * Callback supplying username / password  combination to the WS signing layer
     */
    public ClientPasswordCallback getClientPasswordCallback() {
        return clientPasswordCallback;
    }
}