/*
 * Copyright 2018 Scytl Secure Electronic Voting SA
 *
 * All rights reserved
 *
 * See our extended copyright notice in *file 'Copyright.txt' which is part of this source code package
 */
package com.scytl.products.ov.commons.infrastructure.remote.client;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;

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

import com.scytl.products.ov.commons.infrastructure.config.InfrastructureConfig;
import com.scytl.products.ov.commons.infrastructure.exception.OvCommonsInfrastructureException;
import com.scytl.products.ov.keystore.KeystorePasswords;
import com.scytl.products.ov.keystore.KeystorePasswordsReader;
import com.scytl.products.ov.keystore.KeystorePasswordsReaderFactory;
import com.scytl.products.ov.keystore.KeystoreReader;
import com.scytl.products.ov.keystore.KeystoreReaderFactory;

import okhttp3.ConnectionSpec;
import okhttp3.OkHttpClient;
import okhttp3.OkHttpClient.Builder;
import okhttp3.logging.HttpLoggingInterceptor;
import okhttp3.logging.HttpLoggingInterceptor.Level;
import retrofit2.Converter.Factory;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory;
import retrofit2.converter.gson.GsonConverterFactory;
import retrofit2.converter.jackson.JacksonConverterFactory;

/**
 * Singleton class to centralize the creation of Pooled Rest client. This class is prepared for SSL calls.
 */
public class RestClientConnectionManager {

    private static final Logger LOG = LoggerFactory.getLogger(RestClientConnectionManager.class);

    private static RestClientConnectionManager instance = null;

    private static InfrastructureConfig infrastructureConfig;

    private static KeyStore keyStore = null;

    private static SSLContext sslContext = null;

    private static OkHttpClient okHttpClient = null;

    private static OkHttpClient okHttpClientWithInterceptor = null;

    private static final KeystorePasswordsReaderFactory KEYSTORE_PASSWORDS_READER_FACTORY =
        new KeystorePasswordsReaderFactory();

    private static final KeystoreReader KEYSTORE_READER = new KeystoreReaderFactory().getInstance();

    /**
     * Make private constructor. Singleton class.
     */
    private RestClientConnectionManager() {
    }

    /**
     * Returns a singleton instance of this class.
     *
     * @return
     */
    public static RestClientConnectionManager getInstance() throws OvCommonsInfrastructureException {
        if (instance == null) {

            infrastructureConfig = InfrastructureConfig.getInstance();

            instance = new RestClientConnectionManager();

            // Check if SSL is activated. If yes then configure it.
            if (infrastructureConfig.isSslActivated()) {
                try {

                    configureSSL();

				} catch (KeyStoreException | IOException | CertificateException | NoSuchAlgorithmException
						| KeyManagementException | UnrecoverableKeyException e) {
					LOG.error(e.getMessage());
					throw new OvCommonsInfrastructureException(e.getMessage(), e);
				}
            }

            LOG.debug("New RestClientConnectionManager has been created.");
        }
        return instance;
    }

    /**
     * Returns a HTTP Rest Client initialized with a base URL, which is ready to create a new Retrofit Service.
     * Depending on configured properties and url it will use SSL. It uses Retrofit connection pooling so the same
     * client will be used for every call.
     *
     * @param url
     *            - the base url of the endpoint.
     * @return a HTTP Rest Client for making rest calls to a server.
     */
    public Retrofit getRestClient(String url) {
        return createRestClient(url, false, okHttpClient, null, null, false);
    }
    public Retrofit getRestClientWithJacksonConverter(String url) {
        return createRestClient(url, false, okHttpClient, null, null, true);
    }

