package nl.hsac.fitnesse.fixture.util;

import org.apache.commons.lang3.StringUtils;
import org.apache.http.ConnectionReuseStrategy;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.NoConnectionReuseStrategy;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.client.SystemDefaultCredentialsProvider;
import org.apache.http.impl.client.WinHttpClients;
import org.apache.http.ssl.PrivateKeyStrategy;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.ssl.TrustStrategy;

import javax.net.ssl.SSLContext;
import java.io.File;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Arrays;

/**
 * Helper to create Apache http client.
 */
public class HttpClientFactory {
    private HttpClientBuilder clientBuilder;
    private String userAgent;
    private ConnectionReuseStrategy connectionReuseStrategy;
    private RequestConfig.Builder requestConfigBuilder;
    private CredentialsProvider credentialsProvider;

    private HttpHost proxy;

    private boolean enableContentCompression = false;
    private boolean disableSslVerification = false;

    private File trustStoreFile;
    private char[] trustStorePassword;
    private TrustStrategy trustStrategy;

    private File keyStoreFile;
    private char[] keyStorePassword;
    private char[] keyPassword;
    private PrivateKeyStrategy keyStrategy;

    private boolean useWindowsAuthenticationSettings = false;

    public HttpClientFactory() {
        userAgent = nl.hsac.fitnesse.fixture.util.HttpClient.class.getName();
        requestConfigBuilder = createRequestConfigBuilder();
        credentialsProvider = createCredentialsProvider();
        connectionReuseStrategy = createConnectionReuseStrategy();
        clientBuilder = createClientBuilder();
    }

    /**
     * Creates a client using the current settings.
     *
     * @return apache http client.
     */
    public HttpClient createClient() {

        if (useWindowsAuthenticationSettings) {
            clientBuilder = WinHttpClients.custom();
        } else {
            clientBuilder.setDefaultCredentialsProvider(credentialsProvider);
        }
        if (isSslVerificationDisabled()) {
            disableSSLVerification();
        }
        if (isSSLContextRequired()) {
            SSLContext sslContext = generateSSLContext();
            clientBuilder.setSSLContext(sslContext);
        }
        if (!isContentCompressionEnabled()) {
            clientBuilder.disableContentCompression();
        }
        clientBuilder.setUserAgent(userAgent);
        clientBuilder.setConnectionReuseStrategy(connectionReuseStrategy);
        clientBuilder.setDefaultRequestConfig(requestConfigBuilder.build());

        return buildClient();
    }

    /**
     * Disables SSL certificate verification.
     */
    public void disableSSLVerification() {
        try {
            clientBuilder.setSSLSocketFactory(createAllTrustingSSLConnectionSocketFactory());
        } catch (Exception e) {
            throw new RuntimeException("Unable to create all-trusting SSLConnectionSocketFactory", e);
        }
    }

    /**
     * Resets SSL trust store to default.
     */
    public void clearTrustStore() {
        this.trustStoreFile = null;
        this.trustStrategy = null;
        // erase current password value from memory
        Arrays.fill(this.trustStorePassword, '0');
        this.trustStorePassword = null;
    }

    /**
     * No longer send client certificate.
     */
    public void clearClientCertificate() {
        keyStoreFile = null;
        keyStrategy = null;

        // erase current password values from memory
        Arrays.fill(this.keyStorePassword, '0');
        this.keyStorePassword = null;
        Arrays.fill(this.keyPassword, '0');
        this.keyPassword = null;
    }

    protected SSLContext generateSSLContext() {
        SSLContextBuilder contextBuilder = SSLContexts.custom();
        try {
            if (getTrustStoreFile() != null) {
                contextBuilder.loadTrustMaterial(getTrustStoreFile(), getTrustStorePassword(), getTrustStrategy());
            }

            if (getKeyStoreFile() != null) {
                contextBuilder.loadKeyMaterial(getKeyStoreFile(), getKeyStorePassword(), getKeyPassword(), getPrivateKeyStrategy());
            }

            return contextBuilder.build();
        } catch (GeneralSecurityException | IOException e) {
            throw new RuntimeException("Unable to configure SSL", e);
        }
    }

    protected HttpClient buildClient() {
        return clientBuilder.build();
    }

    public void setProxy(String proxyString) {
        proxy = StringUtils.isBlank(proxyString) ? null : HttpHost.create(proxyString);
        getRequestConfigBuilder().setProxy(proxy);
    }

    public void setProxyUsernameAndPassword(String username, String password) {
        if (proxy == null) {
            throw new IllegalStateException("No proxy set, please configure that before setting credentials");
        }
        AuthScope proxyAuthScope = new AuthScope(proxy);
        Credentials proxyCredentials = new UsernamePasswordCredentials(username, password);
        setCredentials(proxyAuthScope, proxyCredentials);
    }

