/*
 *     Copyright (C) 2015 ASIP Santé
 */
package fr.asipsante.mss.connectkit.connection;

import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.HashSet;
import java.util.Set;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509KeyManager;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Classe mère des factories qui configurent les connexions HTTPS pour
 * différents protocoles d'authentification.
 * 
 * @author ASIP Santé
 * 
 */
public abstract class HttpsConnectionFactory {

    private final class KeyManagerSpi implements X509KeyManager {
        private X509KeyManager spi;

        KeyManagerSpi(X509KeyManager delegate) {
            this.spi = delegate;
        }

        @Override
        public String chooseClientAlias(String[] arg0, Principal[] arg1,
                Socket arg2) {
            String clientAlias = spi.chooseClientAlias(arg0, arg1, arg2);
            LOGGER.debug(spi.getClass() + " called chooseClientAlias("
                    + toArrayString(arg0) + "," + toArrayString(arg1)
                    + ") and returning " + clientAlias);
            return clientAlias;
        }

        @Override
        public String chooseServerAlias(String arg0, Principal[] arg1,
                Socket arg2) {
            String serverAlias = spi.chooseServerAlias(arg0, arg1, arg2);
            LOGGER.debug(spi.getClass() + " called chooseServerAlias(" + arg0
                    + "," + toArrayString(arg1) + "," + arg2
                    + ") and returning " + serverAlias);
            return serverAlias;
        }

        @Override
        public X509Certificate[] getCertificateChain(String arg0) {
            X509Certificate[] certChain = spi.getCertificateChain(arg0);
            LOGGER.debug(spi.getClass() + " called getCertificateChain(" + arg0
                    + ") and returning " + toArrayString(certChain));
            return certChain;
        }

        @Override
        public String[] getClientAliases(String arg0, Principal[] arg1) {
            String[] clientAliases = spi.getClientAliases(arg0, arg1);
            LOGGER.debug(spi.getClass() + " called in getCertificateChain ("
                    + arg0 + "," + toArrayString(arg1) + ") and returning "
                    + toArrayString(clientAliases));
            return clientAliases;
        }

        private String toArrayString(Object[] clientAliases) {
            StringBuilder builder = new StringBuilder("[");
            for (Object o : clientAliases) {
                builder.append(o.toString()).append(",");
            }
            if (clientAliases.length > 0) {
                builder.setLength(builder.length() - 1);
            }
            builder.append("]");
            return builder.toString();
        }

        @Override
        public PrivateKey getPrivateKey(String arg0) {
            PrivateKey key = spi.getPrivateKey(arg0);
            LOGGER.debug(spi.getClass() + " called in getPrivateKey(" + arg0
                    + ") and returning " + key.getClass().getName() + " " + key);
            return key;
        }

        @Override
        public String[] getServerAliases(String arg0, Principal[] arg1) {
            String[] serverAliases = spi.getServerAliases(arg0, arg1);
            LOGGER.debug(spi.getClass()
                    + " called in getServerAliases and returning "
                    + toArrayString(serverAliases));
            return serverAliases;
        }
    }

    private static final Logger LOGGER = LoggerFactory
            .getLogger(HttpsConnectionFactory.class);

    private URL truststoreURL;

    private Set<RequestProperty> requestHeaders = new HashSet<RequestProperty>();

    private String trustpasswd;

    /**
     * Constructeur avec initialisation complète.
     * 
     * @param requestHeaders
     *            headers HTTP à positionner sur toutes les connexions.
     * @param truststoreURL
     *            URL du fichier JKS contenant les certificats d'émetteurs
     *            auxquels il faut faire confiance.
     * @param trustpasswd
     *            mot de passe du truststore.
     */
    public HttpsConnectionFactory(Set<RequestProperty> requestHeaders,
            URL truststoreURL, String trustpasswd) {
        this.requestHeaders.addAll(requestHeaders);
        this.truststoreURL = truststoreURL;
        LOGGER.debug("URL truststore: " + truststoreURL.toExternalForm());
        this.trustpasswd = trustpasswd;
    }

    public SSLSocketFactory getSSLSocketFactory() throws SSLSecurityException {
        SSLSocketFactory socketFactory;
        try {
            TrustManagerFactory tmf = getTrustManagerFactory();
            KeyManagerFactory kmf = getKeyManagerFactory();
            SSLContext context = SSLContext.getInstance("TLSv1");
            KeyManager[] keyManagers = getKeyManagerTable(kmf);
            context.init(keyManagers, tmf.getTrustManagers(), null);
            socketFactory = context.getSocketFactory();

            if (LOGGER.isTraceEnabled()) {
                for (String supported : context.getSocketFactory()
                        .getSupportedCipherSuites()) {
                    LOGGER.trace("Supported cipher: " + supported);
                }
            }
        } catch (IOException e) {
            throw new SSLSecurityException(
                    "Echec instanciation SSLSocketFactory", e);
        } catch (KeyStoreException e) {
            throw new SSLSecurityException(
                    "Echec instanciation SSLSocketFactory", e);
        } catch (NoSuchAlgorithmException e) {
            throw new SSLSecurityException(
                    "Echec instanciation SSLSocketFactory", e);
        } catch (CertificateException e) {
            throw new SSLSecurityException(
                    "Echec instanciation SSLSocketFactory", e);
        } catch (KeyManagementException e) {
            throw new SSLSecurityException(
                    "Echec instanciation SSLSocketFactory", e);
        }
        return socketFactory;
    }

