// Copyright (C) 2012 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.sshd.commands;

import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toList;

import com.google.common.base.Strings;
import com.google.gerrit.common.RawInputUtil;
import com.google.gerrit.entities.Account;
import com.google.gerrit.exceptions.EmailException;
import com.google.gerrit.extensions.api.accounts.EmailInput;
import com.google.gerrit.extensions.api.accounts.SshKeyInput;
import com.google.gerrit.extensions.common.EmailInfo;
import com.google.gerrit.extensions.common.HttpPasswordInput;
import com.google.gerrit.extensions.common.Input;
import com.google.gerrit.extensions.common.NameInput;
import com.google.gerrit.extensions.common.SshKeyInfo;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountResource;
import com.google.gerrit.server.account.AccountSshKey;
import com.google.gerrit.server.account.externalids.ExternalIds;
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.restapi.account.AddSshKey;
import com.google.gerrit.server.restapi.account.CreateEmail;
import com.google.gerrit.server.restapi.account.DeleteActive;
import com.google.gerrit.server.restapi.account.DeleteEmail;
import com.google.gerrit.server.restapi.account.DeleteExternalIds;
import com.google.gerrit.server.restapi.account.DeleteSshKey;
import com.google.gerrit.server.restapi.account.GetEmails;
import com.google.gerrit.server.restapi.account.GetSshKeys;
import com.google.gerrit.server.restapi.account.PutActive;
import com.google.gerrit.server.restapi.account.PutHttpPassword;
import com.google.gerrit.server.restapi.account.PutName;
import com.google.gerrit.server.restapi.account.PutPreferred;
import com.google.gerrit.sshd.CommandMetaData;
import com.google.gerrit.sshd.SshCommand;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;

/** Set a user's account settings. * */
@CommandMetaData(name = "set-account", description = "Change an account's settings")
final class SetAccountCommand extends SshCommand {

  @Argument(
      index = 0,
      required = true,
      metaVar = "USER",
      usage = "full name, email-address, ssh username or account id")
  private Account.Id id;

  @Option(name = "--full-name", metaVar = "NAME", usage = "display name of the account")
  private String fullName;

  @Option(name = "--active", usage = "set account's state to active")
  private boolean active;

  @Option(name = "--inactive", usage = "set account's state to inactive")
  private boolean inactive;

  @Option(name = "--add-email", metaVar = "EMAIL", usage = "email addresses to add to the account")
  private List<String> addEmails = new ArrayList<>();

  @Option(
      name = "--delete-email",
      metaVar = "EMAIL",
      usage = "email addresses to delete from the account")
  private List<String> deleteEmails = new ArrayList<>();

  @Option(
      name = "--preferred-email",
      metaVar = "EMAIL",
      usage = "a registered email address from the account")
  private String preferredEmail;

  @Option(name = "--add-ssh-key", metaVar = "-|KEY", usage = "public keys to add to the account")
  private List<String> addSshKeys = new ArrayList<>();

  @Option(
      name = "--delete-ssh-key",
      metaVar = "-|KEY",
      usage = "public keys to delete from the account")
  private List<String> deleteSshKeys = new ArrayList<>();

  @Option(
      name = "--http-password",
      metaVar = "PASSWORD",
      usage = "password for HTTP authentication for the account")
  private String httpPassword;

  @Option(name = "--clear-http-password", usage = "clear HTTP password for the account")
  private boolean clearHttpPassword;

  @Option(name = "--generate-http-password", usage = "generate a new HTTP password for the account")
  private boolean generateHttpPassword;

  @Option(
      name = "--delete-external-id",
      metaVar = "EXTERNALID",
      usage = "external id to delete from the account")
  private List<String> externalIdsToDelete = new ArrayList<>();

  @Inject private IdentifiedUser.GenericFactory genericUserFactory;

  @Inject private CreateEmail createEmail;

  @Inject private DeleteExternalIds deleteExternalIds;

  @Inject private GetEmails getEmails;

  @Inject private DeleteEmail deleteEmail;

  @Inject private PutPreferred putPreferred;

