/*
 * Copyright 2017-2019 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.gcp.autoconfigure.config;

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import com.google.api.gax.core.CredentialsProvider;
import com.google.auth.Credentials;

import org.springframework.cloud.bootstrap.config.PropertySourceLocator;
import org.springframework.cloud.gcp.core.DefaultCredentialsProvider;
import org.springframework.cloud.gcp.core.GcpProjectIdProvider;
import org.springframework.cloud.gcp.core.UserAgentHeaderProvider;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.util.Assert;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;

/**
 * Custom {@link PropertySourceLocator} for Google Cloud Runtime Configurator API.
 *
 * @author Jisha Abubaker
 * @author Mike Eltsufin
 *
 * @since 1.1
 */
public class GoogleConfigPropertySourceLocator implements PropertySourceLocator {

	private static final String RUNTIMECONFIG_API_ROOT = "https://runtimeconfig.googleapis.com/v1beta1/";

	private static final String ALL_VARIABLES_PATH =
			"projects/{project}/configs/{name}_{profile}/variables?returnValues=true";

	private static final String PROPERTY_SOURCE_NAME = "spring-cloud-gcp";

	private static final String AUTHORIZATION_HEADER = "Authorization";

	private String projectId;

	private Credentials credentials;

	private String name;

	private String profile;

	private int timeout;

	private boolean enabled;

	public GoogleConfigPropertySourceLocator(GcpProjectIdProvider projectIdProvider,
			CredentialsProvider credentialsProvider,
			GcpConfigProperties gcpConfigProperties) throws IOException {
		Assert.notNull(gcpConfigProperties, "Google Config properties must not be null");

		if (gcpConfigProperties.isEnabled()) {
			Assert.notNull(credentialsProvider, "Credentials provider cannot be null");
			Assert.notNull(projectIdProvider, "Project ID provider cannot be null");
			this.credentials = gcpConfigProperties.getCredentials().hasKey()
					? new DefaultCredentialsProvider(gcpConfigProperties).getCredentials()
					: credentialsProvider.getCredentials();
			this.projectId = (gcpConfigProperties.getProjectId() != null)
					? gcpConfigProperties.getProjectId()
					: projectIdProvider.getProjectId();
			Assert.notNull(this.credentials, "Credentials must not be null");

			Assert.notNull(this.projectId, "Project ID must not be null");

			this.timeout = gcpConfigProperties.getTimeoutMillis();
			this.name = gcpConfigProperties.getName();
			this.profile = gcpConfigProperties.getProfile();
			this.enabled = gcpConfigProperties.isEnabled();
			Assert.notNull(this.name, "Config name must not be null");
			Assert.notNull(this.profile, "Config profile must not be null");
		}
	}

	private HttpEntity<Void> getAuthorizedRequest() throws IOException {
		HttpHeaders headers = new HttpHeaders();
		Map<String, List<String>> credentialHeaders = this.credentials.getRequestMetadata();
		Assert.notNull(credentialHeaders, "No valid credential header(s) found");

		credentialHeaders.forEach((key, values) -> values.forEach((value) -> headers.add(key, value)));

		Assert.isTrue(headers.containsKey(AUTHORIZATION_HEADER), "Authorization header required");

		// Adds product version header for usage metrics
		new UserAgentHeaderProvider(this.getClass()).getHeaders().forEach(headers::add);

		return new HttpEntity<>(headers);
	}

	GoogleConfigEnvironment getRemoteEnvironment() throws IOException, HttpClientErrorException {
		SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
		requestFactory.setReadTimeout(this.timeout);
		RestTemplate template = new RestTemplate(requestFactory);
		HttpEntity<Void> requestEntity = getAuthorizedRequest();
		ResponseEntity<GoogleConfigEnvironment> response = template.exchange(
				RUNTIMECONFIG_API_ROOT + ALL_VARIABLES_PATH, HttpMethod.GET, requestEntity,
				GoogleConfigEnvironment.class, this.projectId, this.name, this.profile);

		if (!response.getStatusCode().is2xxSuccessful()) {
			throw new HttpClientErrorException(response.getStatusCode(),
					"Invalid response from Runtime Configurator API");
		}
		return response.getBody();
	}

	@Override
	public PropertySource<?> locate(Environment environment) {
		if (!this.enabled) {
			return new MapPropertySource(PROPERTY_SOURCE_NAME, Collections.emptyMap());
		}
		Map<String, Object> config;
		try {
			GoogleConfigEnvironment googleConfigEnvironment = getRemoteEnvironment();
			Assert.notNull(googleConfigEnvironment, "Configuration not in expected format.");
			config = googleConfigEnvironment.getConfig();
		}
		catch (Exception ex) {
			String message = String.format("Error loading configuration for %s/%s_%s", this.projectId,
					this.name, this.profile);
			throw new RuntimeException(message, ex);
		}
		return new MapPropertySource(PROPERTY_SOURCE_NAME, config);
	}

	public String getProjectId() {
		return this.projectId;
	}
}