/*
 * Copyright 2017-2020 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.vault.config;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import org.springframework.cloud.bootstrap.config.PropertySourceLocator;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertySource;
import org.springframework.util.Assert;

/**
 * Abstract {@link PropertySourceLocator} to create {@link PropertySource}s based on
 * {@link VaultKeyValueBackendProperties} and {@link SecretBackendMetadata}.
 *
 * @author Mark Paluch
 */
public abstract class VaultPropertySourceLocatorSupport implements PropertySourceLocator {

	private final String propertySourceName;

	private final PropertySourceLocatorConfiguration propertySourceLocatorConfiguration;

	/**
	 * Creates a new {@link VaultPropertySourceLocatorSupport} given a
	 * {@link PropertySourceLocatorConfiguration}.
	 * @param propertySourceName must not be {@literal null} or empty.
	 * @param propertySourceLocatorConfiguration must not be {@literal null}.
	 * @since 1.1
	 */
	public VaultPropertySourceLocatorSupport(String propertySourceName,
			PropertySourceLocatorConfiguration propertySourceLocatorConfiguration) {

		Assert.hasText(propertySourceName, "PropertySource name must not be empty");
		Assert.notNull(propertySourceLocatorConfiguration,
				"PropertySourceLocatorConfiguration must not be null");

		this.propertySourceName = propertySourceName;
		this.propertySourceLocatorConfiguration = propertySourceLocatorConfiguration;
	}

	static PropertySourceLocatorConfiguration createConfiguration(
			VaultKeyValueBackendProperties kvBackendProperties) {

		Assert.notNull(kvBackendProperties,
				"VaultKeyValueBackendProperties must not be null");

		return new KeyValuePropertySourceLocatorConfiguration(kvBackendProperties);
	}

	@Override
	public PropertySource<?> locate(Environment environment) {

		if (this.propertySourceLocatorConfiguration instanceof EnvironmentAware) {
			((EnvironmentAware) this.propertySourceLocatorConfiguration)
					.setEnvironment(environment);
		}

		CompositePropertySource propertySource = createCompositePropertySource(
				environment);
		initialize(propertySource);

		return propertySource;
	}

	// -------------------------------------------------------------------------
	// Implementation hooks and helper methods
	// -------------------------------------------------------------------------

	/**
	 * Allows initialization the {@link PropertySource} before use. Implementations may
	 * override this method to preload properties in the {@link PropertySource}.
	 * @param propertySource must not be {@literal null}.
	 */
	protected void initialize(CompositePropertySource propertySource) {
	}

	/**
	 * Creates a {@link CompositePropertySource}.
	 * @param environment must not be {@literal null}.
	 * @return the composite {@link PropertySource}.
	 */
	protected CompositePropertySource createCompositePropertySource(
			Environment environment) {

		List<PropertySource<?>> propertySources = doCreatePropertySources(environment);

		return doCreateCompositePropertySource(this.propertySourceName, propertySources);
	}

	/**
	 * Create {@link PropertySource}s given {@link Environment} from the property
	 * configuration.
	 * @param environment must not be {@literal null}.
	 * @return a {@link List} of ordered {@link PropertySource}s.
	 */
	protected List<PropertySource<?>> doCreatePropertySources(Environment environment) {

		Collection<SecretBackendMetadata> secretBackends = this.propertySourceLocatorConfiguration
				.getSecretBackends();
		List<SecretBackendMetadata> sorted = new ArrayList<>(secretBackends);
		List<PropertySource<?>> propertySources = new ArrayList<>();

		AnnotationAwareOrderComparator.sort(sorted);

		propertySources.addAll(doCreateKeyValuePropertySources(environment));

		for (SecretBackendMetadata backendAccessor : sorted) {

			PropertySource<?> vaultPropertySource = createVaultPropertySource(
					backendAccessor);
			propertySources.add(vaultPropertySource);
		}

		return propertySources;
	}

