/*
 * Copyright 2013-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.config.client;

import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import javax.annotation.PostConstruct;

import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.config.environment.EnvironmentMediaType;
import org.springframework.cloud.configuration.TlsProperties;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponentsBuilder;

/**
 * @author Dave Syer
 *
 */
@ConfigurationProperties(ConfigClientProperties.PREFIX)
public class ConfigClientProperties {

	/**
	 * Prefix for Spring Cloud Config properties.
	 */
	public static final String PREFIX = "spring.cloud.config";

	/**
	 * Placeholder string that allows ${spring.cloud.config.name} to override
	 * ${spring.application.name:application}.
	 */
	public static final String NAME_PLACEHOLDER = "${" + ConfigClientProperties.PREFIX
			+ ".name:${spring.application.name:application}}";

	/**
	 * Name of config discovery enabled property.
	 */
	public static final String CONFIG_DISCOVERY_ENABLED = PREFIX + ".discovery.enabled";

	/**
	 * Token header name.
	 */
	public static final String TOKEN_HEADER = "X-Config-Token";

	/**
	 * State header name.
	 */
	public static final String STATE_HEADER = "X-Config-State";

	/**
	 * Authorization header name.
	 */
	public static final String AUTHORIZATION = "authorization";

	/**
	 * Default profile value.
	 */
	public static final String DEFAULT_PROFILE = "default";

	/**
	 * Flag to say that remote configuration is enabled. Default true;
	 */
	private boolean enabled = true;

	/**
	 * The default profile to use when fetching remote configuration (comma-separated).
	 * Default is "default".
	 */
	private String profile = DEFAULT_PROFILE;

	/**
	 * Name of application used to fetch remote properties.
	 */
	@Value("${spring.application.name:application}")
	private String name;

	/**
	 * The label name to use to pull remote configuration properties. The default is set
	 * on the server (generally "master" for a git based server).
	 */
	private String label;

	/**
	 * The username to use (HTTP Basic) when contacting the remote server.
	 */
	private String username;

	/**
	 * The password to use (HTTP Basic) when contacting the remote server.
	 */
	private String password;

	/**
	 * The URI of the remote server (default http://localhost:8888).
	 */
	private String[] uri = { "http://localhost:8888" };

	/**
	 * The Accept header media type to send to config server.
	 */
	private String mediaType = EnvironmentMediaType.V2_JSON;

	/**
	 * Discovery properties.
	 */
	private Discovery discovery = new Discovery();

	/**
	 * TLS properties.
	 */
	private TlsProperties tls = new TlsProperties();

	/**
	 * Flag to indicate that failure to connect to the server is fatal (default false).
	 */
	private boolean failFast = false;

	/**
	 * Security Token passed thru to underlying environment repository.
	 */
	private String token;

	/**
	 * timeout on waiting to read data from the Config Server.
	 */
	private int requestReadTimeout = (60 * 1000 * 3) + 5000;

	/**
	 * timeout on waiting to connect to the Config Server.
	 */
	private int requestConnectTimeout = 1000 * 10;

	/**
	 * Flag to indicate whether to send state. Default true.
	 */
	private boolean sendState = true;

	/**
	 * Additional headers used to create the client request.
	 */
	private Map<String, String> headers = new HashMap<>();

	ConfigClientProperties() {
	}

	public ConfigClientProperties(Environment environment) {
		String[] profiles = environment.getActiveProfiles();
		if (profiles.length == 0) {
			profiles = environment.getDefaultProfiles();
		}
		this.setProfile(StringUtils.arrayToCommaDelimitedString(profiles));
	}

	public boolean isEnabled() {
		return this.enabled;
	}

	public void setEnabled(boolean enabled) {
		this.enabled = enabled;
	}

	public String[] getUri() {
		return this.uri;
	}

	public void setUri(String[] url) {
		this.uri = url;
	}

