// Copyright (C) 2015 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.gpg.server;

import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GPGKEY;
import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.base.CharMatcher;
import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
import com.google.common.io.BaseEncoding;
import com.google.gerrit.extensions.common.GpgKeyInfo;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ChildCollection;
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.RestReadView;
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.gpg.BouncyCastleUtil;
import com.google.gerrit.gpg.CheckResult;
import com.google.gerrit.gpg.Fingerprint;
import com.google.gerrit.gpg.GerritPublicKeyChecker;
import com.google.gerrit.gpg.PublicKeyChecker;
import com.google.gerrit.gpg.PublicKeyStore;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.AccountResource;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.eclipse.jgit.util.NB;

@Singleton
public class GpgKeys implements ChildCollection<AccountResource, GpgKey> {
  private static final FluentLogger logger = FluentLogger.forEnclosingClass();

  private final DynamicMap<RestView<GpgKey>> views;
  private final Provider<CurrentUser> self;
  private final Provider<PublicKeyStore> storeProvider;
  private final GerritPublicKeyChecker.Factory checkerFactory;
  private final ExternalIds externalIds;

  @Inject
  GpgKeys(
      DynamicMap<RestView<GpgKey>> views,
      Provider<CurrentUser> self,
      Provider<PublicKeyStore> storeProvider,
      GerritPublicKeyChecker.Factory checkerFactory,
      ExternalIds externalIds) {
    this.views = views;
    this.self = self;
    this.storeProvider = storeProvider;
    this.checkerFactory = checkerFactory;
    this.externalIds = externalIds;
  }

  @Override
  public ListGpgKeys list() throws ResourceNotFoundException, AuthException {
    return new ListGpgKeys();
  }

  @Override
  public GpgKey parse(AccountResource parent, IdString id)
      throws ResourceNotFoundException, PGPException, IOException {
    checkVisible(self, parent);

    ExternalId gpgKeyExtId = findGpgKey(id.get(), getGpgExtIds(parent));
    byte[] fp = parseFingerprint(gpgKeyExtId);
    try (PublicKeyStore store = storeProvider.get()) {
      long keyId = keyId(fp);
      for (PGPPublicKeyRing keyRing : store.get(keyId)) {
        PGPPublicKey key = keyRing.getPublicKey();
        if (Arrays.equals(key.getFingerprint(), fp)) {
          return new GpgKey(parent.getUser(), keyRing);
        }
      }
    }

    throw new ResourceNotFoundException(id);
  }

  static ExternalId findGpgKey(String str, Iterable<ExternalId> existingExtIds)
      throws ResourceNotFoundException {
    str = CharMatcher.whitespace().removeFrom(str).toUpperCase();
    if ((str.length() != 8 && str.length() != 40)
        || !CharMatcher.anyOf("0123456789ABCDEF").matchesAllOf(str)) {
      throw new ResourceNotFoundException(str);
    }
    ExternalId gpgKeyExtId = null;
    for (ExternalId extId : existingExtIds) {
      String fpStr = extId.key().id();
      if (!fpStr.endsWith(str)) {
        continue;
      } else if (gpgKeyExtId != null) {
        throw new ResourceNotFoundException("Multiple keys found for " + str);
      }
      gpgKeyExtId = extId;
      if (str.length() == 40) {
        break;
      }
    }
    if (gpgKeyExtId == null) {
      throw new ResourceNotFoundException(str);
    }
    return gpgKeyExtId;
  }

  static byte[] parseFingerprint(ExternalId gpgKeyExtId) {
    return BaseEncoding.base16().decode(gpgKeyExtId.key().id());
  }

  @Override
  public DynamicMap<RestView<GpgKey>> views() {
    return views;
  }

