/*
 * 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.cloudfoundry.identity.uaa.api.common.impl;

import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.cloudfoundry.identity.uaa.api.common.model.WrappedSearchResults;
import org.cloudfoundry.identity.uaa.api.common.model.expr.FilterRequest;
import org.cloudfoundry.identity.uaa.api.common.model.expr.FilterRequestBuilder;
import org.cloudfoundry.identity.uaa.rest.SearchResults;
import org.cloudfoundry.identity.uaa.scim.ScimCore;
import org.cloudfoundry.identity.uaa.scim.ScimUser;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.client.token.AccessTokenProvider;
import org.springframework.security.oauth2.client.token.AccessTokenProviderChain;
import org.springframework.security.oauth2.client.token.DefaultAccessTokenRequest;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsAccessTokenProvider;
import org.springframework.security.oauth2.client.token.grant.implicit.ImplicitAccessTokenProvider;
import org.springframework.security.oauth2.client.token.grant.password.ResourceOwnerPasswordAccessTokenProvider;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;

/**
 * A helper clas used by the various <code>*Operations</code> implementations to handle JSON HTTP communications with
 * the UAA server
 * 
 * @author Josh Ghiloni
 *
 */
public class UaaConnectionHelper {
	private static final AccessTokenProviderChain CHAIN = new AccessTokenProviderChain(
			Arrays.<AccessTokenProvider> asList(new ClientCredentialsAccessTokenProvider(),
					new ImplicitAccessTokenProvider(), new ResourceOwnerPasswordAccessTokenProvider()));

	private OAuth2AccessToken token;

	private URL url;

	private OAuth2ProtectedResourceDetails creds;

	private static final Log log = LogFactory.getLog(UaaConnectionHelper.class);

	/**
	 * Establish connectivity information for this session.
	 * 
	 * @param url
	 * @param creds
	 * @see org.cloudfoundry.identity.uaa.api.UaaConnectionFactory#getConnection(URL, OAuth2ProtectedResourceDetails)
	 */
	public UaaConnectionHelper(URL url, OAuth2ProtectedResourceDetails creds) {
		this.url = url;
		this.creds = creds;
	}

	/**
	 * Do an HTTP GET
	 * 
	 * @param uri the URI of the endpoint (relative to the base URL set in the constructor)
	 * @param responseType the object type to be returned
	 * @param uriVariables any uri variables
	 * @return the response body
	 * @see #exchange(HttpMethod, Object, String, ParameterizedTypeReference, Object...)
	 */
	public <ResponseType> ResponseType get(String uri, ParameterizedTypeReference<ResponseType> responseType,
			Object... uriVariables) {
		return exchange(HttpMethod.GET, null, uri, responseType, uriVariables);
	}

	/**
	 * Do an HTTP DELETE
	 * 
	 * @param uri the URI of the endpoint (relative to the base URL set in the constructor)
	 * @param responseType the object type to be returned
	 * @param uriVariables any uri variables
	 * @return the response body
	 * @see #exchange(HttpMethod, Object, String, ParameterizedTypeReference, Object...)
	 */
	public <ResponseType> ResponseType delete(String uri, ParameterizedTypeReference<ResponseType> responseType,
			Object... uriVariables) {
		return exchange(HttpMethod.DELETE, null, uri, responseType, uriVariables);
	}

	/**
	 * Do an HTTP POST
	 *
	 * @param uri the URI of the endpoint (relative to the base URL set in the constructor)
	 * @param body the request body
	 * @param responseType the object type to be returned
	 * @param uriVariables any uri variables
	 * @return the response body
	 * @see #exchange(HttpMethod, Object, String, ParameterizedTypeReference, Object...)
	 */
	public <RequestType, ResponseType> ResponseType post(String uri, RequestType body,
			ParameterizedTypeReference<ResponseType> responseType, Object... uriVariables) {
		return exchange(HttpMethod.POST, body, uri, responseType, uriVariables);
	}

