package com.nifty.cloud.ncs;


import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.Header;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.NoHttpResponseException;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.utils.URIUtils;
import org.apache.http.entity.BasicHttpEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.message.BasicNameValuePair;

import uk.co.lucasweb.aws.v4.signer.encoding.URLEncoding;

import com.nifty.cloud.ncs.http.ClientProperties;
import com.nifty.cloud.ncs.http.HttpClient;
import com.nifty.cloud.ncs.http.HttpInputStream;
import com.nifty.cloud.ncs.http.HttpMethod;
import com.nifty.cloud.ncs.http.HttpResponse;
import com.nifty.cloud.ncs.http.HttpResponseParser;
import com.nifty.cloud.ncs.service.StorageHeadersImpl;

/**
 * ニフクラクライアント
 *
 */
public abstract class NiftyCloudClient extends HttpClient
{
	private static final Log log = LogFactory.getLog(NiftyCloudClient.class);
	private static final Log requestLog = LogFactory.getLog("com.nifty.cloud.ncs.request");
	private static final Log performanceLog = LogFactory.getLog("com.nifty.cloud.ncs.performance");
	protected URI endpoint;
	protected String serviceName;
	protected String httpMethod;

	enum QueryStringType {
		raw,
		encoded,
	}


	/**
	 * 指定したプロパティを使用してニフクラクライアントを構築します。
	 *
	 * @param properties
	 *  ニフクラクライアントのプロパティ
	 */
	public NiftyCloudClient(ClientProperties properties)
	{
		super(properties);
	}

	/**
	 * ニフクラサービスのエンドポイントを設定します。
	 *
	 * @param endpoint
	 *  ニフクラサービスのエンドポイント
	 *
	 * @throws IllegalArgumentException
	 */
	public void setEndpoint(String endpoint) throws IllegalArgumentException
	{
		if (endpoint.contains("://") == false) {
			endpoint = properties.getProtocol() + "://" + endpoint;
		}

		try {
			this.endpoint = new URI(endpoint);
		} catch (URISyntaxException e) {
			throw (new IllegalArgumentException(e));
		}
	}

	/**
	 * ニフクラサービスのエンドポイントを取得します。
	 *
	 * @return
	 *  ニフクラサービスのエンドポイント
	 */
	public URI getEndpoint()
	{
		return (endpoint);
	}

	/**
	 * HTTP要求送信、応答受信を行います。
	 *
	 * @param <T>
	 * @param request
	 *  ニフクラサービスへの要求内容
	 *
	 * @param successParser
	 * 	正常応答時に使用するレスポンスパーサー
	 * @param errorParser
	 * 	異常応答時に使用するレスポンスパーサー
	 *
	 * @return レスポンス
	 * @throws NiftyCloudServiceException
	 */
	public <T> T execute(NiftyCloudRequest request, HttpResponseParser<NiftyCloudResponse<T>> successParser, HttpResponseParser<NiftyCloudServiceException> errorParser)
		throws NiftyCloudServiceException
	{
		this.serviceName = request.getServiceName();
		this.httpMethod = request.getHttpMethod().toString();

		try {
			HttpRequestBase httpRequest = createHttpRequest(request);

			StringBuffer hostHeader = new StringBuffer();
			hostHeader.append(request.getEndPoint().getHost());
			if (request.isUsingDefaultPort() == false) {
				hostHeader.append(":").append(endpoint.getPort());
			}
			httpRequest.addHeader("Host", hostHeader.toString());
			log.debug("Host Header: " + hostHeader.toString());

			/*
			 * Closes idle connections in the pool.
			 */
			httpClient.getConnectionManager().closeIdleConnections(30, TimeUnit.SECONDS);

			log.info("Sending Request: " + request.toString());

			int maxRetry = properties.getMaxRetry();
			int retry = maxRetry;
			while (retry > 0) {
				try {
					org.apache.http.HttpResponse httpResponse = httpClient.execute(httpRequest);

					if (isSuccess(httpResponse)) {
						return (parseResponse(httpResponse, successParser));
					} else {
						NiftyCloudServiceException exception = parseErrorResponse(httpResponse, errorParser);
						if (shouldRetry(exception, maxRetry - retry) == false) {
							throw (exception);
						}
						retry--;
						exponentiallyWait(maxRetry - retry);
						log.debug("Retry. No. " + (maxRetry - retry));
					}
				} catch (ClientProtocolException e) {
					String errorMessage = String.format("Failed to execute HTTP request: %s", e.getMessage());
					e.printStackTrace();
					log.error(errorMessage);
					if (shouldRetry(e, maxRetry - retry) == false) {
						throw (new NiftyCloudClientException(errorMessage, e));
					}
					retry--;
					exponentiallyWait(maxRetry - retry);
				} catch (IOException e) {
					String errorMessage = String.format("Failed to execute HTTP request: %s", e.getMessage());
					log.error(errorMessage);
					if (shouldRetry(e, maxRetry - retry) == false) {
						throw (new NiftyCloudClientException(errorMessage, e));
					}
					retry--;
					exponentiallyWait(maxRetry - retry);
				} finally {
				}
			}

		} catch (MalformedURLException e) {
			throw (new NiftyCloudClientException("MalformedURLException"));
		}

		throw (new NiftyCloudClientException("HTTP request retry over."));
	}