  public class ListGpgKeys implements RestReadView<AccountResource> {
    @Override
    public Response<Map<String, GpgKeyInfo>> apply(AccountResource rsrc)
        throws PGPException, IOException, ResourceNotFoundException {
      checkVisible(self, rsrc);
      Map<String, GpgKeyInfo> keys = new HashMap<>();
      try (PublicKeyStore store = storeProvider.get()) {
        for (ExternalId extId : getGpgExtIds(rsrc)) {
          byte[] fp = parseFingerprint(extId);
          boolean found = false;
          for (PGPPublicKeyRing keyRing : store.get(keyId(fp))) {
            if (Arrays.equals(keyRing.getPublicKey().getFingerprint(), fp)) {
              found = true;
              GpgKeyInfo info =
                  toJson(
                      keyRing.getPublicKey(), checkerFactory.create(rsrc.getUser(), store), store);
              keys.put(info.id, info);
              info.id = null;
              break;
            }
          }
          if (!found) {
            logger.atWarning().log(
                "No public key stored for fingerprint %s", Fingerprint.toString(fp));
          }
        }
      }
      return Response.ok(keys);
    }
  }

  @Singleton
  public static class Get implements RestReadView<GpgKey> {
    private final Provider<PublicKeyStore> storeProvider;
    private final GerritPublicKeyChecker.Factory checkerFactory;

    @Inject
    Get(Provider<PublicKeyStore> storeProvider, GerritPublicKeyChecker.Factory checkerFactory) {
      this.storeProvider = storeProvider;
      this.checkerFactory = checkerFactory;
    }

    @Override
    public Response<GpgKeyInfo> apply(GpgKey rsrc) throws IOException {
      try (PublicKeyStore store = storeProvider.get()) {
        return Response.ok(
            toJson(
                rsrc.getKeyRing().getPublicKey(),
                checkerFactory.create().setExpectedUser(rsrc.getUser()),
                store));
      }
    }
  }

  private Iterable<ExternalId> getGpgExtIds(AccountResource rsrc) throws IOException {
    return externalIds.byAccount(rsrc.getUser().getAccountId(), SCHEME_GPGKEY);
  }

  private static long keyId(byte[] fp) {
    return NB.decodeInt64(fp, fp.length - 8);
  }

  static void checkVisible(Provider<CurrentUser> self, AccountResource rsrc)
      throws ResourceNotFoundException {
    if (!BouncyCastleUtil.havePGP()) {
      throw new ResourceNotFoundException("GPG not enabled");
    }
    if (!self.get().hasSameAccountId(rsrc.getUser())) {
      throw new ResourceNotFoundException();
    }
  }

  public static GpgKeyInfo toJson(PGPPublicKey key, CheckResult checkResult) throws IOException {
    GpgKeyInfo info = new GpgKeyInfo();

    if (key != null) {
      info.id = PublicKeyStore.keyIdToString(key.getKeyID());
      info.fingerprint = Fingerprint.toString(key.getFingerprint());
      Iterator<String> userIds = key.getUserIDs();
      info.userIds = ImmutableList.copyOf(userIds);

      try (ByteArrayOutputStream out = new ByteArrayOutputStream(4096)) {
        try (ArmoredOutputStream aout = new ArmoredOutputStream(out)) {
          // This is not exactly the key stored in the store, but is equivalent. In
          // particular, it will have a Bouncy Castle version string. The armored
          // stream reader in PublicKeyStore doesn't give us an easy way to extract
          // the original ASCII armor.
          key.encode(aout);
        }
        info.key = new String(out.toByteArray(), UTF_8);
      }
    }

    info.status = checkResult.getStatus();
    info.problems = checkResult.getProblems();

    return info;
  }

  static GpgKeyInfo toJson(PGPPublicKey key, PublicKeyChecker checker, PublicKeyStore store)
      throws IOException {
    return toJson(key, checker.setStore(store).check(key));
  }

  public static void toJson(GpgKeyInfo info, CheckResult checkResult) {
    info.status = checkResult.getStatus();
    info.problems = checkResult.getProblems();
  }
}
