// Copyright 2016 The Bazel Authors. All rights reserved.
//
// 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 com.google.devtools.build.lib.bazel.repository;

import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
import com.google.devtools.build.lib.bazel.repository.cache.RepositoryCache;
import com.google.devtools.build.lib.bazel.repository.cache.RepositoryCache.KeyType;
import com.google.devtools.build.lib.bazel.repository.downloader.HttpDownloader;
import com.google.devtools.build.lib.rules.repository.WorkspaceAttributeMapper;
import com.google.devtools.build.lib.syntax.EvalException;
import com.google.devtools.build.lib.syntax.Type;
import com.google.devtools.build.lib.vfs.Path;
import java.io.IOException;
import java.util.Map;
import java.util.StringJoiner;
import javax.annotation.Nullable;
import org.apache.maven.settings.Server;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.repository.Authentication;
import org.eclipse.aether.repository.AuthenticationContext;
import org.eclipse.aether.repository.AuthenticationDigest;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.resolution.ArtifactRequest;
import org.eclipse.aether.resolution.ArtifactResolutionException;
import org.eclipse.aether.resolution.ArtifactResult;

/**
 * Downloader for JAR files from Maven repositories.
 * TODO(jingwen): standardize interface between this and HttpDownloader
 */
public class MavenDownloader extends HttpDownloader {

  @Nullable
  private String name;
  @Nullable
  private Path outputDirectory;

  public MavenDownloader(RepositoryCache repositoryCache) {
    super(repositoryCache);
  }

  /**
   * Returns the name for this artifact-fetching rule.
   */
  public String getName() {
    return name;
  }

  /**
   * Returns the directory that this artifact will be downloaded to.
   */
  public Path getOutputDirectory() {
    return outputDirectory;
  }

  /**
   * Download the Maven artifact to the output directory. Returns the path to the jar.
   */
  public Path download(String name, WorkspaceAttributeMapper mapper, Path outputDirectory,
      MavenServerValue serverValue) throws IOException, EvalException {
    this.name = name;
    this.outputDirectory = outputDirectory;

    String url = serverValue.getUrl();
    Server server = serverValue.getServer();

    Artifact artifact;
    String artifactId = mapper.get("artifact", Type.STRING);
    String sha1 = mapper.isAttributeValueExplicitlySpecified("sha1")
        ? mapper.get("sha1", Type.STRING) : null;
        if (sha1 != null && !KeyType.SHA1.isValid(sha1)) {
          throw new IOException("Invalid SHA-1 for maven_jar " + name + ": '" + sha1 + "'");
        }
    try {
      artifact = new DefaultArtifact(artifactId);
    } catch (IllegalArgumentException e) {
      throw new IOException(e.getMessage());
    }
 
    boolean isCaching = repositoryCache.isEnabled() && KeyType.SHA1.isValid(sha1);

    if (isCaching) {
      Path downloadPath = getDownloadDestination(artifact);
      Path cachedDestination = repositoryCache.get(sha1, downloadPath, KeyType.SHA1);
      if (cachedDestination != null) {
        return cachedDestination;
      }
    }

    MavenConnector connector = new MavenConnector(outputDirectory.getPathString());
    RepositorySystem system = connector.newRepositorySystem();
    RepositorySystemSession session = connector.newRepositorySystemSession(system);

    RemoteRepository repository = new RemoteRepository.Builder(
        name, MavenServerValue.DEFAULT_ID, url)
        .setAuthentication(new MavenAuthentication(server))
        .build();
    ArtifactRequest artifactRequest = new ArtifactRequest();

    artifactRequest.setArtifact(artifact);
    artifactRequest.setRepositories(ImmutableList.of(repository));

    try {
      ArtifactResult artifactResult = system.resolveArtifact(session, artifactRequest);
      artifact = artifactResult.getArtifact();
    } catch (ArtifactResolutionException e) {
      throw new IOException("Failed to fetch Maven dependency: " + e.getMessage());
    }

    Path downloadPath = outputDirectory.getRelative(artifact.getFile().getAbsolutePath());
    // Verify checksum.
    if (!Strings.isNullOrEmpty(sha1)) {
      RepositoryCache.assertFileChecksum(sha1, downloadPath, KeyType.SHA1);
    }

    if (isCaching) {
      repositoryCache.put(sha1, downloadPath, KeyType.SHA1);
    }
    return downloadPath;
  }

  private Path getDownloadDestination(Artifact artifact) {
    String groupIdPath = artifact.getGroupId().replace('.', '/');
    String artifactId = artifact.getArtifactId();
    String version = artifact.getVersion();
    String filename = artifactId + '-' + version + '.' + artifact.getExtension();

    StringJoiner joiner = new StringJoiner("/");
    joiner.add(groupIdPath).add(artifactId).add(version).add(filename);

    return outputDirectory.getRelative(joiner.toString());
  }

  private static class MavenAuthentication implements Authentication {

    private final Map<String, String> authenticationInfo;

    private MavenAuthentication(Server server) {
      Builder<String, String> builder = ImmutableMap.<String, String>builder();
      // From https://maven.apache.org/settings.html: "If you use a private key to login to the
      // server, make sure you omit the <password> element. Otherwise, the key will be ignored."
      if (server.getPassword() != null) {
        builder.put(AuthenticationContext.USERNAME, server.getUsername());
        builder.put(AuthenticationContext.PASSWORD, server.getPassword());
      } else if (server.getPrivateKey() != null) {
        // getPrivateKey sounds like it returns the key, but it actually returns a path to it.
        builder.put(AuthenticationContext.PRIVATE_KEY_PATH, server.getPrivateKey());
        builder.put(AuthenticationContext.PRIVATE_KEY_PASSPHRASE, server.getPassphrase());
      }
      authenticationInfo = builder.build();
    }

    @Override
    public void fill(
        AuthenticationContext authenticationContext, String s, Map<String, String> map) {
      for (Map.Entry<String, String> entry : authenticationInfo.entrySet()) {
        authenticationContext.put(entry.getKey(), entry.getValue());
      }
    }

    @Override
    public void digest(AuthenticationDigest authenticationDigest) {
      // No-op.
    }
  }

}
