/*
 * Copyright 2015-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.server.support;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.UrlResource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.util.FileSystemUtils;
import org.springframework.util.StringUtils;

/**
 * Base class for components that want to access a source control management system.
 *
 * @author Dave Syer
 */
public abstract class AbstractScmAccessor implements ResourceLoaderAware {

	protected Log logger = LogFactory.getLog(getClass());

	/**
	 * Base directory for local working copy of repository.
	 */
	private File basedir;

	/**
	 * URI of remote repository.
	 */
	private String uri;

	private ConfigurableEnvironment environment;

	/**
	 * Username for authentication with remote repository.
	 */
	private String username;

	/**
	 * Password for authentication with remote repository.
	 */
	private String password;

	/**
	 * Passphrase for unlocking your ssh private key.
	 */
	private String passphrase;

	/**
	 * Reject incoming SSH host keys from remote servers not in the known host list.
	 */
	private boolean strictHostKeyChecking;

	/**
	 * Search paths to use within local working copy. By default searches only the root.
	 */
	private String[] searchPaths;

	private ResourceLoader resourceLoader = new DefaultResourceLoader();

	public AbstractScmAccessor(ConfigurableEnvironment environment) {
		this.environment = environment;
		this.basedir = createBaseDir();
	}

	public AbstractScmAccessor(ConfigurableEnvironment environment, AbstractScmAccessorProperties properties) {
		this.environment = environment;
		this.setBasedir(properties.getBasedir() == null ? createBaseDir() : properties.getBasedir());
		this.passphrase = properties.getPassphrase();
		this.password = properties.getPassword();
		this.searchPaths = properties.getSearchPaths();
		this.strictHostKeyChecking = properties.isStrictHostKeyChecking();
		this.uri = properties.getUri();
		this.username = properties.getUsername();
	}

	@Override
	public void setResourceLoader(ResourceLoader resourceLoader) {
		this.resourceLoader = resourceLoader;
	}

	protected File createBaseDir() {
		try {
			final Path basedir = Files.createTempDirectory("config-repo-");
			Runtime.getRuntime().addShutdownHook(new Thread() {
				@Override
				public void run() {
					try {
						FileSystemUtils.deleteRecursively(basedir);
					}
					catch (IOException e) {
						AbstractScmAccessor.this.logger.warn("Failed to delete temporary directory on exit: " + e);
					}
				}
			});
			return basedir.toFile();
		}
		catch (IOException e) {
			throw new IllegalStateException("Cannot create temp dir", e);
		}
	}

	public ConfigurableEnvironment getEnvironment() {
		return this.environment;
	}

	public void setEnvironment(ConfigurableEnvironment environment) {
		this.environment = environment;
	}

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

	public void setUri(String uri) {
		while (uri.endsWith("/")) {
			uri = uri.substring(0, uri.length() - 1);
		}
		int index = uri.indexOf("://");
		if (index > 0 && !uri.substring(index + "://".length()).contains("/")) {
			// If there's no context path add one
			uri = uri + "/";
		}
		this.uri = uri;
	}

	public File getBasedir() {
		return this.basedir;
	}

	public void setBasedir(File basedir) {
		this.basedir = basedir.getAbsoluteFile();
	}

	public String[] getSearchPaths() {
		return this.searchPaths;
	}

	public void setSearchPaths(String... searchPaths) {
		this.searchPaths = searchPaths;
	}

	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 String getPassphrase() {
		return this.passphrase;
	}

	public void setPassphrase(String passphrase) {
		this.passphrase = passphrase;
	}

	public boolean isStrictHostKeyChecking() {
		return this.strictHostKeyChecking;
	}

	public void setStrictHostKeyChecking(boolean strictHostKeyChecking) {
		this.strictHostKeyChecking = strictHostKeyChecking;
	}

	protected File getWorkingDirectory() {
		if (this.uri.startsWith("file:")) {
			try {
				return new UrlResource(StringUtils.cleanPath(this.uri)).getFile();
			}
			catch (Exception e) {
				throw new IllegalStateException("Cannot convert uri to file: " + this.uri);
			}
		}
		return this.basedir;
	}

	protected String[] getSearchLocations(File dir, String application, String profile, String label) {
		String[] locations = this.searchPaths;
		if (locations == null || locations.length == 0) {
			locations = AbstractScmAccessorProperties.DEFAULT_LOCATIONS;
		}
		else if (!Arrays.equals(locations, AbstractScmAccessorProperties.DEFAULT_LOCATIONS)) {
			locations = StringUtils.concatenateStringArrays(AbstractScmAccessorProperties.DEFAULT_LOCATIONS, locations);
		}
		Collection<String> output = new LinkedHashSet<String>();
		for (String location : locations) {
			String[] profiles = new String[] { profile };
			if (profile != null) {
				profiles = StringUtils.commaDelimitedListToStringArray(profile);
			}
			String[] apps = new String[] { application };
			if (application != null) {
				apps = StringUtils.commaDelimitedListToStringArray(application);
			}
			for (String prof : profiles) {
				for (String app : apps) {
					String value = location;
					if (app != null) {
						value = value.replace("{application}", app);
					}
					if (prof != null) {
						value = value.replace("{profile}", prof);
					}
					if (label != null) {
						value = value.replace("{label}", label);
					}
					if (!value.endsWith("/")) {
						value = value + "/";
					}
					output.addAll(matchingDirectories(dir, value));
				}
			}
		}
		return output.toArray(new String[0]);
	}

	private List<String> matchingDirectories(File dir, String value) {
		List<String> output = new ArrayList<String>();
		try {
			PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(this.resourceLoader);
			String path = new File(dir, value).toURI().toString();
			for (Resource resource : resolver.getResources(path)) {
				if (resource.getFile().isDirectory()) {
					output.add(resource.getURI().toString());
				}
			}
		}
		catch (IOException e) {
		}
		return output;
	}

}
