/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2015 ForgeRock AS.
 */

package org.forgerock.http.apache.async;

import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.forgerock.http.handler.HttpClientHandler.OPTION_CONNECT_TIMEOUT;
import static org.forgerock.http.handler.HttpClientHandler.OPTION_HOSTNAME_VERIFIER;
import static org.forgerock.http.handler.HttpClientHandler.OPTION_KEY_MANAGERS;
import static org.forgerock.http.handler.HttpClientHandler.OPTION_MAX_CONNECTIONS;
import static org.forgerock.http.handler.HttpClientHandler.OPTION_REUSE_CONNECTIONS;
import static org.forgerock.http.handler.HttpClientHandler.OPTION_SO_TIMEOUT;
import static org.forgerock.http.handler.HttpClientHandler.OPTION_SSLCONTEXT_ALGORITHM;
import static org.forgerock.http.handler.HttpClientHandler.OPTION_SSL_CIPHER_SUITES;
import static org.forgerock.http.handler.HttpClientHandler.OPTION_SSL_ENABLED_PROTOCOLS;
import static org.forgerock.http.handler.HttpClientHandler.OPTION_TEMPORARY_STORAGE;
import static org.forgerock.http.handler.HttpClientHandler.OPTION_TRUST_MANAGERS;
import static org.forgerock.http.util.Lists.asArrayOrNull;

import java.security.GeneralSecurityException;
import java.util.List;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;

import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.ProtocolException;
import org.apache.http.client.RedirectStrategy;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.ssl.DefaultHostnameVerifier;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.impl.NoConnectionReuseStrategy;
import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
import org.apache.http.impl.nio.client.HttpAsyncClients;
import org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager;
import org.apache.http.impl.nio.reactor.DefaultConnectingIOReactor;
import org.apache.http.impl.nio.reactor.IOReactorConfig;
import org.apache.http.nio.conn.NoopIOSessionStrategy;
import org.apache.http.nio.conn.SchemeIOSessionStrategy;
import org.apache.http.nio.conn.ssl.SSLIOSessionStrategy;
import org.apache.http.nio.reactor.ConnectingIOReactor;
import org.apache.http.nio.reactor.IOReactorException;
import org.apache.http.protocol.HttpContext;
import org.forgerock.http.HttpApplicationException;
import org.forgerock.http.apache.NoAuthenticationStrategy;
import org.forgerock.http.io.Buffer;
import org.forgerock.http.spi.HttpClient;
import org.forgerock.http.spi.HttpClientProvider;
import org.forgerock.util.Factory;
import org.forgerock.util.Option;
import org.forgerock.util.Options;
import org.forgerock.util.time.Duration;

/**
 * Creates and configures a {@link HttpClient} instance built around Apache HTTP Async Client component.
 *
 * @see <a href="https://hc.apache.org/httpcomponents-asyncclient-dev/index.html">Apache HTTP Async Client</a>
 */
public class AsyncHttpClientProvider implements HttpClientProvider {

    /**
     * Specify the number of worker threads. If not set, the async client implementation manages this setting itself
     * (by default this is number of CPU + 1).
     */
    public static final Option<Integer> OPTION_WORKER_THREADS = Option.of(Integer.class, null);

    /**
     * A redirect strategy that never performs a redirect.
     */
    private static final RedirectStrategy DISABLE_REDIRECT = new RedirectStrategy() {
        @Override
        public boolean isRedirected(final HttpRequest request, final HttpResponse response,
                final HttpContext context) throws ProtocolException {
            return false;
        }

        @Override
        public HttpUriRequest getRedirect(final HttpRequest request, final HttpResponse response,
                final HttpContext context) throws ProtocolException {
            return null;
        }
    };

