/*******************************************************************************
 * Copyright (c) 2016, 2020 Red Hat.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Red Hat - Initial Contribution
 *******************************************************************************/
package org.eclipse.linuxtools.docker.core;

import static javax.ws.rs.HttpMethod.GET;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;

import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;

import org.eclipse.linuxtools.internal.docker.core.DockerImageSearchResult;
import org.eclipse.linuxtools.internal.docker.core.ImageSearchResultV1;
import org.eclipse.linuxtools.internal.docker.core.ImageSearchResultV2;
import org.eclipse.linuxtools.internal.docker.core.OAuth2Utils;
import org.eclipse.linuxtools.internal.docker.core.OAuth2Utils.BearerTokenResponse;
import org.eclipse.linuxtools.internal.docker.core.RepositoryTag;
import org.eclipse.linuxtools.internal.docker.core.RepositoryTagV2;
import org.eclipse.osgi.util.NLS;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.jackson.JacksonFeature;
import org.mandas.docker.client.ObjectMapperProvider;
import org.mandas.docker.client.messages.ImageSearchResult;

/**
 * @since 2.0
 */
public abstract class AbstractRegistry implements IRegistry {

	/** Aliases of the Docker Hub hostname. */
	public static final String[] DOCKERHUB_REGISTRY_ALIASES = new String[] {
			"registry.hub.docker.com", //$NON-NLS-1$
			"index.docker.io" //$NON-NLS-1$
	};

	private static final ClientConfig DEFAULT_CONFIG = new ClientConfig(
			ObjectMapperProvider.class, JacksonFeature.class);

	public static final String DOCKERHUB_REGISTRY = "https://index.docker.io"; //$NON-NLS-1$
	// Cache the URL for searches to avoid excessive calls
	private String cachedHTTPServerAddress;
	// Cache the result of isVersion2 to avoid excessive calls
	private Boolean isV2 = null;

	@Override
	public abstract String getServerAddress();

	/**
	 *
	 * @return the server host (and optional port) to prepend to an image name
	 *         when pushing or pulling
	 */
	// TODO: add this method in the IRegistry interface
	public abstract String getServerHost();

	/**
	 * @return <code>true</code> if this {@link IRegistry} is for Docker Hub,
	 *         <code>false</code> otherwise.
	 */
	// TODO: add this method in the IRegistry interface
	public abstract boolean isDockerHubRegistry();

	/**
	 * @return <code>true</code> if this {@link IRegistry} includes credentials,
	 *         <code>false</code> otherwise.
	 */
	// TODO: add this method in the IRegistry interface
	public abstract boolean isAuthProvided();

	private String getHTTPServerAddress() {
		if (cachedHTTPServerAddress != null) {
			return cachedHTTPServerAddress;
			/*
			 * This is wrong because serverAddress prefixed with http won't work
			 * for push/pull API but let's support this.
			 */
		} else if (getServerAddress().startsWith("http")) { //$NON-NLS-1$
			return getServerAddress();
		}
		// We haven't cached the result, so let's evaluate
		String[] versions = new String[] { "v1", "v2" }; //$NON-NLS-1$ //$NON-NLS-2$
		String[] schemes = new String[] { "http://", "https://" }; //$NON-NLS-1$ //$NON-NLS-2$
		final ClientConfig DEFAULT_CONFIG = new ClientConfig(
				ObjectMapperProvider.class, JacksonFeature.class);
		final Client client = ClientBuilder.newClient(DEFAULT_CONFIG);

		for (String scheme : schemes) {
			for (String ver : versions) {
				String url = scheme + getServerAddress();
				WebTarget queryServer = client.target(url).path(ver);
				try {
					enableDockerAuthenticator();
					Response resp = queryServer.request(APPLICATION_JSON_TYPE)
							.async().method(GET).get();
					int code = resp.getStatus();
					if (code >= 200 && code < 300) {
						cachedHTTPServerAddress = url;
						return url;
					}
				} catch (InterruptedException | ExecutionException e) {
				} finally {
					restoreAuthenticator();
				}
			}
		}
		// URL is probably wrong
		return "http://" + getServerAddress(); //$NON-NLS-1$
	}

