package org.apereo.cas.util.http;

import com.google.common.base.Throwables;
import org.apache.http.ConnectionReuseStrategy;
import org.apache.http.Header;
import org.apache.http.HttpHost;
import org.apache.http.client.AuthenticationStrategy;
import org.apache.http.client.ConnectionBackoffStrategy;
import org.apache.http.client.CookieStore;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.RedirectStrategy;
import org.apache.http.client.ServiceUnavailableRetryStrategy;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.LayeredConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.DefaultHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.DefaultConnectionReuseStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultBackoffStrategy;
import org.apache.http.impl.client.DefaultRedirectStrategy;
import org.apache.http.impl.client.DefaultServiceUnavailableRetryStrategy;
import org.apache.http.impl.client.FutureRequestExecutionService;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.client.ProxyAuthenticationStrategy;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.FactoryBean;

import javax.annotation.PreDestroy;
import javax.net.ssl.HostnameVerifier;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

/**
 * The factory to build a {@link SimpleHttpClient}.
 *
 * @author Jerome Leleu
 * @since 4.1.0
 */
public class SimpleHttpClientFactoryBean implements FactoryBean<SimpleHttpClient> {

    /**
     * Max connections per route.
     */
    public static final int MAX_CONNECTIONS_PER_ROUTE = 50;

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

    private static final int MAX_POOLED_CONNECTIONS = 100;

    private static final int DEFAULT_THREADS_NUMBER = 200;

    private static final int DEFAULT_TIMEOUT = 5000;

    /**
     * The default status codes we accept.
     */
    private static final int[] DEFAULT_ACCEPTABLE_CODES = new int[]{
            HttpURLConnection.HTTP_OK, HttpURLConnection.HTTP_NOT_MODIFIED,
            HttpURLConnection.HTTP_MOVED_TEMP, HttpURLConnection.HTTP_MOVED_PERM,
            HttpURLConnection.HTTP_ACCEPTED, HttpURLConnection.HTTP_NO_CONTENT};

    /**
     * 20% of the total of threads in the pool to handle overhead.
     */
    private static final int DEFAULT_QUEUE_SIZE = (int) (DEFAULT_THREADS_NUMBER * 0.2);

    /**
     * The number of threads used to build the pool of threads (if no executorService provided).
     */
    private int threadsNumber = DEFAULT_THREADS_NUMBER;

    /**
     * The queue size to absorb additional tasks when the threads pool is saturated (if no executorService provided).
     */
    private int queueSize = DEFAULT_QUEUE_SIZE;

    /**
     * The Max pooled connections.
     */
    private int maxPooledConnections = MAX_POOLED_CONNECTIONS;

    /**
     * The Max connections per each route connections.
     */
    private int maxConnectionsPerRoute = MAX_CONNECTIONS_PER_ROUTE;

    /**
     * List of HTTP status codes considered valid by the caller.
     */
    private List<Integer> acceptableCodes = IntStream.of(DEFAULT_ACCEPTABLE_CODES).boxed().collect(Collectors.toList());

    private long connectionTimeout = DEFAULT_TIMEOUT;

    private int readTimeout = DEFAULT_TIMEOUT;

    private RedirectStrategy redirectionStrategy = new DefaultRedirectStrategy();

    /**
     * The socket factory to be used when verifying the validity of the endpoint.
     */
    private SSLConnectionSocketFactory sslSocketFactory = SSLConnectionSocketFactory.getSocketFactory();

    /**
     * The hostname verifier to be used when verifying the validity of the endpoint.
     */
    private HostnameVerifier hostnameVerifier = new DefaultHostnameVerifier();

    /**
     * The credentials provider for endpoints that require authentication.
     */
    private CredentialsProvider credentialsProvider;

    /**
     * The cookie store for authentication.
     */
    private CookieStore cookieStore;

    /**
     * Interface for deciding whether a connection can be re-used for subsequent requests and should be kept alive.
     **/
    private ConnectionReuseStrategy connectionReuseStrategy = new DefaultConnectionReuseStrategy();

    /**
     * When managing a dynamic number of connections for a given route, this strategy assesses whether a
     * given request execution outcome should result in a backoff
     * signal or not, based on either examining the Throwable that resulted or by examining
     * the resulting response (e.g. for its status code).
     */
    private ConnectionBackoffStrategy connectionBackoffStrategy = new DefaultBackoffStrategy();

    /**
     * Strategy interface that allows API users to plug in their own logic to control whether or not a retry
     * should automatically be done, how many times it should be retried and so on.
     */
    private ServiceUnavailableRetryStrategy serviceUnavailableRetryStrategy = new DefaultServiceUnavailableRetryStrategy();

    /**
     * Default headers to be sent.
     **/
    private Collection<? extends Header> defaultHeaders = Collections.emptyList();

