/*
 * Copyright (C) 2019 Google Inc.
 *
 * 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.copybara.git;

import static com.google.copybara.git.GitModule.DEFAULT_INTEGRATE_LABEL;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.CharMatcher;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.primitives.Ints;
import com.google.copybara.ChangeMessage;
import com.google.copybara.GeneralOptions;
import com.google.copybara.LabelFinder;
import com.google.copybara.exception.CannotResolveRevisionException;
import com.google.copybara.exception.RepoException;
import com.google.copybara.exception.ValidationException;
import com.google.re2j.Matcher;
import com.google.re2j.Pattern;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import javax.annotation.Nullable;

/**
 * A class that represents a Gerrit change. It contains all the necessary objects to do a
 * fetch when {@code {@link #fetch(ImmutableMultimap)}} is invoked.
 */
class GerritChange {

  public static final String GERRIT_CHANGE_NUMBER_LABEL = "GERRIT_CHANGE_NUMBER";
  public static final String GERRIT_CHANGE_ID_LABEL = "GERRIT_CHANGE_ID";
  public static final String GERRIT_COMPLETE_CHANGE_ID_LABEL =
      "GERRIT_COMPLETE_CHANGE_ID";
  // TODO(danielromero): Implement (and refer from gerrit_origin documentation in GitModule)
  public static final String GERRIT_CHANGE_URL_LABEL = "GERRIT_CHANGE_URL";
  public static final String GERRIT_CHANGE_BRANCH = "GERRIT_CHANGE_BRANCH";
  public static final String GERRIT_CHANGE_TOPIC = "GERRIT_CHANGE_TOPIC";
  public static final String GERRIT_CHANGE_DESCRIPTION_LABEL = "GERRIT_CHANGE_DESCRIPTION";
  public static final String GERRIT_OWNER_EMAIL_LABEL = "GERRIT_OWNER_EMAIL";
  public static final String GERRIT_OWNER_USERNAME_LABEL = "GERRIT_OWNER_USERNAME";
  private static final String GERRIT_PATCH_SET_REF_PREFIX = "PatchSet ";

  private static final Pattern WHOLE_GERRIT_REF =
      Pattern.compile("refs/changes/[0-9]{2}/([0-9]+)/([0-9]+)");

  private static final Pattern URL_PATTERN =
      Pattern.compile("https?://.*?/([0-9]+)(?:/([0-9]+))?/?");

  private final GitRepository repository;
  private final GeneralOptions generalOptions;
  private final String repoUrl;
  private final int change;
  private final int patchSet;
  private final String ref;

  private GerritChange(GitRepository repository, GeneralOptions generalOptions,
      String repoUrl, int change, int patchSet, String ref) {
    this.repository = Preconditions.checkNotNull(repository);
    this.generalOptions = Preconditions.checkNotNull(generalOptions);
    this.repoUrl = repoUrl;
    this.change = change;
    this.patchSet = patchSet;
    this.ref = ref;
  }

  /**
   * Get the change number
   */
  public int getChange() {
    return change;
  }

  /**
   * Gets the specific PatchSet of the Change
   */
  public int getPatchSet() {
    return patchSet;
  }

  /**
   * Context reference for creating GitRevision
   */
  public String getRef() {
    return ref;
  }

  /**
   * Given a local repository, a repo url and a reference, it tries to do its best to resolve the
   * reference to a Gerrit Change.
   *
   * <p>Note that if the PatchSet is not found in the ref, it will go to Gerrit to get the latest
   * PatchSet number.
   *
   * @return a Gerrit change if it can be resolved. Null otherwise.
   */
  @Nullable
  public static GerritChange resolve(
      GitRepository repository, String repoUrl, String ref, GeneralOptions options)
      throws RepoException, ValidationException {
    if (Strings.isNullOrEmpty(ref)) {
      return null;
    }
    Matcher refMatcher = WHOLE_GERRIT_REF.matcher(ref);
    if (refMatcher.matches()) {
      return new GerritChange(
          repository,
          options,
          repoUrl,
          Ints.tryParse(refMatcher.group(1)),
          Ints.tryParse(refMatcher.group(2)),
          ref);
    }
    // A change number like '23423'
    if (CharMatcher.javaDigit().matchesAllOf(ref)) {
      return resolveLatestPatchSet(repository, options, repoUrl, Ints.tryParse(ref));
    }

    Matcher urlMatcher = URL_PATTERN.matcher(ref);
    if (!urlMatcher.matches()) {
      return null;
    }

    if (!ref.startsWith(repoUrl)) {
      // Assume it is our url. We can make this more strict later
      options
          .console()
          .warn(
              String.format(
                  "Assuming repository '%s' for looking for review '%s'", repoUrl, ref));
    }
    int change = Ints.tryParse(urlMatcher.group(1));
    Integer patchSet = urlMatcher.group(2) == null ? null : Ints.tryParse(urlMatcher.group(2));
    if (patchSet == null) {
      return resolveLatestPatchSet(repository, options, repoUrl, change);
    }
    Map<Integer, GitRevision> patchSets = getGerritPatchSets(repository, repoUrl, change);
    if (!patchSets.containsKey(patchSet)) {
      throw new CannotResolveRevisionException(
          String.format(
              "Cannot find patch set %d for change %d in %s. Available Patch sets: %s",
              patchSet, change, repoUrl, patchSets.keySet()));
    }
    return new GerritChange(
        repository, options, repoUrl, change, patchSet, patchSets.get(patchSet).contextReference());

  }