	/**
	 * Do an HTTP PUT
	 *
	 * @param uri the URI of the endpoint (relative to the base URL set in the constructor)
	 * @param body the request body
	 * @param responseType the object type to be returned
	 * @param uriVariables any uri variables
	 * @return the response body
	 * @see #exchange(HttpMethod, Object, String, ParameterizedTypeReference, Object...)
	 */
	public <RequestType, ResponseType> ResponseType put(String uri, RequestType body,
			ParameterizedTypeReference<ResponseType> responseType, Object... uriVariables) {
		return exchange(HttpMethod.PUT, body, uri, responseType, uriVariables);
	}

	/**
	 * Do an HTTP PUT with SCIM features. SCIM requires PUT requests of a SCIM object have the version of the object set
	 * as the <code>If-Match</code> request header.
	 *
	 * @param uri the URI of the endpoint (relative to the base URL set in the constructor)
	 * @param body the request body
	 * @param responseType the object type to be returned
	 * @param uriVariables any uri variables
	 * @return the response body
	 * @see #exchange(HttpMethod, HttpHeaders, Object, String, ParameterizedTypeReference, Object...)
	 */
	public <RequestType extends ScimCore, ResponseType> ResponseType putScimObject(String uri, RequestType body,
			ParameterizedTypeReference<ResponseType> responseType, Object... uriVariables) {
		HttpHeaders headers = new HttpHeaders();
		headers.set("if-match", String.valueOf(body.getMeta().getVersion()));

		return exchange(HttpMethod.PUT, headers, body, uri, responseType, uriVariables);
	}

	/**
	 * Convenience method to get a user ID for a given username. Equivalent to calling
	 * 
	 * <pre>
	 * {@link org.cloudfoundry.identity.uaa.api.user.UaaUserOperations UaaUserOperations} operations = connection.userOperations();
	 * 
	 * {@link FilterRequestBuilder} builder = new FilterRequestBuilder();
	 * builder.equals("username", userName).attributes("id");
	 * 
	 * SearchResults&lt;ScimUser&gt; users = operations.getUsers(builder.build());
	 * 
	 * return users.getResources().iterator().next().getId();
	 * </pre>
	 * 
	 * @param userName the userName
	 * @return the user ID
	 */
	public String getUserIdByName(String userName) {
		FilterRequestBuilder builder = new FilterRequestBuilder();
		builder.equals("username", userName).attributes("id");

		FilterRequest request = builder.build();

		String uri = buildScimFilterUrl("/Users", request);

		try {
			SearchResults<ScimUser> retval = exchange(HttpMethod.GET, null, uri,
					new ParameterizedTypeReference<WrappedSearchResults<ScimUser>>() {
					});

			Collection<ScimUser> resources = retval.getResources();

			if (CollectionUtils.isEmpty(resources)) {
				return null;
			}

			ScimUser user = resources.iterator().next();
			return user.getId();
		}
		catch (Throwable t) {
			t.printStackTrace();
			return null;
		}
	}

	/**
	 * Make a REST call with default headers
	 * 
	 * @param method the Http Method (GET, POST, etc)
	 * @param uri the URI of the endpoint (relative to the base URL set in the constructor)
	 * @param body the request body
	 * @param responseType the object type to be returned
	 * @param uriVariables any uri variables
	 * @return the response body
	 * @see #exchange(HttpMethod, HttpHeaders, Object, String, ParameterizedTypeReference, Object...)
	 */
	private <RequestType, ResponseType> ResponseType exchange(HttpMethod method, RequestType body, String uri,
			ParameterizedTypeReference<ResponseType> responseType, Object... uriVariables) {
		return exchange(method, new HttpHeaders(), body, uri, responseType, uriVariables);
	}

