/*
 * Copyright 2017-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.cloud.dataflow.rest.util;

import java.net.URI;

import org.apache.http.HttpHost;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.ProxyAuthenticationStrategy;

import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.util.Assert;

/**
 * Utility for configuring a {@link CloseableHttpClient}. This class allows for
 * chained method invocation. If both basic auth credentials and a target host
 * are provided, the HTTP client will aggressively send the credentials
 * without having to receive an HTTP 401 response first.
 *
 * <p>This class can also be used to configure the client used by
 * {@link org.springframework.web.client.RestTemplate} using
 * {@link #buildClientHttpRequestFactory()}.
 *
 * @author Mike Heath
 * @author Gunnar Hillert
 */
public class HttpClientConfigurer {

	private final HttpClientBuilder httpClientBuilder;

	private boolean useBasicAuth;
	private HttpHost targetHost;

	private CredentialsProvider credentialsProvider;

	public static HttpClientConfigurer create(URI targetHost) {
		return new HttpClientConfigurer(targetHost);
	}

	protected HttpClientConfigurer(URI targetHost) {
		httpClientBuilder = HttpClientBuilder.create();
		this.targetHost = new HttpHost(targetHost.getHost(), targetHost.getPort(), targetHost.getScheme());
	}

	public HttpClientConfigurer basicAuthCredentials(String username, String password) {
		final CredentialsProvider credentialsProvider = this.getOrInitializeCredentialsProvider();
		credentialsProvider.setCredentials(new AuthScope(this.targetHost), new UsernamePasswordCredentials(username, password));
		httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
		useBasicAuth = true;
		return this;
	}

	private CredentialsProvider getOrInitializeCredentialsProvider() {
		if (this.credentialsProvider == null) {
			this.credentialsProvider = new BasicCredentialsProvider();
		}
		return this.credentialsProvider;
	}

	/**
	 * Configures the {@link HttpClientBuilder} with a proxy host. If the
	 * {@code proxyUsername} and {@code proxyPassword} are not {@code null}
	 * then a {@link CredentialsProvider} is also configured for the proxy host.
	 *
	 * @param proxyUri Must not be null and must be configured with a scheme (http or https).
	 * @param proxyUsername May be null
	 * @param proxyPassword May be null
	 * @return a reference to {@code this} to enable chained method invocation
	 */
	public HttpClientConfigurer withProxyCredentials(URI proxyUri, String proxyUsername, String proxyPassword) {

		Assert.notNull(proxyUri, "The proxyUri must not be null.");
		Assert.hasText(proxyUri.getScheme(), "The scheme component of the proxyUri must not be empty.");

		httpClientBuilder
			.setProxy(new HttpHost(proxyUri.getHost(), proxyUri.getPort(), proxyUri.getScheme()));
		if (proxyUsername !=null && proxyPassword != null) {
			final CredentialsProvider credentialsProvider = this.getOrInitializeCredentialsProvider();
			credentialsProvider.setCredentials(
				new AuthScope(proxyUri.getHost(), proxyUri.getPort()),
				new UsernamePasswordCredentials(proxyUsername, proxyPassword));
			httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider)
				.setProxyAuthenticationStrategy(new ProxyAuthenticationStrategy());
		}
		return this;
	}

	/**
	 * Sets the client's {@link javax.net.ssl.SSLContext} to use
	 * {@link HttpUtils#buildCertificateIgnoringSslContext()}.
	 *
	 * @return a reference to {@code this} to enable chained method invocation
	 */
	public HttpClientConfigurer skipTlsCertificateVerification() {
		httpClientBuilder.setSSLContext(HttpUtils.buildCertificateIgnoringSslContext());
		httpClientBuilder.setSSLHostnameVerifier(new NoopHostnameVerifier());

		return this;
	}

	public HttpClientConfigurer skipTlsCertificateVerification(boolean skipTlsCertificateVerification) {
		if (skipTlsCertificateVerification) {
			skipTlsCertificateVerification();
		}

		return this;
	}

	public HttpClientConfigurer addInterceptor(HttpRequestInterceptor interceptor) {
		httpClientBuilder.addInterceptorLast(interceptor);

		return this;
	}

	public CloseableHttpClient buildHttpClient() {
		return httpClientBuilder.build();
	}

	public ClientHttpRequestFactory buildClientHttpRequestFactory() {
		if (useBasicAuth && targetHost != null) {
			return new PreemptiveBasicAuthHttpComponentsClientHttpRequestFactory(buildHttpClient(), targetHost);
		} else {
			return new HttpComponentsClientHttpRequestFactory(buildHttpClient());
		}
	}

}