  /**
   * Fetch the change from Gerrit
   *
   * @param additionalLabels additional labels to add to the GitRevision labels
   * @return The resolved and fetched SHA-1 of the change.
   */
  GitRevision fetch(ImmutableMultimap<String, String> additionalLabels)
      throws RepoException, ValidationException {
    String metaRef = String.format("refs/changes/%02d/%d/meta", change % 100, change);
    repository.fetch(repoUrl, /*prune=*/true, /*force=*/true,
        ImmutableList.of(ref + ":refs/gerrit/" + ref, metaRef + ":refs/gerrit/" + metaRef), false);
    GitRevision gitRevision = repository.resolveReference("refs/gerrit/" + ref);
    GitRevision metaRevision = repository.resolveReference("refs/gerrit/" + metaRef);
    String changeId = getChangeIdFromMeta(repository, metaRevision , metaRef);
    String changeNumber = Integer.toString(change);
    String changeDescription = getDescriptionFromMeta(repository, metaRevision , metaRef);
    return new GitRevision(
        repository,
        gitRevision.getSha1(),
        gerritPatchSetAsReviewReference(patchSet),
        changeNumber,
        ImmutableListMultimap.<String, String>builder()
            .put(GERRIT_CHANGE_NUMBER_LABEL, changeNumber)
            .put(GERRIT_CHANGE_ID_LABEL, changeId)
            .put(GERRIT_CHANGE_DESCRIPTION_LABEL, changeDescription)
            .put(
                DEFAULT_INTEGRATE_LABEL,
                new GerritIntegrateLabel(
                    repository, generalOptions, repoUrl, change, patchSet, changeId)
                    .toString())
            .putAll(additionalLabels)
            .build(),
        repoUrl);
  }

  private static GerritChange resolveLatestPatchSet(
      GitRepository repository, GeneralOptions options, String repoUrl,
      int changeNumber)
      throws RepoException, ValidationException {
    Entry<Integer, GitRevision> lastPatchset =
        // Last entry is the latest patchset, since it is ordered by patchsetId.
        getGerritPatchSets(repository, repoUrl, changeNumber).lastEntry();
    return new GerritChange(repository, options, repoUrl, changeNumber, lastPatchset.getKey(),
        lastPatchset.getValue().contextReference());
  }

  /**
   * Use NoteDB for extracting the Change-id. It should be the first commit in the log
   * of the meta reference.
   *
   * TODO(malcon): Remove usage and use Gerrit API in GerritOrigin
   */
  private String getChangeIdFromMeta(GitRepository repo, GitRevision metaRevision,
      String metaRef) throws RepoException {
    List<ChangeMessage> changes = getChanges(repo, metaRevision, metaRef);
    String changeId = null;
    for (LabelFinder change : Iterables.getLast(changes).getLabels()) {
      if (change.isLabel() && change.getName().equals("Change-id")
          && change.getSeparator().equals(": ")) {
        changeId = change.getValue();
      }
    }
    if (changeId == null) {
      throw new RepoException(String.format(
          "Cannot find Change-id in %s. Not present in: \n%s", metaRef,
          Iterables.getLast(changes).getText()));
    }

    return changeId;
  }

  private String getDescriptionFromMeta(GitRepository repo, GitRevision metaRevision,
      String metaRef) throws RepoException {
    List<ChangeMessage> changes = getChanges(repo, metaRevision, metaRef);
    return changes.get(0).getText();
  }

  /**
   * Returns the list of {@link ChangeMessage}s. Guarantees that there is at least one change.
   */
  private List<ChangeMessage> getChanges(GitRepository repo, GitRevision metaRevision,
      String metaRef) throws RepoException {
    List<ChangeMessage> changes = Lists.transform(repo.log(metaRevision.getSha1()).run(),
        e -> ChangeMessage.parseMessage(e.getBody()));

    if (changes.isEmpty()) {
      throw new RepoException("Cannot find any PatchSet in " + metaRef);
    }
    return changes;
  }

  /**
   * Get all the patchsets for a change ordered by the patchset number. Last is the most recent
   * one.
   */
  static TreeMap<Integer, GitRevision> getGerritPatchSets(
      GitRepository repository, String url, int changeNumber)
      throws RepoException, CannotResolveRevisionException {
    TreeMap<Integer, GitRevision> patchSets = new TreeMap<>();
    String basePath = String.format("refs/changes/%02d/%d", changeNumber % 100, changeNumber);
    Map<String, String> refsToSha1 = repository.lsRemote(url, ImmutableList.of(basePath + "/*"));
    if (refsToSha1.isEmpty()) {
      throw new CannotResolveRevisionException(
          String.format("Cannot find change number %d in '%s'", changeNumber, url));
    }
    for (Entry<String, String> e : refsToSha1.entrySet()) {
      if (e.getKey().endsWith("/meta")) {
        continue;
      }
      Preconditions.checkState(
          e.getKey().startsWith(basePath + "/"),
          String.format("Unexpected response reference %s for %s", e.getKey(), basePath));
      Matcher matcher = WHOLE_GERRIT_REF.matcher(e.getKey());
      Preconditions.checkArgument(
          matcher.matches(),
          "Unexpected format for response reference %s for %s",
          e.getKey(),
          basePath);
      int patchSet = Ints.tryParse(matcher.group(2));
      patchSets.put(
          patchSet,
          new GitRevision(
              repository,
              e.getValue(),
              gerritPatchSetAsReviewReference(patchSet),
              e.getKey(),
              ImmutableListMultimap.of(), url));
    }
    return patchSets;
  }

  @VisibleForTesting
  static String gerritPatchSetAsReviewReference(int patchSet) {
    return GERRIT_PATCH_SET_REF_PREFIX + patchSet;
  }
}