/*-
 * -\-\-
 * DBeam Core
 * --
 * Copyright (C) 2016 - 2018 Spotify AB
 * --
 * 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.spotify.dbeam.options;

import com.google.api.client.googleapis.util.Utils;
import com.google.api.client.http.HttpRequestInitializer;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.services.cloudkms.v1.CloudKMS;
import com.google.api.services.cloudkms.v1.CloudKMSScopes;
import com.google.api.services.cloudkms.v1.model.DecryptRequest;
import com.google.api.services.cloudkms.v1.model.DecryptResponse;
import com.google.auth.Credentials;
import com.google.auth.http.HttpCredentialsAdapter;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auto.value.AutoValue;
import com.google.cloud.ServiceOptions;
import com.google.common.base.CharMatcher;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Optional;
import java.util.Properties;

@AutoValue
public abstract class KmsDecrypter {

  private static final String KEYRING;
  private static final String KEY;
  private static final String LOCATION;
  private static final String PROJECT;

  static {
    Properties p = System.getProperties();
    KEYRING = p.getProperty("KMS_KEYRING", "dbeam");
    KEY = p.getProperty("KMS_KEY", "default");
    LOCATION = p.getProperty("KMS_LOCATION", "global");
    PROJECT = p.getProperty("KMS_PROJECT");
  }

  /**
   * Create a new {@link Builder} for customizing the {@link KmsDecrypter} configuration.
   *
   * @return a configured {@link KmsDecrypter}
   */
  public static Builder decrypter() {
    return new AutoValue_KmsDecrypter.Builder()
        .location(LOCATION)
        .key(KEY)
        .keyring(KEYRING)
        .project(Optional.ofNullable(PROJECT));
  }

  /** The location of the KMS key to use for decryption. */
  abstract String location();

  /** The ring of the KMS key to use for decryption. */
  abstract String keyring();

  /** The name of the KMS key to use for decryption. */
  abstract String key();

  /**
   * The GCP project KMS key to use for decryption. Will be detected from credentials or gcloud sdk
   * if not set.
   */
  abstract Optional<String> project();

  /**
   * The {@link HttpTransport} to use for the default credentials and KMS client. Default will be
   * used if not set.
   */
  abstract Optional<HttpTransport> transport();

  /** The {@link Credentials} to use for the KMS client. Default will be used if not set. */
  abstract Optional<Credentials> credentials();

  public abstract Builder builder();

  @AutoValue.Builder
  public abstract static class Builder {
    public abstract Builder location(String location);

    public abstract Builder keyring(String keyring);

    public abstract Builder key(String keyring);

    public abstract Builder project(Optional<String> project);

    public abstract Builder transport(HttpTransport transport);

    public abstract Builder credentials(Credentials credentials);

    public abstract KmsDecrypter build();
  }

  /** Decrypt a base64 encoded cipher text string. */
  String decrypt(final String base64Ciphertext) throws IOException {
    return StandardCharsets.UTF_8.decode(decryptBinary(base64Ciphertext)).toString();
  }

  /**
   * Decrypt a base64 encoded cipher text string.
   *
   * @return A {@link ByteBuffer} with the raw contents.
   */
  ByteBuffer decryptBinary(final String base64Ciphertext) throws IOException {
    final String project = project().orElseGet(ServiceOptions::getDefaultProjectId);
    final String keyName =
        String.format(
            "projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s",
            project, location(), keyring(), key());

    final DecryptResponse response =
        kms()
            .projects()
            .locations()
            .keyRings()
            .cryptoKeys()
            .decrypt(
                keyName,
                new DecryptRequest()
                    .setCiphertext(CharMatcher.whitespace().removeFrom(base64Ciphertext)))
            .execute();
    return ByteBuffer.wrap(Base64.getDecoder().decode(response.getPlaintext()));
  }

  private CloudKMS kms() throws IOException {
    final HttpTransport transport = transport().orElseGet(Utils::getDefaultTransport);
    final JsonFactory jsonFactory = Utils.getDefaultJsonFactory();
    final Credentials credentials =
        credentials().isPresent()
            ? credentials().get()
            : GoogleCredentials.getApplicationDefault();
    return KmsDecrypter.kms(
        transport, jsonFactory, new HttpCredentialsAdapter(scoped(credentials)));
  }

  private static CloudKMS kms(
      final HttpTransport transport,
      final JsonFactory jsonFactory,
      final HttpRequestInitializer httpRequestInitializer) {
    return new CloudKMS.Builder(
        transport, jsonFactory, httpRequestInitializer)
        .setApplicationName("dbeam")
        .build();
  }

  private static Credentials scoped(final Credentials credentials) {
    if (credentials instanceof GoogleCredentials) {
      return ((GoogleCredentials)credentials).createScoped(CloudKMSScopes.all());
    }
    return credentials;
  }
}