package aQute.bnd.deployer.repository.aether;

import static aQute.bnd.deployer.repository.RepoConstants.DEFAULT_CACHE_DIR;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import org.apache.maven.repository.internal.DefaultArtifactDescriptorReader;
import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory;
import org.eclipse.aether.deployment.DeployRequest;
import org.eclipse.aether.impl.ArtifactDescriptorReader;
import org.eclipse.aether.impl.DefaultServiceLocator;
import org.eclipse.aether.impl.DefaultServiceLocator.ErrorHandler;
import org.eclipse.aether.repository.LocalRepository;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.repository.RemoteRepository.Builder;
import org.eclipse.aether.resolution.ArtifactRequest;
import org.eclipse.aether.resolution.ArtifactResolutionException;
import org.eclipse.aether.resolution.ArtifactResult;
import org.eclipse.aether.resolution.VersionRangeRequest;
import org.eclipse.aether.resolution.VersionRangeResult;
import org.eclipse.aether.spi.connector.RepositoryConnectorFactory;
import org.eclipse.aether.spi.connector.transport.TransporterFactory;
import org.eclipse.aether.transfer.AbstractTransferListener;
import org.eclipse.aether.transfer.TransferCancelledException;
import org.eclipse.aether.transfer.TransferEvent;
import org.eclipse.aether.transfer.TransferEvent.RequestType;
import org.eclipse.aether.transfer.TransferResource;
import org.eclipse.aether.transport.file.FileTransporterFactory;
import org.eclipse.aether.transport.http.HttpTransporterFactory;
import org.eclipse.aether.util.repository.AuthenticationBuilder;

import aQute.bnd.deployer.repository.FixedIndexedRepo;
import aQute.bnd.osgi.Jar;
import aQute.bnd.service.IndexProvider;
import aQute.bnd.service.Plugin;
import aQute.bnd.service.Registry;
import aQute.bnd.service.RegistryPlugin;
import aQute.bnd.service.RepositoryPlugin;
import aQute.bnd.service.ResolutionPhase;
import aQute.bnd.version.MavenVersion;
import aQute.bnd.version.Version;
import aQute.lib.io.IO;
import aQute.service.reporter.Reporter;

@aQute.bnd.annotation.plugin.BndPlugin(name = "aether", parameters = AetherRepository.Config.class)
public class AetherRepository implements Plugin, RegistryPlugin, RepositoryPlugin, IndexProvider {

	interface Config {
		String name();

		URI url();

		URI indexUrl();

		String username();

		String password();

		String cache();
	}

	public static final String	PROP_NAME		= "name";
	public static final String	PROP_MAIN_URL	= "url";
	public static final String	PROP_INDEX_URL	= "indexUrl";
	public static final String	PROP_USERNAME	= "username";
	public static final String	PROP_PASSWORD	= "password";
	public static final String	PROP_CACHE		= "cache";

	private static final String	META_OBR		= ".meta/obr.xml";

	Reporter					reporter;
	private Registry			registry;

	// Config Properties
	private String				name			= this.getClass().getSimpleName();
	private URI					mainUri;
	private URI					indexUri;
	private File				cacheDir		= new File(
			System.getProperty("user.home") + File.separator + DEFAULT_CACHE_DIR);
	private String				username		= null;
	private String				password		= "";

	// Initialisation Fields
	private boolean				initialised;
	private RepositorySystem	repoSystem;
	private RemoteRepository	remoteRepo;
	private LocalRepository		localRepo;
	private FixedIndexedRepo	indexedRepo;

	@Override
	public void setReporter(Reporter reporter) {
		this.reporter = reporter;
	}

	@Override
	public void setRegistry(Registry registry) {
		this.registry = registry;
	}