    /**
     * ニフクラサービスへの要求内容から、HTTP要求を作成します。
     *
     * @param request ニフクラサービスへの要求内容
     *
     * @return ニフクラサービスへの要求内容を含むHTTP要求
     */
	protected HttpRequestBase createHttpRequest(NiftyCloudRequest request) throws MalformedURLException
	{
		Map<String, String> parameters = request.getParameters();
		String httpMethod = request.getHttpMethod().toString();
		URI originalUri = request.getUri();
		Map<String, String> headers = request.getHeaders();
		ArrayList<NameValuePair> params = null;
		String path = originalUri.getPath();

		if (request.getResourcePath() != null && request.getResourcePath().length() > 0) {
			if (request.getResourcePath().startsWith("/") == false) {
				path += "/";
			}
			path += request.getResourcePath();
		}

		/**
		 * 要求パラメータをNameValuePairとして設定します。
		 */
		if (parameters.size() > 0) {
			params = new ArrayList<NameValuePair>();
			for (Entry<String, String> parameter : parameters.entrySet()) {
				params.add(new BasicNameValuePair(parameter.getKey(), parameter.getValue()));
			}
		}
		URI uri;
		try {
			uri = URIUtils.createURI(originalUri.getScheme(), originalUri.getHost(), originalUri.getPort(), path, null, null);
		} catch (URISyntaxException e) {
			throw (new NiftyCloudClientException("URISyntaxException", e));
		}
		log.debug("URI: " + uri.toString());

		String encodedParams = null;
		if ( params != null ) {
			encodedParams = getQueryString(params, QueryStringType.encoded);
		}

		boolean requestHasNoPayload = request.getContent() != null;
		boolean requestIsPost = request.getHttpMethod() == HttpMethod.POST;
		boolean putParamsInUri = !requestIsPost || requestHasNoPayload;
		if (encodedParams != null && putParamsInUri) {
			try {
				uri = new URI(uri.toString() + "?" + encodedParams);
			} catch (URISyntaxException e) {
				throw (new NiftyCloudClientException("URISyntaxException", e));
			}
		}

		HttpRequestBase requestBase = null;
		if (httpMethod.equals(HttpMethod.POST.toString())) {
			HttpPost post = new HttpPost(uri);
			if (request.getContent() == null && encodedParams != null) {
				try {
					post.setEntity(new StringEntity(encodedParams));
				} catch (UnsupportedEncodingException e) {
					throw (new NiftyCloudClientException(
							"Unable to create HTTP entity: " + e.getMessage(),
							e));
				}
			} else {
				long contentLength = -1;
				try {
					if (request.getHeaders().get(StorageHeadersImpl.CONTENT_LENGTH) != null) {
						contentLength = Long.parseLong(
							request.getHeaders().get(StorageHeadersImpl.CONTENT_LENGTH));
					}
				} catch (NumberFormatException nfe) {
					log.warn("Unable to parse content length from request.  "
							+ "Buffering contents in memory.");
				}
				BasicHttpEntity entity = new BasicHttpEntity();
				entity.setContentLength(contentLength);
				entity.setContentType(request.getHeaders().get(StorageHeadersImpl.CONTENT_TYPE));
				entity.setContent(request.getContent());
				post.setEntity(entity);
			}
			request.removeHeader(StorageHeadersImpl.CONTENT_LENGTH);
			requestBase = post;
		} else if (httpMethod.equals(HttpMethod.GET.toString())) {
			requestBase = createGetMethod(uri, params);
		} else if (httpMethod.equals(HttpMethod.PUT.toString())) {
			requestBase = createPutMethod(uri, params, request);
		} else if (httpMethod.equals(HttpMethod.DELETE.toString())) {
			requestBase = createDeleteMethod(uri, params);
		} else if (httpMethod.equals(HttpMethod.HEAD.toString())) {
			requestBase = createHeadMethod(uri, params);
		} else {
			throw (new NiftyCloudClientException("Unsupported HTTP method: " + httpMethod));
		}

		for (Entry<String, String> header : headers.entrySet()) {
			requestBase.setHeader(header.getKey(), header.getValue());
		}

		return (requestBase);
	}

