// Copyright (C) 2017 The Android Open Source Project
//
// 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.gerrit.server.group.db;

import static com.google.common.base.Preconditions.checkState;
import static java.util.Objects.requireNonNull;

import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.GroupDescription;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.GroupBackend;
import java.util.Optional;
import java.util.function.Function;
import org.eclipse.jgit.lib.PersonIdent;

/**
 * A formatter for entities used in an audit log which is typically represented by NoteDb commits.
 *
 * <p>The formatted representation of those entities must be parsable so that we can read them later
 * on and map them back to their original entities. {@link AuditLogFormatter} and {@link
 * com.google.gerrit.server.notedb.NoteDbUtil NoteDbUtil} contain some of those parsing/mapping
 * methods.
 */
public class AuditLogFormatter {
  private final Function<Account.Id, Optional<Account>> accountRetriever;
  private final Function<AccountGroup.UUID, Optional<GroupDescription.Basic>> groupRetriever;
  @Nullable private final String serverId;

  public static AuditLogFormatter createBackedBy(
      AccountCache accountCache, GroupBackend groupBackend, String serverId) {
    return create(
        accountId -> getAccount(accountCache, accountId),
        groupUuid -> getGroup(groupBackend, groupUuid),
        serverId);
  }

  private static Optional<Account> getAccount(AccountCache accountCache, Account.Id accountId) {
    return accountCache.get(accountId).map(AccountState::account);
  }

  private static Optional<GroupDescription.Basic> getGroup(
      GroupBackend groupBackend, AccountGroup.UUID groupUuid) {
    return Optional.ofNullable(groupBackend.get(groupUuid));
  }

  public static AuditLogFormatter createBackedBy(
      ImmutableSet<Account> allAccounts,
      ImmutableSet<GroupDescription.Basic> allGroups,
      String serverId) {
    return create(id -> getAccount(allAccounts, id), uuid -> getGroup(allGroups, uuid), serverId);
  }

  private static Optional<GroupDescription.Basic> getGroup(
      ImmutableSet<GroupDescription.Basic> groups, AccountGroup.UUID uuid) {
    return groups.stream().filter(group -> group.getGroupUUID().equals(uuid)).findAny();
  }

  private static Optional<Account> getAccount(ImmutableSet<Account> accounts, Account.Id id) {
    return accounts.stream().filter(account -> account.id().equals(id)).findAny();
  }

  public static AuditLogFormatter createPartiallyWorkingFallBack() {
    return new AuditLogFormatter(id -> Optional.empty(), uuid -> Optional.empty());
  }

  public static AuditLogFormatter create(
      Function<Account.Id, Optional<Account>> accountRetriever,
      Function<AccountGroup.UUID, Optional<GroupDescription.Basic>> groupRetriever,
      String serverId) {
    return new AuditLogFormatter(accountRetriever, groupRetriever, serverId);
  }

  private AuditLogFormatter(
      Function<Account.Id, Optional<Account>> accountRetriever,
      Function<AccountGroup.UUID, Optional<GroupDescription.Basic>> groupRetriever,
      String serverId) {
    this.accountRetriever = requireNonNull(accountRetriever);
    this.groupRetriever = requireNonNull(groupRetriever);
    this.serverId = requireNonNull(serverId);
  }

  private AuditLogFormatter(
      Function<Account.Id, Optional<Account>> accountRetriever,
      Function<AccountGroup.UUID, Optional<GroupDescription.Basic>> groupRetriever) {
    this.accountRetriever = requireNonNull(accountRetriever);
    this.groupRetriever = requireNonNull(groupRetriever);
    serverId = null;
  }

  /**
   * Creates a parsable {@code PersonIdent} for commits which are used as an audit log.
   *
   * <p><em>Parsable</em> means that we can unambiguously identify the original account when being
   * presented with a {@code PersonIdent} of a commit.
   *
   * <p>We typically use the initiator of an action as the author of the commit when using those
   * commits as an audit log. That's something which has to be specified by a caller of this method
   * as this class doesn't create any commits itself.
   *
   * @param account the {@code Account} of the user who should be represented
   * @param personIdent a {@code PersonIdent} which provides the timestamp for the created {@code
   *     PersonIdent}
   * @return a {@code PersonIdent} which can be used for the author of a commit
   */
  public PersonIdent getParsableAuthorIdent(Account account, PersonIdent personIdent) {
    return getParsableAuthorIdent(account.getName(), account.id(), personIdent);
  }

  /**
   * Creates a parsable {@code PersonIdent} for commits which are used as an audit log.
   *
   * <p>See {@link #getParsableAuthorIdent(Account, PersonIdent)} for further details.
   *
   * @param accountId the ID of the account of the user who should be represented
   * @param personIdent a {@code PersonIdent} which provides the timestamp for the created {@code
   *     PersonIdent}
   * @return a {@code PersonIdent} which can be used for the author of a commit
   */
  public PersonIdent getParsableAuthorIdent(Account.Id accountId, PersonIdent personIdent) {
    String accountName = getAccountName(accountId);
    return getParsableAuthorIdent(accountName, accountId, personIdent);
  }

  /**
   * Provides a parsable representation of an account for use in e.g. commit messages.
   *
   * @param accountId the ID of the account of the user who should be represented
   * @return the {@code String} representation of the account
   */
  public String getParsableAccount(Account.Id accountId) {
    String accountName = getAccountName(accountId);
    return formatNameEmail(accountName, getEmailForAuditLog(accountId));
  }

  /**
   * Provides a parsable representation of a group for use in e.g. commit messages.
   *
   * @param groupUuid the UUID of the group
   * @return the {@code String} representation of the group
   */
  public String getParsableGroup(AccountGroup.UUID groupUuid) {
    String uuid = groupUuid.get();
    Optional<GroupDescription.Basic> group = groupRetriever.apply(groupUuid);
    String name = group.map(GroupDescription.Basic::getName).orElse(uuid);
    return formatNameEmail(name, uuid);
  }

  private String getAccountName(Account.Id accountId) {
    Optional<Account> account = accountRetriever.apply(accountId);
    return account
        .map(Account::getName)
        // Historically, the database did not enforce relational integrity, so it is
        // possible for groups to have non-existing members.
        .orElse("No Account for Id #" + accountId);
  }

  private PersonIdent getParsableAuthorIdent(
      String accountname, Account.Id accountId, PersonIdent personIdent) {
    return new PersonIdent(
        accountname,
        getEmailForAuditLog(accountId),
        personIdent.getWhen(),
        personIdent.getTimeZone());
  }

  private String getEmailForAuditLog(Account.Id accountId) {
    // If we ever switch to UUIDs for accounts, consider to remove the serverId and to use a similar
    // approach as for group UUIDs.
    checkState(
        serverId != null, "serverId must be defined; fall-back AuditLogFormatter isn't sufficient");
    return accountId.get() + "@" + serverId;
  }

  private static String formatNameEmail(String name, String email) {
    StringBuilder formattedResult = new StringBuilder();
    PersonIdent.appendSanitized(formattedResult, name);
    formattedResult.append(" <");
    PersonIdent.appendSanitized(formattedResult, email);
    formattedResult.append(">");
    return formattedResult.toString();
  }
}