  @Inject private PutName putName;

  @Inject private PutHttpPassword putHttpPassword;

  @Inject private PutActive putActive;

  @Inject private DeleteActive deleteActive;

  @Inject private AddSshKey addSshKey;

  @Inject private GetSshKeys getSshKeys;

  @Inject private DeleteSshKey deleteSshKey;

  @Inject private PermissionBackend permissionBackend;

  @Inject private Provider<CurrentUser> userProvider;

  @Inject private ExternalIds externalIds;

  private AccountResource rsrc;

  @Override
  public void run() throws Exception {
    enableGracefulStop();
    user = genericUserFactory.create(id);

    validate();
    setAccount();
  }

  private void validate() throws UnloggedFailure {
    PermissionBackend.WithUser userPermission = permissionBackend.user(userProvider.get());

    boolean isAdmin = userPermission.testOrFalse(GlobalPermission.ADMINISTRATE_SERVER);
    boolean canModifyAccount =
        isAdmin || userPermission.testOrFalse(GlobalPermission.MODIFY_ACCOUNT);

    if (!user.hasSameAccountId(userProvider.get()) && !canModifyAccount) {
      throw die(
          "Setting another user's account information requries 'modify account' or 'administrate server' capabilities.");
    }
    if (active || inactive) {
      if (!canModifyAccount) {
        throw die(
            "--active and --inactive require 'modify account' or 'administrate server' capabilities.");
      }
      if (active && inactive) {
        throw die("--active and --inactive options are mutually exclusive.");
      }
    }

    if (generateHttpPassword && clearHttpPassword) {
      throw die("--generate-http-password and --clear-http-password are mutually exclusive.");
    }
    if (!Strings.isNullOrEmpty(httpPassword)) { // gave --http-password
      if (!isAdmin) {
        throw die("--http-password requires 'administrate server' capabilities.");
      }
      if (generateHttpPassword) {
        throw die("--http-password and --generate-http-password options are mutually exclusive.");
      }
      if (clearHttpPassword) {
        throw die("--http-password and --clear-http-password options are mutually exclusive.");
      }
    }
    if (addSshKeys.contains("-") && deleteSshKeys.contains("-")) {
      throw die("Only one option may use the stdin");
    }
    if (deleteSshKeys.contains("ALL")) {
      deleteSshKeys = Collections.singletonList("ALL");
    }
    if (deleteEmails.contains("ALL")) {
      deleteEmails = Collections.singletonList("ALL");
    }
    if (deleteEmails.contains(preferredEmail)) {
      throw die(
          "--preferred-email and --delete-email options are mutually "
              + "exclusive for the same email address.");
    }
    if (externalIdsToDelete.contains("ALL")) {
      externalIdsToDelete = Collections.singletonList("ALL");
    }
  }

  private void setAccount() throws Failure {
    user = genericUserFactory.create(id);
    rsrc = new AccountResource(user.asIdentifiedUser());
    try {
      for (String email : addEmails) {
        addEmail(email);
      }

      for (String email : deleteEmails) {
        deleteEmail(email);
      }

      if (preferredEmail != null) {
        putPreferred(preferredEmail);
      }

      if (fullName != null) {
        NameInput in = new NameInput();
        in.name = fullName;
        putName.apply(rsrc, in);
      }

      if (httpPassword != null || clearHttpPassword || generateHttpPassword) {
        HttpPasswordInput in = new HttpPasswordInput();
        in.httpPassword = httpPassword;
        if (generateHttpPassword) {
          in.generate = true;
        }
        Response<String> resp = putHttpPassword.apply(rsrc, in);
        if (generateHttpPassword) {
          stdout.print("New password: " + resp.value() + "\n");
        }
      }

      if (active) {
        putActive.apply(rsrc, null);
      } else if (inactive) {
        try {
          deleteActive.apply(rsrc, null);
        } catch (ResourceNotFoundException e) {
          // user is already inactive
        }
      }

      addSshKeys = readSshKey(addSshKeys);
      if (!addSshKeys.isEmpty()) {
        addSshKeys(addSshKeys);
      }

      deleteSshKeys = readSshKey(deleteSshKeys);
      if (!deleteSshKeys.isEmpty()) {
        deleteSshKeys(deleteSshKeys);
      }

      for (String externalId : externalIdsToDelete) {
        deleteExternalId(externalId);
      }
    } catch (RestApiException e) {
      throw die(e.getMessage());
    } catch (Exception e) {
      throw new Failure(1, "unavailable", e);
    }
  }