    /**
     * Returns a HTTP Rest Client initialized with a base URL, which is ready to create a new Retrofit Service.
     * Depending on configured properties and url it will use SSL. It uses Retrofit connection pooling so the same
     * client will be used for every call. All requests will be intercepted in order to be signed. It uses a default
     * GSON converter. <b>Should not be used with data streams, as it is inefficient to read the content multiple times. 
     * Currently does not support regular input streams as it does not reset the stream automatically</b>.
     *
     * @param url
     *            - the base url of the endpoint.
     * @param privateKey
     *            - the private key to sign request.
     * @param nodeIdentifier
     *            - Identified request originator.
     * @return a HTTP Rest Client for making rest calls to a server.
     */
    public Retrofit getRestClientWithInterceptor(final String url, final PrivateKey privateKey,
            NodeIdentifier nodeIdentifier) {
        return createRestClient(url, true, okHttpClientWithInterceptor, privateKey, nodeIdentifier, false);
    }

    /**
     * Returns a HTTP Rest Client initialized with a base URL, which is ready to create a new Retrofit Service.
     * Depending on configured properties and url it will use SSL. It uses Retrofit connection pooling so the same
     * client will be used for every call. All requests will be intercepted in order to be signed. It uses a Jackson
     * converter instead of the GSON default converter.<b>Should not be used with data streams, as it is inefficient to read the content multiple times. 
     * Currently does not support regular input streams as it does not reset the stream automatically</b>
     *
     * @param url
     *            - the base url of the endpoint.
     * @param privateKey
     *            - the private key to sign request.
     * @param nodeIdentifier
     *            - Identified request originator.
     * @return a HTTP Rest Client for making rest calls to a server.
     */
    public Retrofit getRestClientWithInterceptorAndJacksonConverter(final String url, final PrivateKey privateKey,
            final NodeIdentifier nodeIdentifier) {
        return createRestClient(url, true, okHttpClientWithInterceptor, privateKey, nodeIdentifier, true);
    }

    /**
     * New factory method to obtain a configuration client with interceptor, whose signing private key is retrieved by
     * the KeystoreReader inside the method rather than provided as a parameter to the method.
     * 
     * @param url
     * @param nodeIdentifier
     * @return
     */
    public Retrofit getConfigurationRestClientWithInterceptor(final String url,
            final NodeIdentifier nodeIdentifier) {
        final KeystorePasswordsReader keystorePasswordsReader =
            KEYSTORE_PASSWORDS_READER_FACTORY.getInstance(nodeIdentifier);
        final KeystorePasswords keystorePasswords = keystorePasswordsReader.read();
        final PrivateKey privateKey = KEYSTORE_READER.readSigningPrivateKey(nodeIdentifier, keystorePasswords);
        keystorePasswords.destroy();
        return this.getRestClientWithInterceptor(url, privateKey, nodeIdentifier);
    }

    private Retrofit createRestClient(String url, boolean withInterceptor, OkHttpClient client,
            PrivateKey privateKey, NodeIdentifier nodeIdentifier, final boolean useJacksonConverter) {

        // Check if the client is already created, if not, then initialize it.
		OkHttpClient restClient = client;

		if (restClient == null) {
			LOG.debug("A new RestClient has been created.");
			restClient = new OkHttpClient();

			if (withInterceptor) {
				LOG.debug("A signature will be included in all requests header.");
				restClient = new OkHttpClient.Builder()
						.addInterceptor(new RestClientInterceptor(privateKey, nodeIdentifier)).build();
			}
		}

        // Check if SSL is activated or not, returning an appropriate Retrofit Rest Client.
        if (infrastructureConfig.isSslActivated() && url.toLowerCase().contains("https")) {

			// Returns a client with SLL.
			Builder newBuilder = restClient.newBuilder();
			newBuilder.socketFactory(sslContext.getSocketFactory());
			newBuilder.connectionSpecs(Arrays.asList(ConnectionSpec.MODERN_TLS));
			Retrofit restAdapter = createRetrofitClient(url, newBuilder, useJacksonConverter);

			LOG.debug("A SSL RestClient has been served.");
			return restAdapter;

        } else {

			LOG.debug("A non SSL RestClient has been served.");
			// Returns a client without SLL.
			Builder newBuilder = restClient.newBuilder();

			// Returns a client without SLL.
			Retrofit restAdapter = createRetrofitClient(url, newBuilder, useJacksonConverter);

			LOG.debug("A non SSL RestClient has been served.");
			return restAdapter;
        }
    }
    