    public void useWindowsAuthentication(boolean useWindowsAuth) {
        this.useWindowsAuthenticationSettings = useWindowsAuth;
    }

    public void configureBasicAuthentication(String username, String password) {
        setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(username, password));
    }

    public void setCredentials(AuthScope scope, Credentials credentials) {
        getCredentialsProvider().setCredentials(scope, credentials);
    }

    public void setConnectionTimeout(int timeoutInMs) {
        getRequestConfigBuilder().setConnectTimeout(timeoutInMs);
    }

    public int getConnectionTimeout() {
        return getRequestConfigBuilder().build().getConnectTimeout();
    }

    public void setSocketTimeout(int timeoutInMs) {
        getRequestConfigBuilder().setSocketTimeout(timeoutInMs);
    }

    public int getSocketTimeout() {
        return getRequestConfigBuilder().build().getSocketTimeout();
    }

    public boolean isContentCompressionEnabled() {
        return enableContentCompression;
    }

    public void setContentCompression(boolean contentCompression) {
        this.enableContentCompression = contentCompression;
    }

    public boolean isSslVerificationDisabled() {
        return disableSslVerification;
    }

    public void setDisableSslVerification(boolean disableSslVerification) {
        this.disableSslVerification = disableSslVerification;
        clientBuilder.setSSLSocketFactory(null);
    }

    protected boolean isSSLContextRequired() {
        return keyStoreFile != null || trustStoreFile != null;
    }

    public File getTrustStoreFile() {
        return trustStoreFile;
    }

    public void setTrustStoreFile(File trustStoreFile) {
        this.trustStoreFile = trustStoreFile;
    }

    public char[] getTrustStorePassword() {
        return trustStorePassword;
    }

    public void setTrustStorePassword(char[] trustStorePassword) {
        this.trustStorePassword = trustStorePassword;
    }

    public TrustStrategy getTrustStrategy() {
        return trustStrategy;
    }

    public void setTrustStrategy(TrustStrategy trustStrategy) {
        this.trustStrategy = trustStrategy;
    }

    public File getKeyStoreFile() {
        return keyStoreFile;
    }

    public void setKeyStoreFile(File keyStoreFile) {
        this.keyStoreFile = keyStoreFile;
    }

    public char[] getKeyStorePassword() {
        return keyStorePassword;
    }

    public void setKeyStorePassword(char[] keyStorePassword) {
        this.keyStorePassword = keyStorePassword;
    }

    public char[] getKeyPassword() {
        return keyPassword;
    }

    public void setKeyPassword(char[] keyPassword) {
        this.keyPassword = keyPassword;
    }

    public PrivateKeyStrategy getPrivateKeyStrategy() {
        return keyStrategy;
    }

    public void setPrivateKeyStrategy(PrivateKeyStrategy keyStrategy) {
        this.keyStrategy = keyStrategy;
    }

    public HttpClientBuilder getClientBuilder() {
        return clientBuilder;
    }

    public void setClientBuilder(HttpClientBuilder builder) {
        clientBuilder = builder;
    }

    public ConnectionReuseStrategy getConnectionReuseStrategy() {
        return connectionReuseStrategy;
    }

    public void setConnectionReuseStrategy(ConnectionReuseStrategy connectionReuseStrategy) {
        this.connectionReuseStrategy = connectionReuseStrategy;
    }

    public String getUserAgent() {
        return userAgent;
    }

    public void setUserAgent(String userAgent) {
        this.userAgent = userAgent;
    }

    public CredentialsProvider getCredentialsProvider() {
        return credentialsProvider;
    }

    public void setCredentialsProvider(CredentialsProvider credentialsProvider) {
        this.credentialsProvider = credentialsProvider;
    }

    public RequestConfig.Builder getRequestConfigBuilder() {
        return requestConfigBuilder;
    }

    public void setRequestConfigBuilder(RequestConfig.Builder requestConfigBuilder) {
        this.requestConfigBuilder = requestConfigBuilder;
    }

    protected HttpClientBuilder createClientBuilder() {
        return HttpClients.custom()
                .useSystemProperties();
    }

    protected ConnectionReuseStrategy createConnectionReuseStrategy() {
        return NoConnectionReuseStrategy.INSTANCE;
    }

    protected SystemDefaultCredentialsProvider createCredentialsProvider() {
        return new SystemDefaultCredentialsProvider();
    }

    protected RequestConfig.Builder createRequestConfigBuilder() {
        return RequestConfig.custom()
                .setCookieSpec(CookieSpecs.STANDARD);
    }

    protected SSLConnectionSocketFactory createAllTrustingSSLConnectionSocketFactory() throws Exception {
        SSLContext allTrustingSSLContext = SSLContexts.custom()
                .loadTrustMaterial(null, (a, b) -> true)
                .build();
        return new SSLConnectionSocketFactory(allTrustingSSLContext, (a, b) -> true);
    }
}