	@Override
	public void setProperties(Map<String,String> props) throws Exception {
		// Read name property
		if (props.containsKey(PROP_NAME))
			name = props.get(PROP_NAME);

		// Read main Nexus URL property
		String mainUrlStr = props.get(PROP_MAIN_URL);
		if (mainUrlStr == null)
			throw new IllegalArgumentException(
					String.format("Attribute '%s' must be set on '%s' plugin.", PROP_MAIN_URL, getClass().getName()));
		try {
			if (mainUrlStr.endsWith("/"))
				mainUrlStr = mainUrlStr.substring(0, mainUrlStr.length() - 1);
			mainUri = new URI(mainUrlStr);
		} catch (URISyntaxException e) {
			throw new IllegalArgumentException(String.format("Invalid '%s' property in plugin %s", PROP_MAIN_URL), e);
		}

		// Read index URL property if present
		String indexUriStr = props.get(PROP_INDEX_URL);
		if (indexUriStr == null) {
			indexUri = findDefaultVirtualIndexUri(mainUri);
		} else {
			try {
				indexUri = new URI(indexUriStr);
			} catch (URISyntaxException e) {
				throw new IllegalArgumentException(String.format("Invalid '%s' property in plugin %s", PROP_INDEX_URL),
						e);
			}
		}

		// Read username and password
		if (props.containsKey(PROP_USERNAME))
			username = props.get(PROP_USERNAME);
		if (props.containsKey(PROP_PASSWORD))
			password = props.get(PROP_PASSWORD);

		// Read cache path property
		String cachePath = props.get(PROP_CACHE);
		if (cachePath != null) {
			cacheDir = new File(cachePath);
			if (!cacheDir.isDirectory()) {
				String canonicalPath;
				try {
					canonicalPath = cacheDir.getCanonicalPath();
				} catch (IOException e) {
					throw new IllegalArgumentException(
							String.format("Could not canonical path for cacheDir '%s'.", cachePath), e);
				}
				throw new IllegalArgumentException(
						String.format("Cache path '%s' does not exist, or is not a directory", canonicalPath));
			}
		}
	}

	protected final synchronized void init() throws Exception {
		if (initialised)
			return;

		// Initialise Aether
		DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator();
		locator.addService(ArtifactDescriptorReader.class, DefaultArtifactDescriptorReader.class);
		locator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class);
		locator.addService(TransporterFactory.class, FileTransporterFactory.class);
		locator.addService(TransporterFactory.class, HttpTransporterFactory.class);
		locator.setErrorHandler(new ErrorHandler() {
			@Override
			public void serviceCreationFailed(Class< ? > type, Class< ? > impl, Throwable exception) {
				if (reporter != null)
					reporter.error("Service creation failed for type %s using impl %s: %s", type, impl,
							exception.getLocalizedMessage());
				exception.printStackTrace();
			}
		});
		repoSystem = locator.getService(RepositorySystem.class);
		if (repoSystem == null)
			throw new IllegalArgumentException("Failed to initialise Aether repository system");

		Builder builder = new RemoteRepository.Builder("remote", "default", mainUri.toString());
		if (username != null) {
			AuthenticationBuilder authBuilder = new AuthenticationBuilder().addUsername(username);
			if (password != null)
				authBuilder.addPassword(password);
			builder.setAuthentication(authBuilder.build());
		}
		remoteRepo = builder.build();
		localRepo = new LocalRepository(new File(cacheDir, "aether-local"));

		// Initialise Index
		if (indexUri == null) {
			indexedRepo = null;
		} else {
			// Test whether the index URI exists and is available.
			HttpURLConnection connection = (HttpURLConnection) indexUri.toURL().openConnection();
			try {
				connection.setRequestMethod("HEAD");
				int responseCode = connection.getResponseCode();
				if (responseCode >= 400) {
					indexedRepo = null;
				} else {
					indexedRepo = new FixedIndexedRepo();
					Map<String,String> config = new HashMap<String,String>();
					indexedRepo.setReporter(this.reporter);
					indexedRepo.setRegistry(registry);

					config.put(FixedIndexedRepo.PROP_CACHE, cacheDir.getAbsolutePath());
					config.put(FixedIndexedRepo.PROP_LOCATIONS, indexUri.toString());
					indexedRepo.setProperties(config);
				}
			} catch (UnknownHostException e) {
				return;
			} finally {
				connection.disconnect();
			}
		}