  private void addSshKeys(List<String> sshKeys)
      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
    for (String sshKey : sshKeys) {
      SshKeyInput in = new SshKeyInput();
      in.raw = RawInputUtil.create(sshKey.getBytes(UTF_8), "text/plain");
      addSshKey.apply(rsrc, in);
    }
  }

  private void deleteSshKeys(List<String> sshKeys) throws Exception {
    List<SshKeyInfo> infos = getSshKeys.apply(rsrc).value();
    if (sshKeys.contains("ALL")) {
      for (SshKeyInfo i : infos) {
        deleteSshKey(i);
      }
    } else {
      for (String sshKey : sshKeys) {
        for (SshKeyInfo i : infos) {
          if (sshKey.trim().equals(i.sshPublicKey) || sshKey.trim().equals(i.comment)) {
            deleteSshKey(i);
          }
        }
      }
    }
  }

  private void deleteSshKey(SshKeyInfo i)
      throws AuthException, RepositoryNotFoundException, IOException, ConfigInvalidException,
          PermissionBackendException {
    AccountSshKey sshKey = AccountSshKey.create(user.getAccountId(), i.seq, i.sshPublicKey);
    deleteSshKey.apply(new AccountResource.SshKey(user.asIdentifiedUser(), sshKey), null);
  }

  private void addEmail(String email)
      throws UnloggedFailure, RestApiException, IOException, ConfigInvalidException,
          PermissionBackendException {
    EmailInput in = new EmailInput();
    in.email = email;
    in.noConfirmation = true;
    try {
      createEmail.apply(rsrc, IdString.fromDecoded(email), in);
    } catch (EmailException e) {
      throw die(e.getMessage());
    }
  }

  private void deleteEmail(String email) throws Exception {
    if (email.equals("ALL")) {
      List<EmailInfo> emails = getEmails.apply(rsrc).value();
      for (EmailInfo e : emails) {
        deleteEmail.apply(new AccountResource.Email(user.asIdentifiedUser(), e.email), new Input());
      }
    } else {
      deleteEmail.apply(new AccountResource.Email(user.asIdentifiedUser(), email), new Input());
    }
  }

  private void putPreferred(String email) throws Exception {
    for (EmailInfo e : getEmails.apply(rsrc).value()) {
      if (e.email.equals(email)) {
        putPreferred.apply(new AccountResource.Email(user.asIdentifiedUser(), email), null);
        return;
      }
    }
    stderr.println("preferred email not found: " + email);
  }

  private List<String> readSshKey(List<String> sshKeys)
      throws UnsupportedEncodingException, IOException {
    if (!sshKeys.isEmpty()) {
      int idx = sshKeys.indexOf("-");
      if (idx >= 0) {
        StringBuilder sshKey = new StringBuilder();
        BufferedReader br = new BufferedReader(new InputStreamReader(in, UTF_8));
        String line;
        while ((line = br.readLine()) != null) {
          sshKey.append(line).append("\n");
        }
        sshKeys.set(idx, sshKey.toString());
      }
    }
    return sshKeys;
  }

  private void deleteExternalId(String externalId)
      throws IOException, RestApiException, ConfigInvalidException, PermissionBackendException {
    List<String> ids;
    if (externalId.equals("ALL")) {
      ids =
          externalIds.byAccount(rsrc.getUser().getAccountId()).stream()
              .map(e -> e.key().get())
              .collect(toList());
      if (ids.isEmpty()) {
        throw new ResourceNotFoundException("Account has no external Ids");
      }
    } else {
      ids = Collections.singletonList(externalId);
    }
    deleteExternalIds.apply(rsrc, ids);
  }
}
