/*
 * Copyright 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 io.spring.start.site;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import io.spring.initializr.metadata.BillOfMaterials;
import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.collection.CollectRequest;
import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory;
import org.eclipse.aether.graph.Dependency;
import org.eclipse.aether.impl.DefaultServiceLocator;
import org.eclipse.aether.internal.impl.DefaultRepositorySystem;
import org.eclipse.aether.repository.LocalRepository;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.repository.RemoteRepository.Builder;
import org.eclipse.aether.repository.RepositoryPolicy;
import org.eclipse.aether.resolution.ArtifactDescriptorException;
import org.eclipse.aether.resolution.ArtifactDescriptorRequest;
import org.eclipse.aether.resolution.ArtifactResult;
import org.eclipse.aether.resolution.DependencyRequest;
import org.eclipse.aether.resolution.DependencyResolutionException;
import org.eclipse.aether.resolution.DependencyResult;
import org.eclipse.aether.spi.connector.RepositoryConnectorFactory;
import org.eclipse.aether.spi.connector.transport.GetTask;
import org.eclipse.aether.spi.connector.transport.PeekTask;
import org.eclipse.aether.spi.connector.transport.PutTask;
import org.eclipse.aether.spi.connector.transport.Transporter;
import org.eclipse.aether.spi.connector.transport.TransporterFactory;
import org.eclipse.aether.spi.locator.ServiceLocator;
import org.eclipse.aether.transfer.NoTransporterException;
import org.eclipse.aether.transport.http.HttpTransporterFactory;
import org.eclipse.aether.util.artifact.JavaScopes;
import org.eclipse.aether.util.filter.DependencyFilterUtils;
import org.eclipse.aether.util.repository.SimpleArtifactDescriptorPolicy;

import org.springframework.util.FileSystemUtils;

final class DependencyResolver {

	private static final Collection<DependencyResolver> instances = new ArrayList<>();

	private static final ThreadLocal<DependencyResolver> instanceForThread = ThreadLocal.withInitial(() -> {
		DependencyResolver instance = new DependencyResolver();
		instances.add(instance);
		return instance;
	});

	private static final RepositoryPolicy repositoryPolicy = new RepositoryPolicy(true,
			RepositoryPolicy.UPDATE_POLICY_NEVER, RepositoryPolicy.CHECKSUM_POLICY_IGNORE);

	static final RemoteRepository mavenCentral = createRemoteRepository("central", "https://repo1.maven.org/maven2",
			false);

	private static final Map<String, List<Dependency>> managedDependencies = new ConcurrentHashMap<>();

	private final Path localRepositoryLocation;

	private final RepositorySystemSession repositorySystemSession;

	private final RepositorySystem repositorySystem;

	DependencyResolver() {
		try {
			ServiceLocator serviceLocator = createServiceLocator();
			DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession();
			session.setArtifactDescriptorPolicy(new SimpleArtifactDescriptorPolicy(false, false));
			this.localRepositoryLocation = Files.createTempDirectory("metadata-validation-m2");
			LocalRepository localRepository = new LocalRepository(this.localRepositoryLocation.toFile());
			this.repositorySystem = serviceLocator.getService(RepositorySystem.class);
			session.setLocalRepositoryManager(
					this.repositorySystem.newLocalRepositoryManager(session, localRepository));
			session.setReadOnly();
			this.repositorySystemSession = session;
		}
		catch (Exception ex) {
			throw new RuntimeException(ex);
		}
	}

	static RemoteRepository createRemoteRepository(String id, String url, boolean snapshot) {
		Builder repositoryBuilder = new Builder(id, "default", url);
		if (snapshot) {
			repositoryBuilder.setSnapshotPolicy(repositoryPolicy);
		}
		else {
			repositoryBuilder.setReleasePolicy(repositoryPolicy);
		}
		return repositoryBuilder.build();
	}

	static List<String> resolveDependencies(String groupId, String artifactId, String version,
			List<BillOfMaterials> boms, List<RemoteRepository> repositories) {
		DependencyResolver instance = instanceForThread.get();
		List<Dependency> managedDependencies = instance.getManagedDependencies(boms, repositories);
		Dependency aetherDependency = new Dependency(new DefaultArtifact(groupId, artifactId, "pom",
				instance.getVersion(groupId, artifactId, version, managedDependencies)), "compile");
		CollectRequest collectRequest = new CollectRequest((org.eclipse.aether.graph.Dependency) null,
				Collections.singletonList(aetherDependency), repositories);
		collectRequest.setManagedDependencies(managedDependencies);
		DependencyRequest dependencyRequest = new DependencyRequest(collectRequest,
				DependencyFilterUtils.classpathFilter(JavaScopes.COMPILE, JavaScopes.RUNTIME));
		try {
			return instance.resolveDependencies(dependencyRequest).getArtifactResults().stream()
					.map(ArtifactResult::getArtifact)
					.map((artifact) -> artifact.getGroupId() + ":" + artifact.getArtifactId())
					.collect(Collectors.toList());
		}
		catch (DependencyResolutionException ex) {
			throw new RuntimeException(ex);
		}
	}

	static void cleanUp() {
		instances.forEach(DependencyResolver::deleteLocalRepository);
	}

	void deleteLocalRepository() {
		try {
			FileSystemUtils.deleteRecursively(this.localRepositoryLocation);
		}
		catch (IOException ex) {
			// Continue
		}
	}

	private List<Dependency> getManagedDependencies(List<BillOfMaterials> boms, List<RemoteRepository> repositories) {
		return boms.stream().flatMap(
				(bom) -> getManagedDependencies(bom.getGroupId(), bom.getArtifactId(), bom.getVersion(), repositories))
				.collect(Collectors.toList());
	}

	private Stream<Dependency> getManagedDependencies(String groupId, String artifactId, String version,
			List<RemoteRepository> repositories) {
		String key = groupId + ":" + artifactId + ":" + version;
		List<org.eclipse.aether.graph.Dependency> managedDependencies = DependencyResolver.managedDependencies
				.computeIfAbsent(key,
						(coords) -> resolveManagedDependencies(groupId, artifactId, version, repositories));
		return managedDependencies.stream();
	}

	private List<org.eclipse.aether.graph.Dependency> resolveManagedDependencies(String groupId, String artifactId,
			String version, List<RemoteRepository> repositories) {
		try {
			return this.repositorySystem
					.readArtifactDescriptor(this.repositorySystemSession, new ArtifactDescriptorRequest(
							new DefaultArtifact(groupId, artifactId, "pom", version), repositories, null))
					.getManagedDependencies();
		}
		catch (ArtifactDescriptorException ex) {
			throw new RuntimeException(ex);
		}
	}

	private DependencyResult resolveDependencies(DependencyRequest dependencyRequest)
			throws DependencyResolutionException {
		DependencyResult resolved = this.repositorySystem.resolveDependencies(this.repositorySystemSession,
				dependencyRequest);
		return resolved;
	}

	private String getVersion(String groupId, String artifactId, String version,
			List<org.eclipse.aether.graph.Dependency> managedDependencies) {
		if (version != null) {
			return version;
		}
		for (org.eclipse.aether.graph.Dependency managedDependency : managedDependencies) {
			if (groupId.equals(managedDependency.getArtifact().getGroupId())
					&& artifactId.equals(managedDependency.getArtifact().getArtifactId())) {
				return managedDependency.getArtifact().getVersion();
			}
		}
		return null;
	}

	private static ServiceLocator createServiceLocator() {
		DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator();
		locator.addService(RepositorySystem.class, DefaultRepositorySystem.class);
		locator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class);
		locator.addService(TransporterFactory.class, DependencyResolver.JarSkippingHttpTransporterFactory.class);
		return locator;
	}

	private static class JarSkippingHttpTransporterFactory implements TransporterFactory {

		private final HttpTransporterFactory delegate = new HttpTransporterFactory();

		@Override
		public Transporter newInstance(RepositorySystemSession session, RemoteRepository repository)
				throws NoTransporterException {
			return new JarGetSkippingTransporter(this.delegate.newInstance(session, repository));
		}

		@Override
		public float getPriority() {
			return 5.0f;
		}

		private static final class JarGetSkippingTransporter implements Transporter {

			private final Transporter delegate;

			private JarGetSkippingTransporter(Transporter delegate) {
				this.delegate = delegate;
			}

			@Override
			public int classify(Throwable error) {
				return this.delegate.classify(error);
			}

			@Override
			public void peek(PeekTask task) throws Exception {
				this.delegate.peek(task);
			}

			@Override
			public void get(GetTask task) throws Exception {
				if (task.getLocation().getPath().endsWith(".jar")) {
					return;
				}
				this.delegate.get(task);
			}

			@Override
			public void put(PutTask task) throws Exception {
				this.delegate.put(task);
			}

			@Override
			public void close() {
				this.delegate.close();
			}

		}

	}

}