		initialised = true;
	}

	@Override
	public PutResult put(InputStream stream, PutOptions options) throws Exception {
		init();

		DigestInputStream digestStream = new DigestInputStream(stream, MessageDigest.getInstance("SHA-1"));
		final File tmpFile = IO.createTempFile(cacheDir, "put", ".bnd");
		try {
			IO.copy(digestStream, tmpFile);
			byte[] digest = digestStream.getMessageDigest().digest();

			if (options.digest != null && !Arrays.equals(options.digest, digest))
				throw new IOException("Retrieved artifact digest doesn't match specified digest");

			// Get basic info about the bundle we're deploying
			Jar jar = new Jar(tmpFile);
			Artifact artifact = ConversionUtils.fromBundleJar(jar);
			artifact = artifact.setFile(tmpFile);

			// Setup the Aether repo session and create the deployment request
			DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession();
			session.setLocalRepositoryManager(repoSystem.newLocalRepositoryManager(session, localRepo));
			final DeployRequest request = new DeployRequest();
			request.addArtifact(artifact);
			request.setRepository(remoteRepo);

			// Capture the result including remote resource URI
			final ResultHolder resultHolder = new ResultHolder();
			session.setTransferListener(new AbstractTransferListener() {
				@Override
				public void transferSucceeded(TransferEvent event) {
					TransferResource resource = event.getResource();
					if (event.getRequestType() == RequestType.PUT && tmpFile.equals(resource.getFile())) {
						PutResult result = new PutResult();
						result.artifact = URI.create(resource.getRepositoryUrl() + resource.getResourceName());
						resultHolder.result = result;
						System.out.println(
								"UPLOADED to: " + URI.create(resource.getRepositoryUrl() + resource.getResourceName()));
					}
				}

				@Override
				public void transferFailed(TransferEvent event) {
					if (event.getRequestType() == RequestType.PUT && tmpFile.equals(event.getResource().getFile()))
						resultHolder.error = event.getException();
				}

				@Override
				public void transferCorrupted(TransferEvent event) throws TransferCancelledException {
					if (event.getRequestType() == RequestType.PUT && tmpFile.equals(event.getResource().getFile()))
						resultHolder.error = event.getException();
				}
			});

			// Do the deploy and report results
			repoSystem.deploy(session, request);

			if (resultHolder.result != null) {
				if (indexedRepo != null)
					indexedRepo.reset();

				return resultHolder.result;
			} else if (resultHolder.error != null) {
				throw new Exception("Error during artifact upload", resultHolder.error);
			} else {
				throw new Exception("Artifact was not uploaded");
			}
		} finally {
			if (tmpFile != null && tmpFile.isFile())
				IO.delete(tmpFile);
		}
	}

	@Override
	public List<String> list(String pattern) throws Exception {
		init();

		// only supported with a valid index
		return indexedRepo != null ? indexedRepo.list(pattern) : null;
	}

	@Override
	public SortedSet<Version> versions(String bsn) throws Exception {
		init();

		// Use the index by preference
		if (indexedRepo != null)
			return indexedRepo.versions(ConversionUtils.maybeMavenCoordsToBsn(bsn));

		Artifact artifact = null;

		try {
			artifact = new DefaultArtifact(bsn + ":[0,)");
		} catch (Exception e) {
			// ignore non-GAV style dependencies
		}

		if (artifact == null)
			return null;

		// Setup the Aether repo session and create the range request
		DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession();
		session.setLocalRepositoryManager(repoSystem.newLocalRepositoryManager(session, localRepo));

		VersionRangeRequest rangeRequest = new VersionRangeRequest();
		rangeRequest.setArtifact(artifact);
		rangeRequest.setRepositories(Collections.singletonList(remoteRepo));

		// Resolve the range
		VersionRangeResult rangeResult = repoSystem.resolveVersionRange(session, rangeRequest);

		// Add to the result
		SortedSet<Version> versions = new TreeSet<Version>();
		for (org.eclipse.aether.version.Version version : rangeResult.getVersions()) {
			try {
				versions.add(MavenVersion.parseString(version.toString()).getOSGiVersion());
			} catch (IllegalArgumentException e) {
				// ignore version
			}
		}
		return versions;
	}

	@Override
	public File get(String bsn, Version version, Map<String,String> properties, DownloadListener... listeners)
			throws Exception {
		init();

		// Use the index by preference
		if (indexedRepo != null)
			return indexedRepo.get(ConversionUtils.maybeMavenCoordsToBsn(bsn), version, properties, listeners);

		File file = null;
		boolean getSource = false;
		try {
			// Setup the Aether repo session and request
			DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession();
			session.setLocalRepositoryManager(repoSystem.newLocalRepositoryManager(session, localRepo));

			if (bsn.endsWith(".source")) {
				String originalBsn = properties.get("bsn");

				if (originalBsn != null) {
					bsn = originalBsn;
					getSource = true;
				}
			}

			String[] coords = ConversionUtils.getGroupAndArtifactForBsn(bsn);

			MavenVersion mvnVersion = new MavenVersion(version);

			String versionStr = null;

			if ("exact".equals(properties.get("strategy")) || getSource) {
				versionStr = properties.get("version");
			} else {
				versionStr = mvnVersion.toString();
			}

			Artifact artifact = null;

			if (getSource) {
				artifact = new DefaultArtifact(coords[0], coords[1], "sources", "jar", versionStr);
			} else {
				artifact = new DefaultArtifact(coords[0], coords[1], "jar", versionStr);
			}

			ArtifactRequest request = new ArtifactRequest();
			request.setArtifact(artifact);
			request.setRepositories(Collections.singletonList(remoteRepo));

			// Log the transfer
			session.setTransferListener(new AbstractTransferListener() {
				@Override
				public void transferStarted(TransferEvent event) throws TransferCancelledException {
					System.err.println(event);
				}

				@Override
				public void transferSucceeded(TransferEvent event) {
					System.err.println(event);
				}

				@Override
				public void transferFailed(TransferEvent event) {
					System.err.println(event);
				}
			});

			try {
				// Resolve the version
				ArtifactResult artifactResult = repoSystem.resolveArtifact(session, request);
				artifact = artifactResult.getArtifact();
				file = artifact.getFile();
			} catch (ArtifactResolutionException ex) {
				// could not download artifact, simply return null
			}

			return file;
		} finally {
			for (DownloadListener dl : listeners) {
				if (file != null)
					dl.success(file);
				else
					dl.failure(null, "Download failed");
			}
		}
	}

	@Override
	public boolean canWrite() {
		return true;
	}

	@Override
	public String getName() {
		return name;
	}

	@Override
	public String getLocation() {
		return "http://localhost:8080/nexus/content/repositories/scratch1";
	}

	/**
	 * <p>
	 * Find the default URI of the OBR index for a hosted repository, assuming
	 * that the OBR plugin is used to generate a Virtual repository with the
	 * same name as the referenced repository with the addition of "-obr" to its
	 * name.
	 * </p>
	 * <p>
	 * For example suppose there is a hosted repository with the identity
	 * "releases"; it will have the URI {@code
	 * http://hostname/nexus/content/repositories/releases}. We assume there is
	 * a Virtual OBR repository with ID "releases-obr". It will have the URL
	 * {@code http://hostname/nexus/content/repositories/releases-obr}, and the
	 * OBR index will be at {@code
	 * http://hostname/nexus/content/repositories/releases-obr/.meta/obr.xml} .
	 * 
	 * @param hostedUri The URI of the source Hosted repository.
	 */
	private static URI findDefaultVirtualIndexUri(URI hostedUri) {
		StringBuilder sb = new StringBuilder();
		sb.append(hostedUri.getScheme());
		sb.append("://");
		sb.append(hostedUri.getHost());

		if (hostedUri.getPort() > 0)
			sb.append(":" + hostedUri.getPort());

		String path = hostedUri.getPath();

		if (path != null && path.endsWith("/"))
			path = path.substring(0, path.length() - 1);

		if (path == null || path.length() == 0)
			sb.append("/");
		else
			sb.append(path);

		return URI.create(sb.toString() + "-obr/" + META_OBR);
	}

	@Override
	public List<URI> getIndexLocations() throws Exception {
		init();
		return indexedRepo != null ? indexedRepo.getIndexLocations() : Collections.<URI> emptyList();
	}

	@Override
	public Set<ResolutionPhase> getSupportedPhases() {
		return EnumSet.allOf(ResolutionPhase.class);
	}

	static class ResultHolder {
		RepositoryPlugin.PutResult	result;
		Exception					error;
	}

}