    /**
     * Default strategy implementation for proxy host authentication.
     **/
    private AuthenticationStrategy proxyAuthenticationStrategy = new ProxyAuthenticationStrategy();

    /**
     * Determines whether circular redirects (redirects to the same location) should be allowed.
     **/
    private boolean circularRedirectsAllowed = true;

    /**
     * Determines whether authentication should be handled automatically.
     **/
    private boolean authenticationEnabled;

    /**
     * Determines whether redirects should be handled automatically.
     **/
    private boolean redirectsEnabled = true;

    /**
     * The executor service used to create a {@link #buildRequestExecutorService}.
     */
    private ExecutorService executorService;

    @Override
    public SimpleHttpClient getObject() throws Exception {

        final CloseableHttpClient httpClient = buildHttpClient();

        final FutureRequestExecutionService requestExecutorService = buildRequestExecutorService(httpClient);

        return new SimpleHttpClient(this.acceptableCodes, httpClient, requestExecutorService);
    }

    @Override
    public Class<?> getObjectType() {
        return SimpleHttpClient.class;
    }

    @Override
    public boolean isSingleton() {
        return false;
    }

    /**
     * Build a HTTP client based on the current properties.
     *
     * @return the built HTTP client
     */
    private CloseableHttpClient buildHttpClient() {
        try {

            final ConnectionSocketFactory plainsf = PlainConnectionSocketFactory.getSocketFactory();
            final LayeredConnectionSocketFactory sslsf = this.sslSocketFactory;

            final Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
                    .register("http", plainsf)
                    .register("https", sslsf)
                    .build();

            final PoolingHttpClientConnectionManager connMgmr = new PoolingHttpClientConnectionManager(registry);
            connMgmr.setMaxTotal(this.maxPooledConnections);
            connMgmr.setDefaultMaxPerRoute(this.maxConnectionsPerRoute);
            connMgmr.setValidateAfterInactivity(DEFAULT_TIMEOUT);

            final HttpHost httpHost = new HttpHost(InetAddress.getLocalHost());
            final HttpRoute httpRoute = new HttpRoute(httpHost);
            connMgmr.setMaxPerRoute(httpRoute, MAX_CONNECTIONS_PER_ROUTE);

            final RequestConfig requestConfig = RequestConfig.custom()
                    .setSocketTimeout(this.readTimeout)
                    .setConnectTimeout(Long.valueOf(this.connectionTimeout).intValue())
                    .setConnectionRequestTimeout(Long.valueOf(this.connectionTimeout).intValue())
                    .setCircularRedirectsAllowed(this.circularRedirectsAllowed)
                    .setRedirectsEnabled(this.redirectsEnabled)
                    .setAuthenticationEnabled(this.authenticationEnabled)
                    .build();


            final HttpClientBuilder builder = HttpClients.custom()
                    .setConnectionManager(connMgmr)
                    .setDefaultRequestConfig(requestConfig)
                    .setSSLSocketFactory(sslsf)
                    .setSSLHostnameVerifier(this.hostnameVerifier)
                    .setRedirectStrategy(this.redirectionStrategy)
                    .setDefaultCredentialsProvider(this.credentialsProvider)
                    .setDefaultCookieStore(this.cookieStore)
                    .setConnectionReuseStrategy(this.connectionReuseStrategy)
                    .setConnectionBackoffStrategy(this.connectionBackoffStrategy)
                    .setServiceUnavailableRetryStrategy(this.serviceUnavailableRetryStrategy)
                    .setProxyAuthenticationStrategy(this.proxyAuthenticationStrategy)
                    .setDefaultHeaders(this.defaultHeaders)
                    .useSystemProperties();

            return builder.build();

        } catch (final Exception e) {
            LOGGER.error(e.getMessage(), e);
            throw Throwables.propagate(e);
        }
    }

    /**
     * Build a {@link FutureRequestExecutionService} from the current properties and a HTTP client.
     *
     * @param httpClient the provided HTTP client
     * @return the built request executor service
     */
    private FutureRequestExecutionService buildRequestExecutorService(final CloseableHttpClient httpClient) {
        if (this.executorService == null) {
            this.executorService = new ThreadPoolExecutor(this.threadsNumber, this.threadsNumber,
                    0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(this.queueSize));
        }

        return new FutureRequestExecutionService(httpClient, this.executorService);
    }

    public ExecutorService getExecutorService() {
        return this.executorService;
    }

    public void setExecutorService(final ExecutorService executorService) {
        this.executorService = executorService;
    }

    public int getThreadsNumber() {
        return this.threadsNumber;
    }

    public void setThreadsNumber(final int threadsNumber) {
        this.threadsNumber = threadsNumber;
    }

    public int getQueueSize() {
        return this.queueSize;
    }

    public void setQueueSize(final int queueSize) {
        this.queueSize = queueSize;
    }

