// Copyright (C) 2014 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.restapi.account;

import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.entities.Account;
import com.google.gerrit.extensions.client.ListAccountsOption;
import com.google.gerrit.extensions.client.ListOption;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.AccountVisibility;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.extensions.restapi.TopLevelResource;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.index.query.QueryResult;
import com.google.gerrit.server.account.AccountDirectory.FillOptions;
import com.google.gerrit.server.account.AccountInfoComparator;
import com.google.gerrit.server.account.AccountLoader;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.permissions.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.query.account.AccountPredicates;
import com.google.gerrit.server.query.account.AccountQueryBuilder;
import com.google.gerrit.server.query.account.AccountQueryProcessor;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.util.Collections;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.jgit.lib.Config;
import org.kohsuke.args4j.Option;

/**
 * REST endpoint to query accounts.
 *
 * <p>This REST endpoint handles {@code GET /accounts/} requests.
 *
 * <p>The account queries are parsed by {@link AccountQueryBuilder} and executed by {@link
 * AccountQueryProcessor}.
 */
public class QueryAccounts implements RestReadView<TopLevelResource> {
  private static final int MAX_SUGGEST_RESULTS = 100;

  private final PermissionBackend permissionBackend;
  private final AccountLoader.Factory accountLoaderFactory;
  private final AccountQueryBuilder queryBuilder;
  private final Provider<AccountQueryProcessor> queryProcessorProvider;
  private final boolean suggestConfig;
  private final int suggestFrom;

  private AccountLoader accountLoader;
  private boolean suggest;
  private Integer limit;
  private int suggestLimit = 10;
  private String query;
  private Integer start;
  private EnumSet<ListAccountsOption> options;

  @Option(name = "--suggest", metaVar = "SUGGEST", usage = "suggest users")
  public void setSuggest(boolean suggest) {
    this.suggest = suggest;
  }

  @Option(
      name = "--limit",
      aliases = {"-n"},
      metaVar = "CNT",
      usage = "maximum number of users to return")
  public void setLimit(int n) {
    this.limit = n;

    if (n < 0) {
      suggestLimit = 10;
    } else if (n == 0) {
      suggestLimit = MAX_SUGGEST_RESULTS;
    } else {
      suggestLimit = Math.min(n, MAX_SUGGEST_RESULTS);
    }
  }

  @Option(name = "-o", usage = "Output options per account")
  public void addOption(ListAccountsOption o) {
    options.add(o);
  }

  @Option(name = "-O", usage = "Output option flags, in hex")
  void setOptionFlagsHex(String hex) {
    options.addAll(ListOption.fromBits(ListAccountsOption.class, Integer.parseInt(hex, 16)));
  }

  @Option(
      name = "--query",
      aliases = {"-q"},
      metaVar = "QUERY",
      usage = "match users")
  public void setQuery(String query) {
    this.query = query;
  }

  @Option(
      name = "--start",
      aliases = {"-S"},
      metaVar = "CNT",
      usage = "Number of accounts to skip")
  public void setStart(int start) {
    this.start = start;
  }

  @Inject
  QueryAccounts(
      PermissionBackend permissionBackend,
      AccountLoader.Factory accountLoaderFactory,
      AccountQueryBuilder queryBuilder,
      Provider<AccountQueryProcessor> queryProcessorProvider,
      @GerritServerConfig Config cfg) {
    this.permissionBackend = permissionBackend;
    this.accountLoaderFactory = accountLoaderFactory;
    this.queryBuilder = queryBuilder;
    this.queryProcessorProvider = queryProcessorProvider;
    this.suggestFrom = cfg.getInt("suggest", null, "from", 0);
    this.options = EnumSet.noneOf(ListAccountsOption.class);

    if ("off".equalsIgnoreCase(cfg.getString("suggest", null, "accounts"))) {
      suggestConfig = false;
    } else {
      boolean suggest;
      try {
        AccountVisibility av = cfg.getEnum("suggest", null, "accounts", AccountVisibility.ALL);
        suggest = (av != AccountVisibility.NONE);
      } catch (IllegalArgumentException err) {
        suggest = cfg.getBoolean("suggest", null, "accounts", true);
      }
      this.suggestConfig = suggest;
    }
  }

  @Override
  public Response<List<AccountInfo>> apply(TopLevelResource rsrc)
      throws RestApiException, PermissionBackendException {
    if (Strings.isNullOrEmpty(query)) {
      throw new BadRequestException("missing query field");
    }

    if (suggest && (!suggestConfig || query.length() < suggestFrom)) {
      return Response.ok(Collections.emptyList());
    }

    Set<FillOptions> fillOptions = EnumSet.of(FillOptions.ID);
    if (options.contains(ListAccountsOption.DETAILS)) {
      fillOptions.addAll(AccountLoader.DETAILED_OPTIONS);
    }
    boolean modifyAccountCapabilityChecked = false;
    if (options.contains(ListAccountsOption.ALL_EMAILS)) {
      permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
      modifyAccountCapabilityChecked = true;
      fillOptions.add(FillOptions.EMAIL);
      fillOptions.add(FillOptions.SECONDARY_EMAILS);
    }
    if (suggest) {
      fillOptions.addAll(AccountLoader.DETAILED_OPTIONS);
      fillOptions.add(FillOptions.EMAIL);

      if (modifyAccountCapabilityChecked) {
        fillOptions.add(FillOptions.SECONDARY_EMAILS);
      } else {
        try {
          permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
          fillOptions.add(FillOptions.SECONDARY_EMAILS);
        } catch (AuthException e) {
          // Do nothing.
        }
      }
    }
    accountLoader = accountLoaderFactory.create(fillOptions);

    AccountQueryProcessor queryProcessor = queryProcessorProvider.get();
    if (queryProcessor.isDisabled()) {
      throw new MethodNotAllowedException("query disabled");
    }

    if (limit != null) {
      queryProcessor.setUserProvidedLimit(limit);
    }

    if (start != null) {
      queryProcessor.setStart(start);
    }

    Map<Account.Id, AccountInfo> matches = new LinkedHashMap<>();
    try {
      Predicate<AccountState> queryPred;
      if (suggest) {
        queryPred = queryBuilder.defaultQuery(query);
        queryProcessor.setUserProvidedLimit(suggestLimit);
      } else {
        queryPred = queryBuilder.parse(query);
      }
      if (!AccountPredicates.hasActive(queryPred)) {
        // if neither 'is:active' nor 'is:inactive' appears in the query only
        // active accounts should be queried
        queryPred = AccountPredicates.andActive(queryPred);
      }
      QueryResult<AccountState> result = queryProcessor.query(queryPred);
      for (AccountState accountState : result.entities()) {
        Account.Id id = accountState.account().id();
        matches.put(id, accountLoader.get(id));
      }

      accountLoader.fill();

      List<AccountInfo> sorted =
          AccountInfoComparator.ORDER_NULLS_LAST.sortedCopy(matches.values());
      if (!sorted.isEmpty() && result.more()) {
        sorted.get(sorted.size() - 1)._moreAccounts = true;
      }
      return Response.ok(sorted);
    } catch (QueryParseException e) {
      if (suggest) {
        return Response.ok(ImmutableList.of());
      }
      throw new BadRequestException(e.getMessage());
    }
  }
}