	public String getName() {
		return this.name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getProfile() {
		return this.profile;
	}

	public void setProfile(String env) {
		this.profile = env;
	}

	public String getLabel() {
		return this.label;
	}

	public void setLabel(String label) {
		this.label = label;
	}

	public String getUsername() {
		return this.username;
	}

	public void setUsername(String username) {
		this.username = username;
	}

	public String getPassword() {
		return this.password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	public Credentials getCredentials(int index) {
		return extractCredentials(index);
	}

	public String getMediaType() {
		return this.mediaType;
	}

	public void setMediaType(String mediaType) {
		this.mediaType = mediaType;
	}

	public Discovery getDiscovery() {
		return this.discovery;
	}

	public void setDiscovery(Discovery discovery) {
		this.discovery = discovery;
	}

	public TlsProperties getTls() {
		return tls;
	}

	public void setTls(TlsProperties tls) {
		this.tls = tls;
	}

	@PostConstruct
	public void checkTlsStoreType() {
		tls.postConstruct();
	}

	public boolean isFailFast() {
		return this.failFast;
	}

	public void setFailFast(boolean failFast) {
		this.failFast = failFast;
	}

	public String getToken() {
		return this.token;
	}

	public void setToken(String token) {
		this.token = token;
	}

	public int getRequestReadTimeout() {
		return this.requestReadTimeout;
	}

	public void setRequestReadTimeout(int requestReadTimeout) {
		this.requestReadTimeout = requestReadTimeout;
	}

	public int getRequestConnectTimeout() {
		return this.requestConnectTimeout;
	}

	public void setRequestConnectTimeout(int requestConnectTimeout) {
		this.requestConnectTimeout = requestConnectTimeout;
	}

	public boolean isSendState() {
		return this.sendState;
	}

	public void setSendState(boolean sendState) {
		this.sendState = sendState;
	}

	public Map<String, String> getHeaders() {
		return this.headers;
	}

	public void setHeaders(Map<String, String> headers) {
		this.headers = headers;
	}

	private Credentials extractCredentials(int index) {
		Credentials result = new Credentials();
		int noOfUrl = this.uri.length;
		if (index < 0 || index >= noOfUrl) {
			throw new IllegalStateException("Trying to access an invalid array index");
		}
		String uri = this.uri[index];
		result.uri = uri;
		Credentials explicitCredentials = getUsernamePassword();
		result.username = explicitCredentials.username;
		result.password = explicitCredentials.password;
		try {
			URL url = new URL(uri);
			String userInfo = url.getUserInfo();
			// no credentials in url, return explicit credentials
			if (StringUtils.isEmpty(userInfo) || ":".equals(userInfo)) {
				return result;
			}
			String bare = UriComponentsBuilder.fromHttpUrl(uri).userInfo(null).build().toUriString();
			result.uri = bare;

			// if userInfo does not contain a :, then append a : to it
			if (!userInfo.contains(":")) {
				userInfo = userInfo + ":";
			}

			int sepIndex = userInfo.indexOf(":");
			// set username and password from uri
			result.username = userInfo.substring(0, sepIndex);
			result.password = userInfo.substring(sepIndex + 1);

			// override password if explicitly set
			if (explicitCredentials.password != null) {
				// Explicit username / password takes precedence
				result.password = explicitCredentials.password;
			}
			// override username if explicitly set
			if (!"user".equals(explicitCredentials.username)) {
				// But the username can be overridden
				result.username = explicitCredentials.username;
			}

			result.password = URLDecoder.decode(result.password, StandardCharsets.UTF_8.toString());
			result.username = URLDecoder.decode(result.username, StandardCharsets.UTF_8.toString());
			return result;
		}
		catch (MalformedURLException | UnsupportedEncodingException e) {
			throw new IllegalStateException("Invalid URL: " + uri, e);
		}
	}

	private Credentials getUsernamePassword() {
		Credentials credentials = new Credentials();

		if (StringUtils.hasText(this.password)) {
			credentials.password = this.password.trim();
		}

		if (StringUtils.hasText(this.username)) {
			credentials.username = this.username.trim();
		}
		else {
			credentials.username = "user";
		}
		return credentials;
	}

	public ConfigClientProperties override(org.springframework.core.env.Environment environment) {
		ConfigClientProperties override = new ConfigClientProperties();
		BeanUtils.copyProperties(this, override);
		override.setName(environment.resolvePlaceholders(NAME_PLACEHOLDER));
		if (environment.containsProperty(ConfigClientProperties.PREFIX + ".profile")) {
			override.setProfile(environment.getProperty(ConfigClientProperties.PREFIX + ".profile"));
		}
		if (environment.containsProperty(ConfigClientProperties.PREFIX + ".label")) {
			override.setLabel(environment.getProperty(ConfigClientProperties.PREFIX + ".label"));
		}
		return override;
	}

	@Override
	public String toString() {
		return "ConfigClientProperties [enabled=" + this.enabled + ", profile=" + this.profile + ", name=" + this.name
				+ ", label=" + this.label + ", username=" + this.username + ", password=" + this.password + ", uri="
				+ Arrays.toString(this.uri) + ", mediaType=" + this.mediaType + ", discovery=" + this.discovery
				+ ", failFast=" + this.failFast + ", token=" + this.token + ", requestConnectTimeout="
				+ this.requestConnectTimeout + ", requestReadTimeout=" + this.requestReadTimeout + ", sendState="
				+ this.sendState + ", headers=" + this.headers + "]";
	}

	/**
	 * Credentials properties.
	 */
	public static class Credentials {

		private String username;

		private String password;

		private String uri;

		public String getUsername() {
			return this.username;
		}

		public String getPassword() {
			return this.password;
		}

		public String getUri() {
			return this.uri;
		}

	}

	/**
	 * Discovery properties.
	 */
	public static class Discovery {

		/**
		 * Default config server service id name.
		 */
		public static final String DEFAULT_CONFIG_SERVER = "configserver";

		/**
		 * Flag to indicate that config server discovery is enabled (config server URL
		 * will be looked up via discovery).
		 */
		private boolean enabled;

		/**
		 * Service id to locate config server.
		 */
		private String serviceId = DEFAULT_CONFIG_SERVER;

		public boolean isEnabled() {
			return this.enabled;
		}

		public void setEnabled(boolean enabled) {
			this.enabled = enabled;
		}

		public String getServiceId() {
			return this.serviceId;
		}

		public void setServiceId(String serviceId) {
			this.serviceId = serviceId;
		}

	}

}
