// Copyright (C) 2019 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.logging;

import com.google.auto.value.AutoValue;
import com.google.common.base.MoreObjects;
import com.google.common.base.MoreObjects.ToStringHelper;
import com.google.common.collect.ImmutableList;
import com.google.common.flogger.LazyArg;
import com.google.common.flogger.LazyArgs;
import com.google.gerrit.common.Nullable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Optional;

/** Metadata that is provided to {@link PerformanceLogger}s as context for performance records. */
@AutoValue
public abstract class Metadata {
  /** The numeric ID of an account. */
  public abstract Optional<Integer> accountId();

  /**
   * The type of an action (ACCOUNT_UPDATE, CHANGE_UPDATE, GROUP_UPDATE, INDEX_QUERY,
   * PLUGIN_UPDATE).
   */
  public abstract Optional<String> actionType();

  /** An authentication domain name. */
  public abstract Optional<String> authDomainName();

  /** The name of a branch. */
  public abstract Optional<String> branchName();

  /** Key of an entity in a cache. */
  public abstract Optional<String> cacheKey();

  /** The name of a cache. */
  public abstract Optional<String> cacheName();

  /** The name of the implementation class. */
  public abstract Optional<String> className();

  /** The numeric ID of a change. */
  public abstract Optional<Integer> changeId();

  /**
   * The type of change ID which the user used to identify a change (e.g. numeric ID, triplet etc.).
   */
  public abstract Optional<String> changeIdType();

  /** The cause of an error. */
  public abstract Optional<String> cause();

  /** The SHA1 of a commit. */
  public abstract Optional<String> commit();

  /** The type of an event. */
  public abstract Optional<String> eventType();

  /** The value of the @Export annotation which was used to register a plugin extension. */
  public abstract Optional<String> exportValue();

  /** Path of a file in a repository. */
  public abstract Optional<String> filePath();

  /** Garbage collector name. */
  public abstract Optional<String> garbageCollectorName();

  /** Git operation (CLONE, FETCH). */
  public abstract Optional<String> gitOperation();

  /** The numeric ID of an internal group. */
  public abstract Optional<Integer> groupId();

  /** The name of a group. */
  public abstract Optional<String> groupName();

  /** The UUID of a group. */
  public abstract Optional<String> groupUuid();

  /** HTTP status response code. */
  public abstract Optional<Integer> httpStatus();

  /** The name of a secondary index. */
  public abstract Optional<String> indexName();

  /** The version of a secondary index. */
  public abstract Optional<Integer> indexVersion();

  /** The name of the implementation method. */
  public abstract Optional<String> memoryPoolName();

  /** The name of the implementation method. */
  public abstract Optional<String> methodName();

  /** One or more resources */
  public abstract Optional<Boolean> multiple();

  /** The name of an operation that is performed. */
  public abstract Optional<String> operationName();

  /** Partial or full computation */
  public abstract Optional<Boolean> partial();

  /** If a value is still current or not */
  public abstract Optional<Boolean> outdated();

  /** Path of a metadata file in NoteDb. */
  public abstract Optional<String> noteDbFilePath();

  /** Name of a metadata ref in NoteDb. */
  public abstract Optional<String> noteDbRefName();

  /** Type of a sequence in NoteDb (ACCOUNTS, CHANGES, GROUPS). */
  public abstract Optional<String> noteDbSequenceType();

  /** The ID of a patch set. */
  public abstract Optional<Integer> patchSetId();

  /** Plugin metadata that doesn't fit into any other category. */
  public abstract ImmutableList<PluginMetadata> pluginMetadata();

  /** The name of a plugin. */
  public abstract Optional<String> pluginName();

  /** The name of a Gerrit project (aka Git repository). */
  public abstract Optional<String> projectName();

  /** The type of a Git push to Gerrit (CREATE_REPLACE, NORMAL, AUTOCLOSE). */
  public abstract Optional<String> pushType();

  /** The number of resources that is processed. */
  public abstract Optional<Integer> resourceCount();

  /** The name of a REST view. */
  public abstract Optional<String> restViewName();

  /** The SHA1 of Git commit. */
  public abstract Optional<String> revision();

  /** The username of an account. */
  public abstract Optional<String> username();

  /**
   * Returns a string representation of this instance that is suitable for logging. This is wrapped
   * in a {@link LazyArg} because it is expensive to evaluate.
   *
   * <p>{@link #toString()} formats the {@link Optional} fields as {@code key=Optional[value]} or
   * {@code key=Optional.empty}. Since this class has many optional fields from which usually only a
   * few are populated this leads to long string representations such as
   *
   * <pre>
   * Metadata{accountId=Optional.empty, actionType=Optional.empty, authDomainName=Optional.empty,
   * branchName=Optional.empty, cacheKey=Optional.empty, cacheName=Optional.empty,
   * className=Optional.empty, changeId=Optional[9212550], changeIdType=Optional.empty,
   * cause=Optional.empty, eventType=Optional.empty, exportValue=Optional.empty,
   * filePath=Optional.empty, garbageCollectorName=Optional.empty, gitOperation=Optional.empty,
   * groupId=Optional.empty, groupName=Optional.empty, groupUuid=Optional.empty,
   * httpStatus=Optional.empty, indexName=Optional.empty, indexVersion=Optional[0],
   * methodName=Optional.empty, multiple=Optional.empty, operationName=Optional.empty,
   * partial=Optional.empty, noteDbFilePath=Optional.empty, noteDbRefName=Optional.empty,
   * noteDbSequenceType=Optional.empty, patchSetId=Optional.empty, pluginMetadata=[],
   * pluginName=Optional.empty, projectName=Optional.empty, pushType=Optional.empty,
   * resourceCount=Optional.empty, restViewName=Optional.empty, revision=Optional.empty,
   * username=Optional.empty}
   * </pre>
   *
   * <p>That's hard to read in logs. This is why this method
   *
   * <ul>
   *   <li>drops fields which have {@code Optional.empty} as value and
   *   <li>reformats values that are {@code Optional[value]} to {@code value}.
   * </ul>
   *
   * <p>For the example given above the formatted string would look like this:
   *
   * <pre>
   * Metadata{changeId=9212550, indexVersion=0, pluginMetadata=[]}
   * </pre>
   *
   * @return string representation of this instance that is suitable for logging
   */
  LazyArg<String> toStringForLoggingLazy() {
    // Don't use a lambda because different compilers generate different method names for lambdas,
    // e.g. "lambda$myFunction$0" vs. just "lambda$0" in Eclipse. We need to identify the method
    // by name to skip it and avoid infinite recursion.
    return LazyArgs.lazy(this::toStringForLoggingImpl);
  }