	/**
	 * Make a REST call with custom headers
	 * 
	 * @param method the Http Method (GET, POST, etc)
	 * @param uri the URI of the endpoint (relative to the base URL set in the constructor)
	 * @param body the request body
	 * @param responseType the object type to be returned
	 * @param uriVariables any uri variables
	 * @return the response body
	 * @see org.springframework.web.client.RestTemplate#exchange(String, HttpMethod, HttpEntity, ParameterizedTypeReference, Object...)
	 */
	private <RequestType, ResponseType> ResponseType exchange(HttpMethod method, HttpHeaders headers, RequestType body,
			String uri, ParameterizedTypeReference<ResponseType> responseType, Object... uriVariables) {
		getHeaders(headers);

		RestTemplate template = new RestTemplate();
		template.setInterceptors(LoggerInterceptor.INTERCEPTOR);

		HttpEntity<RequestType> requestEntity = null;
		if (body == null) {
			requestEntity = new HttpEntity<RequestType>(headers);
		}
		else {
			requestEntity = new HttpEntity<RequestType>(body, headers);
		}

		// combine url into the varargs
		List<Object> varList = new ArrayList<Object>();
		varList.add(url);
		if (uriVariables != null && uriVariables.length > 0) {
			varList.addAll(Arrays.asList(uriVariables));
		}

		ResponseEntity<ResponseType> responseEntity = template.exchange("{base}" + uri, method, requestEntity,
				responseType, varList.toArray());

		if (HttpStatus.Series.SUCCESSFUL.equals(responseEntity.getStatusCode().series())) {
			return responseEntity.getBody();
		}
		else {
			return null;
		}
	}

	/**
	 * Because variable substitution used by {@link org.springframework.web.client.RestTemplate} escapes things in a way
	 * that makes SCIM filtering difficult, manually include the parameters in the uri
	 * 
	 * @param baseUrl the url relative to the base URL (i.e. /Users, /oauth/clients, etc)
	 * @param request the Filter Request to populate the URL
	 * @return the URL
	 */
	public String buildScimFilterUrl(String baseUrl, FilterRequest request) {
		StringBuilder uriBuilder = new StringBuilder(baseUrl);

		boolean hasParams = false;

		if (request.getAttributes() != null && !request.getAttributes().isEmpty()) {
			uriBuilder.append("?attributes=").append(
					StringUtils.collectionToCommaDelimitedString(request.getAttributes()));

			hasParams = true;
		}

		if (StringUtils.hasText(request.getFilter())) {
			if (hasParams) {
				uriBuilder.append("&");
			}
			else {
				uriBuilder.append("?");
			}

			uriBuilder.append("filter=").append(request.getFilter());
			hasParams = true;
		}

		if (request.getStart() > 0) {
			if (hasParams) {
				uriBuilder.append("&");
			}
			else {
				uriBuilder.append("?");
			}

			uriBuilder.append("startIndex=").append(request.getStart());
			hasParams = true;
		}

		if (request.getCount() > 0) {
			if (hasParams) {
				uriBuilder.append("&");
			}
			else {
				uriBuilder.append("?");
			}

			uriBuilder.append("count=").append(request.getCount());
			hasParams = true;
		}

		return uriBuilder.toString();
	}

	/**
	 * Add the Authorization, Content-Type, and Accept headers to the request
	 * 
	 * @param headers
	 */
	private void getHeaders(HttpHeaders headers) {
		OAuth2AccessToken token = getAccessToken();
		headers.add("Authorization", token.getTokenType() + " " + token.getValue());

		if (headers.getContentType() == null) {
			headers.setContentType(MediaType.APPLICATION_JSON);
		}

		if (headers.getAccept() == null || headers.getAccept().size() == 0) {
			headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
		}
	}

	/**
	 * Get the OAuth access token (and refresh it if necessary)
	 * 
	 * @return
	 */
	private OAuth2AccessToken getAccessToken() {
		if (token == null) {
			token = CHAIN.obtainAccessToken(creds, new DefaultAccessTokenRequest());
		}
		else if (token.isExpired()) {
			refreshAccessToken();
		}

		return token;
	}

	/**
	 * refresh the access token
	 */
	private void refreshAccessToken() {
		Assert.notNull(token);

		token = CHAIN.refreshAccessToken(creds, token.getRefreshToken(), new DefaultAccessTokenRequest());
	}

	/**
	 * An interceptor used to log information about HTTP calls
	 * 
	 * @author Josh Ghiloni
	 *
	 */
	private static class LoggerInterceptor implements ClientHttpRequestInterceptor {
		public static final List<ClientHttpRequestInterceptor> INTERCEPTOR = Arrays
				.<ClientHttpRequestInterceptor> asList(new LoggerInterceptor());

		public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
				throws IOException {
			if (log.isDebugEnabled()) {
				log.debug(new String(body, "UTF-8"));
			}

			return execution.execute(request, body);
		}
	}
}