	protected String getPathAndQuery(NiftyCloudRequest request)
	{
		Map<String, String> parameters = request.getParameters();
		URI originalUri = request.getUri();
		ArrayList<NameValuePair> params = null;
		String path = originalUri.getPath();

		if (request.getResourcePath() != null && request.getResourcePath().length() > 0) {
			if (request.getResourcePath().startsWith("/") == false) {
				path += "/";
			}
			path += request.getResourcePath();
		}

		/**
		 * 要求パラメータをNameValuePairとして設定します。
		 */
		if (parameters.size() > 0) {
			params = new ArrayList<NameValuePair>();
			for (Entry<String, String> parameter : parameters.entrySet()) {
				params.add(new BasicNameValuePair(parameter.getKey(), parameter.getValue()));
			}
		}

		String normalizePath = normalizePath(path);

		String encodedParams = null;
		if ( params != null ) {
			encodedParams = getQueryString(params, QueryStringType.raw);
		}

		String pathAndQuery = normalizePath;
		boolean requestHasNoPayload = request.getContent() != null;
		boolean requestIsPost = request.getHttpMethod() == HttpMethod.POST;
		boolean putParamsInUri = !requestIsPost || requestHasNoPayload;
		if (encodedParams != null && putParamsInUri) {
			pathAndQuery = normalizePath + "?" + encodedParams;
		}

		return pathAndQuery;
	}

	public static String normalizePath(String path) {
		StringBuilder buffer = new StringBuilder();

		if (path == null || !path.startsWith("/")) {
			buffer.append('/');
		}

		if (path != null) {
			buffer.append(path);
		}

		return buffer.toString();
	}

	public static String getQueryString(List<? extends NameValuePair> parameters, QueryStringType stringType) {
		StringBuilder result = new StringBuilder();

		for (NameValuePair parameter : parameters) {
			String key = "";
			String value = "";

			if (stringType == QueryStringType.encoded) {
				key = URLEncoding.encodeQueryComponent(parameter.getName());

				value = parameter.getValue();
				if (value != null) {
					value = URLEncoding.encodeQueryComponent(parameter.getValue());
				} else {
					value = "";
				}
			} else {
				key = parameter.getName();
				value = parameter.getValue();
			}

			if (result.length() > 0) {
				result.append('&');
			}

			result.append(key);
			if (value != null) {
				result.append("=");
				result.append(value);
			}
		}

		return result.toString();
	}