	private Retrofit createRetrofitClient(String url, Builder newBuilder, boolean useJacksonConverter) {
        HttpLoggingInterceptor logging = new HttpLoggingInterceptor();  
	    logging.setLevel(Level.NONE);
		newBuilder.addInterceptor(logging); 
		newBuilder.connectTimeout(infrastructureConfig.getSystemConnectionTimeOut(), TimeUnit.SECONDS);
		newBuilder.readTimeout(infrastructureConfig.getSystemReadTimeOut(), TimeUnit.SECONDS);
		newBuilder.writeTimeout(infrastructureConfig.getSystemWriteTimeOut(), TimeUnit.SECONDS);
		String validUrl = url;
		String retrofit2CompatibleUrlEnding = "/";
        if(!url.endsWith(retrofit2CompatibleUrlEnding)){
		    validUrl = url.concat(retrofit2CompatibleUrlEnding);
		}
		Factory factory;
		if(useJacksonConverter){
			factory = JacksonConverterFactory.create();
		} else {
			factory = GsonConverterFactory.create();
		}

		return new Retrofit.Builder().addCallAdapterFactory(RxJavaCallAdapterFactory.create())
				.addConverterFactory(factory).baseUrl(validUrl).client(newBuilder.build()).build();
	}


    /**
     * Creates needed KeyStore and SSLContext for SLL calls.
     */
    private static void configureSSL()
            throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException,
            IOException, CertificateException {

        keyStore = readKeyStoreFromFile();
        sslContext = createSSLContextUsingKeystore(keyStore);
    }

    /**
     * Reads filesystem to load KeyStore file based on configuration properties.
     * 
     * @return
     * @throws KeyStoreException
     * @throws IOException
     * @throws CertificateException
     * @throws NoSuchAlgorithmException
     */
    private static KeyStore readKeyStoreFromFile()
            throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException {

        KeyStore keyStore = KeyStore.getInstance(infrastructureConfig.getRestKeystoreType());


        try (InputStream fis = getKeyStoreInputStream()){
            keyStore.load(fis, infrastructureConfig.getRestKeystorePassword().toCharArray());
        }
        return keyStore;
    }

    private static InputStream getKeyStoreInputStream() throws FileNotFoundException {
        InputStream fis = RestClientConnectionManager.class.getResourceAsStream(infrastructureConfig.getRestKeystoreFilepath());
        if (fis == null) {
            fis = new FileInputStream(infrastructureConfig.getRestKeystoreFilepath());
        }
        return fis;
    }

    /**
     * Creates SSLContext based on configuration properties and a specific KeyStore.
     * 
     * @param keyStore
     * @return
     * @throws KeyManagementException
     * @throws NoSuchAlgorithmException
     * @throws KeyStoreException
     * @throws UnrecoverableKeyException
     * @throws IOException
     * @throws CertificateException
     */
    private static SSLContext createSSLContextUsingKeystore(KeyStore keyStore)
            throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException,
            IOException, CertificateException {

        TrustManagerFactory trustManagerFactory =
            TrustManagerFactory.getInstance(infrastructureConfig.getRestKeytrustAlgorithm());
        trustManagerFactory.init(keyStore);

        KeyManagerFactory keyManagerFactory =
            KeyManagerFactory.getInstance(infrastructureConfig.getRestKeytrustAlgorithm());
        keyManagerFactory.init(keyStore, infrastructureConfig.getRestKeystorePassword().toCharArray());

        SSLContext sslContext = SSLContext.getInstance(infrastructureConfig.getRestSslProtocol());
        sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom());

        return sslContext;
    }

}