	@Override
	public List<IDockerImageSearchResult> getImages(String term)
			throws DockerException {

		final Client client = ClientBuilder.newClient(DEFAULT_CONFIG);
		List<IDockerImageSearchResult> result = new ArrayList<>();
		WebTarget queryImagesResource;

		if (isVersion2()) {
			final GenericType<ImageSearchResultV2> IMAGE_SEARCH_RESULT_LIST = new GenericType<>() {
			};
			ImageSearchResultV2 cisr = null;
			queryImagesResource = client.target(getHTTPServerAddress())
					.path("v2").path("_catalog"); //$NON-NLS-1$ //$NON-NLS-2$
			try {
				cisr = queryImagesResource.request(APPLICATION_JSON_TYPE)
						.async().method(GET, IMAGE_SEARCH_RESULT_LIST).get();
			} catch (InterruptedException | ExecutionException e) {
				throw new DockerException(e);
			}
			List<ImageSearchResult> tmp = cisr.getRepositories().stream()
					.filter(e -> e.name().contains(term))
					.collect(Collectors.toList());

			result.addAll(tmp.stream()
					.map(r -> new DockerImageSearchResult(r.description(),
							r.official(), r.automated(), r.name(),
							r.starCount()))
					.collect(Collectors.toList()));
		} else {
			ImageSearchResultV1 pisr = null;
			final GenericType<ImageSearchResultV1> IMAGE_SEARCH_RESULT_LIST = new GenericType<>() {
			};
			int page = 0;
			try {
				while (pisr == null || pisr.getPage() < pisr.getTotalPages()) {
					page++;
					queryImagesResource = client.target(getHTTPServerAddress())
							.path("v1").path("search") //$NON-NLS-1$ //$NON-NLS-2$
							.queryParam("q", term) //$NON-NLS-1$
							.queryParam("page", page); //$NON-NLS-1$
					pisr = queryImagesResource.request(APPLICATION_JSON_TYPE)
							.async().method(GET, IMAGE_SEARCH_RESULT_LIST)
							.get();
					List<ImageSearchResult> tmp = pisr.getResult();
					result.addAll(tmp.stream()
							.map(r -> new DockerImageSearchResult(
									r.description(), r.official(),
									r.automated(), r.name(), r.starCount()))
							.collect(Collectors.toList()));
				}
			} catch (InterruptedException | ExecutionException e) {
				throw new DockerException(e);
			}
		}

		return result;
	}

	@Override
	public List<IRepositoryTag> getTags(String repository)
			throws DockerException {

		final Client client = ClientBuilder.newClient(DEFAULT_CONFIG);
		try {
			if (isVersion2()) {
				return retrieveTagsFromRegistryV2(client, repository);
			} else if (isDockerHubRegistry()) {
				return retrieveTagsFromDockerHub(client, repository);
			} else {
				return retrieveTagsFromRegistryV1(client, repository);
			}
		} catch (InterruptedException | ExecutionException e) {
			throw new DockerException(e);
		}

	}

	/**
	 * Retrieves the list of tags for a given repository, assuming that the
	 * target registry is Docker Hub.
	 *
	 * @param client
	 *            the client to use
	 * @param repository
	 *            the repository to look-up
	 * @return the list of tags for the given repository
	 * @throws CancellationException
	 *             if the computation was cancelled
	 */
	private List<IRepositoryTag> retrieveTagsFromDockerHub(final Client client,
			final String repository)
			throws DockerException {
		try {
			// if the given repository is an official repository, we need to
			// prepend
			// with 'library'
			final String repoName = repository.contains("/") ? repository //$NON-NLS-1$
					: "library/" + repository; //$NON-NLS-1$
			// Docker Hub Registry may require a Bearer token which needs to be
			// retrieved if the initial request returned a 401/Forbidden
			// response.
			// In that case, the "Www-Authenticate" response header provides the
			// information needed to obtain a token.
			// attempt to query the registry without a bearer token
			final WebTarget queryTagsResource = client
					.target(getHTTPServerAddress()).path("v2") //$NON-NLS-1$
					.path(repoName).path("tags").path("list"); //$NON-NLS-1$ //$NON-NLS-2$
			// return queryTagsResource.request(APPLICATION_JSON_TYPE).async()
			// .method(GET, REPOSITORY_TAGS_RESULT_LIST).get();
			final Response response = queryTagsResource
					.request(APPLICATION_JSON_TYPE).async().get()
					.get(10, TimeUnit.SECONDS);
			if (response.getStatus() == 200) {
				return response.readEntity(RepositoryTagV2.class).getTags();
			} else if (response.getStatus() != 401) { // anything but
														// "Unauthorized"
				throw new DockerException(
						NLS.bind(Messages.ImageTagsList_failure, repository,
								response.readEntity(String.class)));
			}
			// for "Unauthorized response, let's get a Bearer token and try
			// again
			final String wwwAuthenticateResponseHeader = response
					.getHeaderString("Www-Authenticate"); //$NON-NLS-1$
			// parse the header which should have the following form:
			// Bearer
			// realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:jboss/wildfly:pull
			final Map<String, String> authenticateInfo = OAuth2Utils
					.parseWwwAuthenticateHeader(wwwAuthenticateResponseHeader);
			if (authenticateInfo == null
					|| !authenticateInfo.containsKey("realm") //$NON-NLS-1$
					|| !authenticateInfo.containsKey("service") //$NON-NLS-1$
					|| !authenticateInfo.containsKey("scope")) { //$NON-NLS-1$
				throw new DockerException(NLS.bind(
						Messages.ImageTagsList_failure_invalidWwwAuthenticateFormat,
						repository));
			}
			// now, call the auth service to obtain a Bearer token:
			final String realm = authenticateInfo.get("realm"); //$NON-NLS-1$
			final String service = authenticateInfo.get("service"); //$NON-NLS-1$
			final String scope = authenticateInfo.get("scope"); //$NON-NLS-1$
			final WebTarget bearerTokenRetrievalTarget = client.target(realm)
					.queryParam("service", service) //$NON-NLS-1$
					.queryParam("scope", scope); //$NON-NLS-1$
			final BearerTokenResponse bearerTokenRetrievalResponse = bearerTokenRetrievalTarget
					.request(APPLICATION_JSON_TYPE).async()
					.get(BearerTokenResponse.class).get(10, TimeUnit.SECONDS);
			// finally, perform the same request, using the Bearer token:
			final WebTarget queryTagsResourceWithBearerTokenTarget = client
					.target(getHTTPServerAddress()).path("v2") //$NON-NLS-1$
					.path(repoName).path("tags").path("list"); //$NON-NLS-1$ //$NON-NLS-2$
			return queryTagsResourceWithBearerTokenTarget
					.request(APPLICATION_JSON_TYPE)
					.header("Authorization", //$NON-NLS-1$
							"Bearer " + bearerTokenRetrievalResponse.getToken()) //$NON-NLS-1$
					.async().get(RepositoryTagV2.class)
					.get(10, TimeUnit.SECONDS)
					.getTags();
		} catch (TimeoutException | ExecutionException
				| InterruptedException e) {
			throw new DockerException(NLS.bind(Messages.ImageTagsList_failure,
					repository, e.getMessage()), e);
		}

	}

