/*
 * Copyright (C) 2011 lightcouch.org
 *
 * 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
 *
 * http://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.lightcouch;

import java.io.Closeable;
import java.io.IOException;
import java.net.URLDecoder;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.net.ssl.SSLContext;

import org.apache.http.Consts;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.HttpResponse;
import org.apache.http.HttpResponseInterceptor;
import org.apache.http.RequestLine;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.AuthCache;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.client.utils.HttpClientUtils;
import org.apache.http.config.ConnectionConfig;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.BasicAuthCache;
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.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;

/**
 * Presents a <i>client</i> to CouchDB database server.
 * <p>This class is the main object to use to gain access to the APIs.
 * <h3>Usage Example:</h3> 
 * <p>Create a new client instance:
 * <pre>
 * CouchDbClient dbClient = new CouchDbClient();
 * </pre>
 * 
 * <p>Start using the API by the client:
 * 
 * <p>Documents <code>CRUD</code> APIs is accessed by the client directly, eg.: {@link CouchDbClientBase#find(Class, String) dbClient.find(Foo.class, "doc-id")}
 * <p>View APIs {@link View dbClient.view()} 
 * <p>Change Notifications {@link Changes dbClient.changes()}
 * <p>Replication {@link Replication dbClient.replication()} and {@link Replicator dbClient.replicator()} 
 * <p>DB server {@link CouchDbContext dbClient.context()}
 * <p>Design documents {@link CouchDbDesign dbClient.design()}
 * 
 * <p>At the end of a client usage; it's useful to call: {@link #shutdown()} to ensure proper release of resources.
 * 
 * @see CouchDbClientAndroid
 * @since 0.0.2
 * @author Ahmed Yehia
 *
 */
public class CouchDbClient extends CouchDbClientBase implements Closeable {

	/**
	 * Constructs a new instance of this class, expects a configuration file named 
	 * <code>couchdb.properties</code> to be available in your application default classpath.
	 */
	public CouchDbClient() {
		super();
	}
	
	/**
	 * Constructs a new instance of this class.
	 * @param configFileName The configuration file name.
	 */
	public CouchDbClient(String configFileName) {
		super(new CouchDbConfig(configFileName));
	}
	
	/**
	 * Constructs a new instance of this class.
	 * @param dbName The database name.
	 * @param createDbIfNotExist To create a new database if it does not already exist.
	 * @param protocol The protocol to use (i.e http or https)
	 * @param host The database host address
	 * @param port The database listening port
	 * @param username The Username credential
	 * @param password The Password credential
	 */
	public CouchDbClient(String dbName, boolean createDbIfNotExist, 
			String protocol, String host, int port, String username, String password) { 
		super(new CouchDbConfig(new CouchDbProperties(dbName, createDbIfNotExist, protocol, host, port, username, password)));
	}
	
	/**
	 * Constructs a new instance of this class.
	 * @param properties An object containing configuration properties.
	 * @see CouchDbProperties
	 */
	public CouchDbClient(CouchDbProperties properties) {
		super(new CouchDbConfig(properties));
	}
	
	/**
	 * @return {@link CloseableHttpClient} instance.
	 */
	@Override
	HttpClient createHttpClient(CouchDbProperties props) {
		try {
			Registry<ConnectionSocketFactory> registry = createRegistry(props);
			PoolingHttpClientConnectionManager ccm = createConnectionManager(props, registry);
			HttpClientBuilder clientBuilder = HttpClients.custom()
					.setConnectionManager(ccm)
					.setDefaultConnectionConfig(ConnectionConfig.custom()
							.setCharset(Consts.UTF_8).build())
					.setDefaultRequestConfig(RequestConfig.custom()
							.setSocketTimeout(props.getSocketTimeout())
							.setConnectTimeout(props.getConnectionTimeout()).build());
			if (props.getProxyHost() != null) 
				clientBuilder.setProxy(new HttpHost(props.getProxyHost(), props.getProxyPort()));
			if (props.getUsername() != null) {
				CredentialsProvider credsProvider = new BasicCredentialsProvider();
				credsProvider.setCredentials(new AuthScope(props.getHost(),
						props.getPort()),
						new UsernamePasswordCredentials(props.getUsername(),
								props.getPassword()));
				clientBuilder.setDefaultCredentialsProvider(credsProvider);
				props.clearPassword();
			}
			registerInterceptors(clientBuilder);
			return clientBuilder.build();
		} catch (Exception e) {
			throw new IllegalStateException("Error Creating HTTPClient: ", e);
		}
	}

	@Override
	HttpContext createContext() {
		AuthCache authCache = new BasicAuthCache();
		authCache.put(host, new BasicScheme());
		HttpContext context = new BasicHttpContext();
	    context.setAttribute(HttpClientContext.AUTH_CACHE, authCache);
		return context;
	}
	
	private PoolingHttpClientConnectionManager createConnectionManager(
			CouchDbProperties props, Registry<ConnectionSocketFactory> registry) {
		PoolingHttpClientConnectionManager ccm = new PoolingHttpClientConnectionManager(registry);
		if (props.getMaxConnections() != 0) {
			ccm.setMaxTotal(props.getMaxConnections());
			ccm.setDefaultMaxPerRoute(props.getMaxConnections());
		}
		return ccm;
	}

	private Registry<ConnectionSocketFactory> createRegistry(CouchDbProperties props) throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException {
		RegistryBuilder<ConnectionSocketFactory> registry = RegistryBuilder
				.<ConnectionSocketFactory> create();

		if("https".equals(props.getProtocol())) {
			SSLContext sslcontext = SSLContexts.custom()
					.loadTrustMaterial(null, new TrustStrategy(){
						public boolean isTrusted(X509Certificate[] chain, String authType)
								throws CertificateException {
							return true;
						}
					}).build();

			return registry.register("https", new SSLConnectionSocketFactory(sslcontext, 
					new NoopHostnameVerifier())).build();
		} else {
			return registry.register("http", PlainConnectionSocketFactory.INSTANCE).build();
		}
	}
	
	/**
	 * Adds request/response interceptors for logging and validation.
	 * @param clientBuilder
	 */
	private void registerInterceptors(HttpClientBuilder clientBuilder) {
		clientBuilder.addInterceptorFirst(new HttpRequestInterceptor() {
			public void process(final HttpRequest request,
					final HttpContext context) throws IOException {
				if (log.isInfoEnabled()) {
					RequestLine req = request.getRequestLine();
					log.info("> " + req.getMethod() + " " + URLDecoder.decode(req.getUri(), "UTF-8"));
				}
			}
		});
		clientBuilder.addInterceptorFirst(new HttpResponseInterceptor() {
			public void process(final HttpResponse response,
					final HttpContext context) throws IOException {
				if (log.isInfoEnabled()) {
					log.info("< Status: " + response.getStatusLine().getStatusCode());
				}
				validate(response);
			}
		});
	}
	
	public void shutdown() {
		HttpClientUtils.closeQuietly(this.httpClient);
	}

	@Override
	public void close() throws IOException {
		shutdown();
	}
}