	/**
	 * Create {@link PropertySource}s using the kv {@literal secret} backend. Property
	 * sources for the kv secret backend derive from the application name and active
	 * profiles to generate context paths.
	 * @param environment must not be {@literal null}.
	 * @return {@link List} of {@link PropertySource}s.
	 */
	protected List<PropertySource<?>> doCreateKeyValuePropertySources(
			Environment environment) {
		return new ArrayList<>();
	}

	/**
	 * Create a {@link CompositePropertySource} given a {@link List} of
	 * {@link PropertySource}s.
	 * @param propertySourceName the property source name.
	 * @param propertySources the property sources.
	 * @return the {@link CompositePropertySource} to use.
	 */
	protected CompositePropertySource doCreateCompositePropertySource(
			String propertySourceName, List<PropertySource<?>> propertySources) {

		CompositePropertySource compositePropertySource = new CompositePropertySource(
				propertySourceName);

		for (PropertySource<?> propertySource : propertySources) {
			compositePropertySource.addPropertySource(propertySource);
		}

		return compositePropertySource;
	}

	/**
	 * Create {@link VaultPropertySource} initialized with a {@link SecretBackendMetadata}
	 * .
	 * @param accessor the {@link SecretBackendMetadata}.
	 * @return the {@link VaultPropertySource} to use.
	 */
	protected abstract PropertySource<?> createVaultPropertySource(
			SecretBackendMetadata accessor);

	private static class KeyValuePropertySourceLocatorConfiguration
			implements PropertySourceLocatorConfiguration {

		private final VaultKeyValueBackendPropertiesSupport keyValueBackendProperties;

		KeyValuePropertySourceLocatorConfiguration(
				VaultKeyValueBackendPropertiesSupport keyValueBackendProperties) {
			this.keyValueBackendProperties = keyValueBackendProperties;
		}

		@Override
		public Collection<SecretBackendMetadata> getSecretBackends() {

			if (this.keyValueBackendProperties.isEnabled()) {

				List<String> contexts = KeyValueSecretBackendMetadata.buildContexts(
						this.keyValueBackendProperties,
						this.keyValueBackendProperties.getProfiles());

				List<SecretBackendMetadata> result = new ArrayList<>(contexts.size());

				for (String context : contexts) {
					result.add(KeyValueSecretBackendMetadata.create(
							this.keyValueBackendProperties.getBackend(), context));
				}

				return result;
			}

			return Collections.emptyList();
		}

	}

	private static class WrappedPropertySourceLocatorConfiguration
			implements PropertySourceLocatorConfiguration {

		private final List<SecretBackendMetadata> metadata;

		WrappedPropertySourceLocatorConfiguration(List<SecretBackendMetadata> metadata) {
			this.metadata = metadata;
		}

		@Override
		public Collection<SecretBackendMetadata> getSecretBackends() {
			return this.metadata;
		}

	}

	private static class CompositePropertySourceConfiguration
			implements PropertySourceLocatorConfiguration, EnvironmentAware {

		private final List<PropertySourceLocatorConfiguration> configurations;

		CompositePropertySourceConfiguration(
				PropertySourceLocatorConfiguration... configurations) {

			List<PropertySourceLocatorConfiguration> copy = new ArrayList<>(
					Arrays.asList(configurations));

			AnnotationAwareOrderComparator.sortIfNecessary(copy);

			this.configurations = copy;
		}

		@Override
		public Collection<SecretBackendMetadata> getSecretBackends() {

			List<SecretBackendMetadata> result = new ArrayList<>();

			for (PropertySourceLocatorConfiguration configuration : this.configurations) {
				result.addAll(configuration.getSecretBackends());
			}

			return result;
		}

		@Override
		public void setEnvironment(Environment environment) {

			for (PropertySourceLocatorConfiguration configuration : this.configurations) {
				if (configuration instanceof EnvironmentAware) {
					((EnvironmentAware) configuration).setEnvironment(environment);
				}
			}
		}

	}

}