    @Override
    public HttpClient newHttpClient(final Options options) throws HttpApplicationException {

        final Factory<Buffer> storage = options.get(OPTION_TEMPORARY_STORAGE);

        // SSL
        final SSLContext sslContext;
        try {
            sslContext = SSLContext.getInstance(options.get(OPTION_SSLCONTEXT_ALGORITHM));
            sslContext.init(options.get(OPTION_KEY_MANAGERS),
                            options.get(OPTION_TRUST_MANAGERS), null);
        } catch (final GeneralSecurityException e) {
            throw new HttpApplicationException("Can't create SSL Context", e);
        }

        HostnameVerifier verifier = new DefaultHostnameVerifier();
        switch (options.get(OPTION_HOSTNAME_VERIFIER)) {
        case ALLOW_ALL:
            verifier = NoopHostnameVerifier.INSTANCE;
            break;
        }

        List<String> protocols = options.get(OPTION_SSL_ENABLED_PROTOCOLS);
        List<String> ciphers = options.get(OPTION_SSL_CIPHER_SUITES);

        // Create a registry of custom connection session strategies for supported protocol schemes
        Registry<SchemeIOSessionStrategy> registry =
                RegistryBuilder.<SchemeIOSessionStrategy>create()
                        .register("http", NoopIOSessionStrategy.INSTANCE)
                        .register("https", new SSLIOSessionStrategy(sslContext, asArrayOrNull(protocols),
                                asArrayOrNull(ciphers), verifier))
                        .build();

        // Timeouts
        final Duration soTimeout = options.get(OPTION_SO_TIMEOUT);
        final Duration connectTimeout = options.get(OPTION_CONNECT_TIMEOUT);
        // FIXME GSA Can we support requestConnectTimeout ?

        // Create I/O reactor configuration
        IOReactorConfig.Builder reactorBuilder = IOReactorConfig.custom();

        if (!connectTimeout.isUnlimited()) {
            reactorBuilder.setConnectTimeout((int) connectTimeout.to(MILLISECONDS));
        }
        if (!soTimeout.isUnlimited()) {
            reactorBuilder.setSoTimeout((int) soTimeout.to(MILLISECONDS));
        }
        Integer threadCount = options.get(OPTION_WORKER_THREADS);
        if (threadCount != null) {
            reactorBuilder.setIoThreadCount(threadCount);
        }
        IOReactorConfig ioReactorConfig = reactorBuilder.build();

        // Create a custom I/O reactor
        ConnectingIOReactor reactor;
        try {
            reactor = new DefaultConnectingIOReactor(ioReactorConfig);
        } catch (IOReactorException e) {
            throw new HttpApplicationException("Cannot create I/O Reactor", e);
        }

        // Create a connection manager with custom configuration.
        PoolingNHttpClientConnectionManager manager = new PoolingNHttpClientConnectionManager(reactor, registry);

        // Connection pooling
        final int maxConnections = options.get(OPTION_MAX_CONNECTIONS);
        manager.setMaxTotal(maxConnections);
        manager.setDefaultMaxPerRoute(maxConnections);

        // FIXME GSA Couldn't find how to configure retries in async http client
        //if (!options.get(OPTION_RETRY_REQUESTS)) {
        //    builder.disableAutomaticRetries();
        //}

        // Create a client with the given custom dependencies and configuration.
        HttpAsyncClientBuilder builder = HttpAsyncClients.custom();

        if (!options.get(OPTION_REUSE_CONNECTIONS)) {
            builder.setConnectionReuseStrategy(NoConnectionReuseStrategy.INSTANCE);
        }

        // TODO Uncomment when we'll have a user-agent Option
        // builder.setUserAgent("CHF/1.0");

        CloseableHttpAsyncClient client = builder.setConnectionManager(manager)
                .disableCookieManagement()
                .setRedirectStrategy(DISABLE_REDIRECT)
                .setTargetAuthenticationStrategy(NoAuthenticationStrategy.INSTANCE)
                .setProxyAuthenticationStrategy(NoAuthenticationStrategy.INSTANCE)
                .build();
        client.start();
        return new AsyncHttpClient(client, storage);
    }
}