	/**
	 * Retrieves the list of tags for a given repository, assuming that the
	 * target registry is a registry v2 instance.
	 *
	 * @param client
	 *            the client to use
	 * @param repository
	 *            the repository to look-up
	 * @return the list of tags for the given repository
	 * @throws CancellationException
	 *             - if the computation was cancelled
	 * @throws ExecutionException
	 *             - if the computation threw an exception
	 * @throws InterruptedException
	 *             - if the current thread was interrupted while waiting
	 */
	private List<IRepositoryTag> retrieveTagsFromRegistryV1(final Client client,
			final String repository)
			throws InterruptedException, ExecutionException {
		final GenericType<Map<String, String>> REPOSITORY_TAGS_RESULT_LIST = new GenericType<>() {
		};
		final WebTarget queryTagsResource = client
				.target(getHTTPServerAddress()).path("v1") //$NON-NLS-1$
				.path("repositories").path(repository).path("tags"); //$NON-NLS-1$ //$NON-NLS-2$
		return queryTagsResource.request(APPLICATION_JSON_TYPE).async()
				.method(GET, REPOSITORY_TAGS_RESULT_LIST).get().entrySet()
				.stream().map(e -> new RepositoryTag(e.getKey(), e.getValue()))
				.collect(Collectors.toList());
	}

	/**
	 * Retrieves the list of tags for a given repository, assuming that the
	 * target registry is a registry v1 instance.
	 *
	 * @param client
	 *            the client to use
	 * @param repository
	 *            the repository to look-up
	 * @return the list of tags for the given repository
	 * @throws CancellationException
	 *             - if the computation was cancelled
	 * @throws ExecutionException
	 *             - if the computation threw an exception
	 * @throws InterruptedException
	 *             - if the current thread was interrupted while waiting
	 */
	private List<IRepositoryTag> retrieveTagsFromRegistryV2(final Client client,
			final String repository)
			throws InterruptedException, ExecutionException {
		final GenericType<RepositoryTagV2> REPOSITORY_TAGS_RESULT_LIST = new GenericType<>() {
		};
		final WebTarget queryTagsResource = client
				.target(getHTTPServerAddress()).path("v2") //$NON-NLS-1$
				.path(repository).path("tags").path("list"); //$NON-NLS-1$ //$NON-NLS-2$
		final RepositoryTagV2 crts = queryTagsResource
				.request(APPLICATION_JSON_TYPE).async()
				.method(GET, REPOSITORY_TAGS_RESULT_LIST).get();
		return crts.getTags();
	}

	@Override
	public boolean isVersion2() {
		if (isV2 != null) {
			return isV2;
		}
		// We haven't cached the result, so let's evaluate
		final ClientConfig DEFAULT_CONFIG = new ClientConfig(
				ObjectMapperProvider.class, JacksonFeature.class);
		final Client client = ClientBuilder.newClient(DEFAULT_CONFIG);
		final WebTarget pingApiv2Resource = client
				.target(getHTTPServerAddress()).path("v2"); //$NON-NLS-1$
		try {
			final Response response = pingApiv2Resource
					.request(APPLICATION_JSON_TYPE).async().get().get();
			if (response.getStatus() == Status.OK.getStatusCode()) {
				isV2 = true;
				return true;
			}
		} catch (ExecutionException | InterruptedException e) {
			// do nothing
		}
		isV2 = false;
		return false;
	}

	/**
	 * Enable an Authenticator used to pass this registry's authentication
	 * credentials to HTTP Authentication requests.
	 */
	protected abstract void enableDockerAuthenticator();

	/**
	 * Restore the default Authenticator (likely
	 * org.eclipse.ui.internal.net.auth.NetAuthenticator)
	 */
	protected abstract void restoreAuthenticator();

}