	private <T> T parseResponse(org.apache.http.HttpResponse httpResponse, HttpResponseParser<NiftyCloudResponse<T>> parser) throws IOException
	{
		HttpResponse response = createHttpResponse(httpResponse, httpMethod);

		boolean writePerformanceLog = false;
		HttpInputStream httpInputStream = null;
		if (performanceLog.isTraceEnabled()) {
			if (response.getContent() != null) {
				writePerformanceLog = true;
				httpInputStream = new HttpInputStream(response.getContent());
				response.setContent(httpInputStream);
			}
		}

		try {
			long startTime = System.currentTimeMillis();
			NiftyCloudResponse<? extends T> ncsResponse = parser.parse(response);
			long endTime = System.currentTimeMillis();

			if (writePerformanceLog) {
				performanceLog.trace(String.format("%d [bytes], %d [msec]", httpInputStream.getCount(), endTime - startTime));
			}

			if (ncsResponse == null) {
				throw (new RuntimeException("Unable to parse response data."));
			}

			requestLog.info(String.format("Success: %d, Request ID: %s", response.getStatusCode(), ncsResponse.getRequestId()));

			return (ncsResponse.getResult());

		} catch (Exception e) {
			String errorMessage = String.format("Unable to parse response (%s)", e.getMessage());
			log.error(errorMessage, e);
			throw (new NiftyCloudClientException(errorMessage, e));
		}

	}

	private NiftyCloudServiceException parseErrorResponse(org.apache.http.HttpResponse httpResponse, HttpResponseParser<NiftyCloudServiceException> parser) throws IOException
	{
		int statusCode = httpResponse.getStatusLine().getStatusCode();
		HttpResponse response = createHttpResponse(httpResponse, httpMethod);

		NiftyCloudServiceException exception = null;

		try {
			exception = parser.parse(response);
			requestLog.info(String.format("Error: %s", exception.toString()));
		} catch (Exception e) {
			String errorMessage = String.format("Unable to parse error response (%s): %s", e.getMessage(), httpResponse.getEntity().getContent().toString());
			log.error(errorMessage, e);
			throw (new NiftyCloudClientException(errorMessage, e));
		}

		exception.setStatusCode(statusCode);
		exception.setServiceName(serviceName);
		exception.fillInStackTrace();

		return (exception);
	}

	/**
	 * 指定した文字列をURLエンコードします。
	 *
	 * @param src
	 *  エンコード対象の文字列
	 *
	 * @return
	 *  エンコード後の文字列
	 */
	public String urlEncode(String src)
	{
		if (src == null) {
			return (null);
		}

		try {
			String encoded = URLEncoder.encode(src, DEFAULT_ENCODING)
									.replace("+", "%20")
									.replace("*", "%2A")
									.replace("%7E", "~")
									.replace("%2F", "/");
			return (encoded);

		} catch (UnsupportedEncodingException e) {
			throw (new NiftyCloudClientException("Can't encode " + src, e));
		}
	}

	/**
	 * リトライすべきかの判定
	 *
	 * @param exception
	 *  例外
	 * @param retry
	 *  リトライ回数
	 * @return
	 *  true : リトライすべき / false : リトライすべきでない
	 */
	protected boolean shouldRetry(Exception exception, int retry)
	{
		if (retry > properties.getMaxRetry()) {
			return (false);
		}

		if (exception instanceof NoHttpResponseException
			|| exception instanceof SocketException
			|| exception instanceof SocketTimeoutException) {
			log.debug(String.format("Retrying on %s: %s", exception.getClass().getName(), exception.getMessage()));
			return (true);
		}

		if (exception instanceof NiftyCloudServiceException) {
			NiftyCloudServiceException ncse = (NiftyCloudServiceException)exception;
			int statusCode = ncse.getStatusCode();

			if (statusCode == HttpStatus.SC_INTERNAL_SERVER_ERROR || statusCode == HttpStatus.SC_SERVICE_UNAVAILABLE) {
				return (true);
			}

			if (statusCode == HttpStatus.SC_BAD_REQUEST && ncse.getErrorCode() != null && ncse.getErrorCode().equals("Throttling")) {
				return (true);
			}
		}

		return (false);
	}

}