    public int getMaxPooledConnections() {
        return this.maxPooledConnections;
    }

    public void setMaxPooledConnections(final int maxPooledConnections) {
        this.maxPooledConnections = maxPooledConnections;
    }

    public int getMaxConnectionsPerRoute() {
        return this.maxConnectionsPerRoute;
    }

    public void setMaxConnectionsPerRoute(final int maxConnectionsPerRoute) {
        this.maxConnectionsPerRoute = maxConnectionsPerRoute;
    }

    public List<Integer> getAcceptableCodes() {
        return Collections.unmodifiableList(this.acceptableCodes);
    }

    public void setAcceptableCodes(final int[] acceptableCodes) {
        this.acceptableCodes = IntStream.of(acceptableCodes).boxed().collect(Collectors.toList());
    }

    public long getConnectionTimeout() {
        return this.connectionTimeout;
    }

    public void setConnectionTimeout(final long connectionTimeout) {
        this.connectionTimeout = connectionTimeout;
    }

    public int getReadTimeout() {
        return this.readTimeout;
    }


    public void setReadTimeout(
            final int readTimeout) {
        this.readTimeout = readTimeout;
    }

    public RedirectStrategy getRedirectionStrategy() {
        return this.redirectionStrategy;
    }

    public void setRedirectionStrategy(final RedirectStrategy redirectionStrategy) {
        this.redirectionStrategy = redirectionStrategy;
    }

    public SSLConnectionSocketFactory getSslSocketFactory() {
        return this.sslSocketFactory;
    }

    public void setSslSocketFactory(final SSLConnectionSocketFactory sslSocketFactory) {
        this.sslSocketFactory = sslSocketFactory;
    }

    public HostnameVerifier getHostnameVerifier() {
        return this.hostnameVerifier;
    }

    public void setHostnameVerifier(final HostnameVerifier hostnameVerifier) {
        this.hostnameVerifier = hostnameVerifier;
    }

    public CredentialsProvider getCredentialsProvider() {
        return this.credentialsProvider;
    }

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

    public CookieStore getCookieStore() {
        return this.cookieStore;
    }

    public void setCookieStore(final CookieStore cookieStore) {
        this.cookieStore = cookieStore;
    }

    public ConnectionReuseStrategy getConnectionReuseStrategy() {
        return this.connectionReuseStrategy;
    }

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

    public ConnectionBackoffStrategy getConnectionBackoffStrategy() {
        return this.connectionBackoffStrategy;
    }

    public void setConnectionBackoffStrategy(final ConnectionBackoffStrategy connectionBackoffStrategy) {
        this.connectionBackoffStrategy = connectionBackoffStrategy;
    }

    public ServiceUnavailableRetryStrategy getServiceUnavailableRetryStrategy() {
        return this.serviceUnavailableRetryStrategy;
    }

    public void setServiceUnavailableRetryStrategy(final ServiceUnavailableRetryStrategy serviceUnavailableRetryStrategy) {
        this.serviceUnavailableRetryStrategy = serviceUnavailableRetryStrategy;
    }

    public Collection<? extends Header> getDefaultHeaders() {
        return this.defaultHeaders;
    }

    public void setDefaultHeaders(final Collection<? extends Header> defaultHeaders) {
        this.defaultHeaders = defaultHeaders;
    }

    public AuthenticationStrategy getProxyAuthenticationStrategy() {
        return this.proxyAuthenticationStrategy;
    }

    public void setProxyAuthenticationStrategy(final AuthenticationStrategy proxyAuthenticationStrategy) {
        this.proxyAuthenticationStrategy = proxyAuthenticationStrategy;
    }

    public boolean isCircularRedirectsAllowed() {
        return this.circularRedirectsAllowed;
    }

    public void setCircularRedirectsAllowed(final boolean circularRedirectsAllowed) {
        this.circularRedirectsAllowed = circularRedirectsAllowed;
    }

    public boolean isAuthenticationEnabled() {
        return this.authenticationEnabled;
    }

    public void setAuthenticationEnabled(final boolean authenticationEnabled) {
        this.authenticationEnabled = authenticationEnabled;
    }

    public boolean isRedirectsEnabled() {
        return this.redirectsEnabled;
    }

    public void setRedirectsEnabled(final boolean redirectsEnabled) {
        this.redirectsEnabled = redirectsEnabled;
    }

    /**
     * Destroy.
     */
    @PreDestroy
    public void destroy() {
        if (this.executorService != null) {
            this.executorService.shutdownNow();
            this.executorService = null;
        }
    }

    /**
     * The type Default http client.
     */
    public static class DefaultHttpClient extends SimpleHttpClientFactoryBean {
    }



    /**
     * The type Ssl trust store aware http client.
     */
    public static class SslTrustStoreAwareHttpClient extends DefaultHttpClient {

    }
}