    /**
     * Permet d'ajouter des headers à mettre en place sur toutes les connexions
     * créées.
     * 
     * @param headers
     */
    public void addHttpHeaders(Set<RequestProperty> headers) {
        requestHeaders.addAll(headers);
    }

    /**
     * Crée une connexion (encore non active).
     * 
     * @param hostname
     *            nom d'hôte à contacter.
     * @param port
     *            port de destination de la connexion.
     * @param servicePath
     *            path de la ressource à laquelle se connecter.
     * @param protocolSpec
     *            version de protocole SSL à utiliser.
     * @return
     * @throws SSLSecurityException
     * @throws IOException
     */
    public HttpsURLConnection createHttpsConnection(String hostname, int port,
            String servicePath, String protocolSpec)
            throws SSLSecurityException, IOException {
        // param�trage HTTP

        int portEffectif = port == -1 ? 443 : port;
        URL url = new URL("HTTPS", hostname, portEffectif, servicePath);
        return setConnectionUp(protocolSpec, url);
    }

    /**
     * Crée une connexion (encore non active).
     * 
     * @param url
     *            URL de destination.
     * @param protocolSpec
     *            version de SSL à utiliser.
     * @return
     * @throws SSLSecurityException
     * @throws IOException
     */
    public HttpsURLConnection createHttpsConnection(URL url, String protocolSpec)
            throws IOException, SSLSecurityException {
        return setConnectionUp(protocolSpec, url);
    }

    private HttpsURLConnection setConnectionUp(String protocolSpec, URL url)
            throws IOException, SSLSecurityException {
        HttpsURLConnection connection = (HttpsURLConnection) url
                .openConnection();
        connection.setRequestMethod("POST");
        connection.setDoInput(true);
        connection.setDoOutput(true);
        connection.setInstanceFollowRedirects(false);
        for (RequestProperty header : requestHeaders) {
            setRequestProperty(connection, header);
        }

        // configuration truststore & keystore (certificats logiciels)
        TrustManagerFactory tmf;
        try {
            tmf = getTrustManagerFactory();
            KeyManagerFactory kmf = getKeyManagerFactory();
            SSLContext context = SSLContext.getInstance(protocolSpec);
            KeyManager[] keyManagers = getKeyManagerTable(kmf);
            context.init(keyManagers, tmf.getTrustManagers(), null);
            connection.setSSLSocketFactory(context.getSocketFactory());

            if (LOGGER.isTraceEnabled()) {
                for (String supported : context.getSocketFactory()
                        .getSupportedCipherSuites()) {
                    LOGGER.trace("Supported cipher: " + supported);
                }
            }
        } catch (KeyStoreException e) {
            throw new SSLSecurityException(
                    "Echec configuration SSL de la connection", e);
        } catch (NoSuchAlgorithmException e) {
            throw new SSLSecurityException(
                    "Echec configuration SSL de la connection", e);
        } catch (CertificateException e) {
            throw new SSLSecurityException(
                    "Echec configuration SSL de la connection", e);
        } catch (KeyManagementException e) {
            throw new SSLSecurityException(
                    "Echec configuration SSL de la connection", e);
        }

        return connection;
    }

    private KeyManager[] getKeyManagerTable(KeyManagerFactory kmf) {
        KeyManager[] table;
        if (kmf == null) {
            table = null;
        } else {
            KeyManager[] spiManagerSet = kmf.getKeyManagers();
            if (LOGGER.isDebugEnabled()) {
                table = new KeyManager[spiManagerSet.length];
                int index = 0;
                for (KeyManager manager : spiManagerSet) {
                    if (manager instanceof X509KeyManager) {
                        table[index++] = new KeyManagerSpi(
                                (X509KeyManager) manager);
                    } else {
                        table[index++] = manager;
                    }
                }
            } else {
                table = spiManagerSet;
            }
        }
        return table;
    }

    /**
     * Spécifié par les différents types de HttpsFactories afin de définir une
     * stratégie d'authentification client.
     * 
     * @return
     * @throws SSLSecurityException
     */
    protected abstract KeyManagerFactory getKeyManagerFactory()
            throws SSLSecurityException;

    private void setRequestProperty(HttpsURLConnection connection,
            RequestProperty property) {
        connection.setRequestProperty(property.getName(), property.getValue());
    }

    private TrustManagerFactory getTrustManagerFactory()
            throws KeyStoreException, IOException, NoSuchAlgorithmException,
            CertificateException {

        KeyStore trustStore = KeyStore.getInstance("jks");
        InputStream resourceAsStream = truststoreURL.openStream();
        trustStore.load(resourceAsStream, trustpasswd.toCharArray());
        TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX");
        tmf.init(trustStore);
        return tmf;
    }

}