/*
 * Copyright 2012-2015 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
 *
 *      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.springframework.boot.cli.compiler.grape;

import java.io.File;
import java.lang.reflect.Field;
import java.util.List;

import org.apache.maven.settings.Mirror;
import org.apache.maven.settings.Proxy;
import org.apache.maven.settings.Server;
import org.apache.maven.settings.Settings;
import org.apache.maven.settings.building.DefaultSettingsBuilderFactory;
import org.apache.maven.settings.building.DefaultSettingsBuildingRequest;
import org.apache.maven.settings.building.SettingsBuildingException;
import org.apache.maven.settings.building.SettingsBuildingRequest;
import org.apache.maven.settings.crypto.DefaultSettingsDecrypter;
import org.apache.maven.settings.crypto.DefaultSettingsDecryptionRequest;
import org.apache.maven.settings.crypto.SettingsDecrypter;
import org.apache.maven.settings.crypto.SettingsDecryptionResult;
import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.repository.Authentication;
import org.eclipse.aether.repository.AuthenticationSelector;
import org.eclipse.aether.repository.LocalRepository;
import org.eclipse.aether.repository.MirrorSelector;
import org.eclipse.aether.repository.ProxySelector;
import org.eclipse.aether.util.repository.AuthenticationBuilder;
import org.eclipse.aether.util.repository.ConservativeAuthenticationSelector;
import org.eclipse.aether.util.repository.DefaultAuthenticationSelector;
import org.eclipse.aether.util.repository.DefaultMirrorSelector;
import org.eclipse.aether.util.repository.DefaultProxySelector;
import org.sonatype.plexus.components.cipher.DefaultPlexusCipher;
import org.sonatype.plexus.components.cipher.PlexusCipherException;
import org.sonatype.plexus.components.sec.dispatcher.DefaultSecDispatcher;

import org.springframework.boot.cli.util.Log;

/**
 * Auto-configuration for a RepositorySystemSession that uses Maven's settings.xml to
 * determine the configuration settings
 *
 * @author Andy Wilkinson
 */
public class SettingsXmlRepositorySystemSessionAutoConfiguration
		implements RepositorySystemSessionAutoConfiguration {

	private final String homeDir;

	public SettingsXmlRepositorySystemSessionAutoConfiguration() {
		this(System.getProperty("user.home"));
	}

	SettingsXmlRepositorySystemSessionAutoConfiguration(String homeDir) {
		this.homeDir = homeDir;
	}

	@Override
	public void apply(DefaultRepositorySystemSession session,
			RepositorySystem repositorySystem) {

		Settings settings = loadSettings();
		SettingsDecryptionResult decryptionResult = decryptSettings(settings);
		if (!decryptionResult.getProblems().isEmpty()) {
			Log.error(
					"Maven settings decryption failed. Some Maven repositories may be inaccessible");
			// Continue - the encrypted credentials may not be used
		}

		session.setOffline(settings.isOffline());
		session.setMirrorSelector(createMirrorSelector(settings));
		session.setAuthenticationSelector(
				createAuthenticationSelector(decryptionResult.getServers()));
		session.setProxySelector(createProxySelector(decryptionResult.getProxies()));

		String localRepository = settings.getLocalRepository();
		if (localRepository != null) {
			session.setLocalRepositoryManager(repositorySystem.newLocalRepositoryManager(
					session, new LocalRepository(localRepository)));
		}
	}

	private Settings loadSettings() {
		File settingsFile = new File(this.homeDir, ".m2/settings.xml");
		SettingsBuildingRequest request = new DefaultSettingsBuildingRequest();
		request.setUserSettingsFile(settingsFile);
		request.setSystemProperties(System.getProperties());
		try {
			return new DefaultSettingsBuilderFactory().newInstance().build(request)
					.getEffectiveSettings();
		}
		catch (SettingsBuildingException ex) {
			throw new IllegalStateException(
					"Failed to build settings from " + settingsFile, ex);
		}
	}

	private SettingsDecryptionResult decryptSettings(Settings settings) {
		DefaultSettingsDecryptionRequest request = new DefaultSettingsDecryptionRequest(
				settings);

		return createSettingsDecrypter().decrypt(request);
	}

	private SettingsDecrypter createSettingsDecrypter() {
		SettingsDecrypter settingsDecrypter = new DefaultSettingsDecrypter();
		setField(DefaultSettingsDecrypter.class, "securityDispatcher", settingsDecrypter,
				new SpringBootSecDispatcher());
		return settingsDecrypter;
	}

	private void setField(Class<?> clazz, String fieldName, Object target, Object value) {
		try {
			Field field = clazz.getDeclaredField(fieldName);
			field.setAccessible(true);
			field.set(target, value);
		}
		catch (Exception e) {
			throw new IllegalStateException(
					"Failed to set field '" + fieldName + "' on '" + target + "'", e);
		}
	}

	private MirrorSelector createMirrorSelector(Settings settings) {
		DefaultMirrorSelector selector = new DefaultMirrorSelector();
		for (Mirror mirror : settings.getMirrors()) {
			selector.add(mirror.getId(), mirror.getUrl(), mirror.getLayout(), false,
					mirror.getMirrorOf(), mirror.getMirrorOfLayouts());
		}
		return selector;
	}

	private AuthenticationSelector createAuthenticationSelector(List<Server> servers) {
		DefaultAuthenticationSelector selector = new DefaultAuthenticationSelector();
		for (Server server : servers) {
			AuthenticationBuilder auth = new AuthenticationBuilder();
			auth.addUsername(server.getUsername()).addPassword(server.getPassword());
			auth.addPrivateKey(server.getPrivateKey(), server.getPassphrase());
			selector.add(server.getId(), auth.build());
		}
		return new ConservativeAuthenticationSelector(selector);
	}

	private ProxySelector createProxySelector(List<Proxy> proxies) {
		DefaultProxySelector selector = new DefaultProxySelector();
		for (Proxy proxy : proxies) {
			Authentication authentication = new AuthenticationBuilder()
					.addUsername(proxy.getUsername()).addPassword(proxy.getPassword())
					.build();
			selector.add(
					new org.eclipse.aether.repository.Proxy(proxy.getProtocol(),
							proxy.getHost(), proxy.getPort(), authentication),
					proxy.getNonProxyHosts());
		}
		return selector;
	}

	private class SpringBootSecDispatcher extends DefaultSecDispatcher {

		public SpringBootSecDispatcher() {
			this._configurationFile = new File(
					SettingsXmlRepositorySystemSessionAutoConfiguration.this.homeDir,
					".m2/settings-security.xml").getAbsolutePath();
			try {
				this._cipher = new DefaultPlexusCipher();
			}
			catch (PlexusCipherException e) {
				throw new IllegalStateException(e);
			}
		}
	}
}