  private String toStringForLoggingImpl() {
    // Append class name.
    String className = getClass().getSimpleName();
    if (className.startsWith("AutoValue_")) {
      className = className.substring(10);
    }
    ToStringHelper stringHelper = MoreObjects.toStringHelper(className);

    // Append key-value pairs for field which are set.
    Method[] methods = Metadata.class.getDeclaredMethods();
    Arrays.sort(methods, Comparator.comparing(Method::getName));
    for (Method method : methods) {
      if (Modifier.isStatic(method.getModifiers())) {
        // skip static method
        continue;
      }

      if (method.getName().equals("toStringForLoggingLazy")
          || method.getName().equals("toStringForLoggingImpl")) {
        // Don't call myself in infinite recursion.
        continue;
      }

      if (method.getReturnType().equals(Void.TYPE) || method.getParameterCount() > 0) {
        // skip method since it's not a getter
        continue;
      }

      method.setAccessible(true);

      Object returnValue;
      try {
        returnValue = method.invoke(this);
      } catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException e) {
        // should never happen
        throw new IllegalStateException(e);
      }

      if (returnValue instanceof Optional) {
        Optional<?> fieldValueOptional = (Optional<?>) returnValue;
        if (!fieldValueOptional.isPresent()) {
          // drop this key-value pair
          continue;
        }

        // format as 'key=value' instead of 'key=Optional[value]'
        stringHelper.add(method.getName(), fieldValueOptional.get());
      } else {
        // not an Optional value, keep as is
        stringHelper.add(method.getName(), returnValue);
      }
    }

    return stringHelper.toString();
  }

  public static Metadata.Builder builder() {
    return new AutoValue_Metadata.Builder();
  }

  public static Metadata empty() {
    return builder().build();
  }

  @AutoValue.Builder
  public abstract static class Builder {
    public abstract Builder accountId(int accountId);

    public abstract Builder actionType(@Nullable String actionType);

    public abstract Builder authDomainName(@Nullable String authDomainName);

    public abstract Builder branchName(@Nullable String branchName);

    public abstract Builder cacheKey(@Nullable String cacheKey);

    public abstract Builder cacheName(@Nullable String cacheName);

    public abstract Builder className(@Nullable String className);

    public abstract Builder changeId(int changeId);

    public abstract Builder changeIdType(@Nullable String changeIdType);

    public abstract Builder cause(@Nullable String cause);

    public abstract Builder commit(@Nullable String commit);

    public abstract Builder eventType(@Nullable String eventType);

    public abstract Builder exportValue(@Nullable String exportValue);

    public abstract Builder filePath(@Nullable String filePath);

    public abstract Builder garbageCollectorName(@Nullable String garbageCollectorName);

    public abstract Builder gitOperation(@Nullable String gitOperation);

    public abstract Builder groupId(int groupId);

    public abstract Builder groupName(@Nullable String groupName);

    public abstract Builder groupUuid(@Nullable String groupUuid);

    public abstract Builder httpStatus(int httpStatus);

    public abstract Builder indexName(@Nullable String indexName);

    public abstract Builder indexVersion(int indexVersion);

    public abstract Builder memoryPoolName(@Nullable String memoryPoolName);

    public abstract Builder methodName(@Nullable String methodName);

    public abstract Builder multiple(boolean multiple);

    public abstract Builder operationName(String operationName);

    public abstract Builder partial(boolean partial);

    public abstract Builder outdated(boolean outdated);

    public abstract Builder noteDbFilePath(@Nullable String noteDbFilePath);

    public abstract Builder noteDbRefName(@Nullable String noteDbRefName);

    public abstract Builder noteDbSequenceType(@Nullable String noteDbSequenceType);

    public abstract Builder patchSetId(int patchSetId);

    abstract ImmutableList.Builder<PluginMetadata> pluginMetadataBuilder();

    public Builder addPluginMetadata(PluginMetadata pluginMetadata) {
      pluginMetadataBuilder().add(pluginMetadata);
      return this;
    }

    public abstract Builder pluginName(@Nullable String pluginName);

    public abstract Builder projectName(@Nullable String projectName);

    public abstract Builder pushType(@Nullable String pushType);

    public abstract Builder resourceCount(int resourceCount);

    public abstract Builder restViewName(@Nullable String restViewName);

    public abstract Builder revision(@Nullable String revision);

    public abstract Builder username(@Nullable String username);

    public abstract Metadata build();
  }
}
