/*
 * Copyright (c) 2014 Red Hat, Inc. and others
 *
 * Red Hat licenses this file to you 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 io.vertx.config.vault.utils;

import io.vertx.core.json.JsonObject;
import io.vertx.core.net.JksOptions;
import io.vertx.core.net.PemKeyCertOptions;
import org.apache.commons.exec.*;
import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;

import static org.awaitility.Awaitility.await;
import static org.hamcrest.core.Is.is;

/**
 * @author <a href="http://escoffier.me">Clement Escoffier</a>
 */
public class VaultProcess {


  public static final String VAULT_VERSION = "1.1.2";
  public static final String CA_CERT_ARG = "-ca-cert=target/vault/config/ssl/cert.pem";
  private File executable;
  private String unseal;
  private String token;
  private ExecuteWatchdog watchDog;
  private String backend;

  public VaultProcess() {
    executable = VaultDownloader.download();
    try {
      Certificates.createVaultCertAndKey();
      Certificates.createClientCertAndKey();
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  public void initAndUnsealVault() {
    backend = null;
    File stored = new File("target/vault/file");
    FileUtils.deleteQuietly(stored);
    startServer();
    init();
    unseal();
    login();
    enableSecretEngineKvV1();
    enableSecretEngineKvV2();
  }

  private void init() {
    String line = executable.getAbsolutePath() + " operator init -key-shares=1 -key-threshold=1 " + CA_CERT_ARG;
    System.out.println(">> " + line);
    CommandLine parse = CommandLine.parse(line);
    DefaultExecutor executor = new DefaultExecutor();
    PumpStreamHandler pump = new PumpStreamHandler(new VaultOutputStream().addExtractor(
      l -> {
        if (l.contains("Unseal Key 1:")) {
          unseal = l.replace("Unseal Key 1: ", "").trim();
        } else if (l.contains("Initial Root Token:")) {
          token = l.replace("Initial Root Token: ", "").trim();
        }
      }
    ), System.err);

    ExecuteWatchdog watchDog = new ExecuteWatchdog(ExecuteWatchdog.INFINITE_TIMEOUT);
    executor.setWatchdog(watchDog);
    executor.setStreamHandler(pump);
    try {
      executor.execute(parse);
    } catch (IOException e) {
      throw new RuntimeException(e);
    }

//    await().until(() -> token != null);
//    await().until(() -> unseal != null);

    System.out.println("Vault Server initialized (but sealed)");
    System.out.println("Root token: " + token);
    System.out.println("Unseal key: " + unseal);
  }

  public void unseal() {
    run("operator unseal " + CA_CERT_ARG + " " + unseal);
    System.out.println("Vault Server ready !");
  }

  public void login() {
    run("login " + CA_CERT_ARG + " " + token );
    System.out.println("Vault secret engine V1 is enabled !");
  }

  public void enableSecretEngineKvV1() {
    run("secrets enable " + CA_CERT_ARG + " -path=secret kv");
    System.out.println("Vault secret engine V1 is enabled !");
  }

  public void enableSecretEngineKvV2() {
    run("secrets enable " + CA_CERT_ARG + " -path=secret-v2 kv-v2");
    System.out.println("Vault secret engine V2 is enabled !");
  }

  private void startServer() {
    String line = executable.getAbsolutePath() + " server -config=src/test/resources/config.json";
    System.out.println(">> " + line);
    CommandLine parse = CommandLine.parse(line);
    DefaultExecutor executor = new DefaultExecutor();
    DefaultExecuteResultHandler resultHandler = new DefaultExecuteResultHandler();
    AtomicBoolean ready = new AtomicBoolean();
    PumpStreamHandler pump = new PumpStreamHandler(new VaultOutputStream().addExtractor(
      l -> {
        if (l.contains("Vault server started!")) {
          ready.set(true);
        }
      }
    ), System.err);

    watchDog = new ExecuteWatchdog(ExecuteWatchdog.INFINITE_TIMEOUT);
    executor.setWatchdog(watchDog);
    executor.setStreamHandler(pump);
    try {
      executor.execute(parse, resultHandler);
    } catch (IOException e) {
      throw new RuntimeException(e);
    }

    await().untilAtomic(ready, is(true));
    System.out.println("Vault Server ready - but not yet initialized");
  }


  public void shutdown() {
    if (watchDog != null) {
      watchDog.destroyProcess();
    }
  }

  public boolean run(String args) {
    String cli = executable.getAbsolutePath() + " " + args;
    System.out.println(">> " + cli);
    CommandLine parse = CommandLine.parse(cli);
    DefaultExecutor executor = new DefaultExecutor();
    executor.setExitValue(0);
    try {
      return executor.execute(parse) == 0;
    } catch (IOException e) {
      e.printStackTrace();
      return false;
    }
  }

  public boolean isRunning() {
    return run("status " + CA_CERT_ARG);
  }

  public String getHost() {
    return "127.0.0.1";
  }

  public int getPort() {
    return 8200;
  }

  public JsonObject getConfiguration() {
    JsonObject config = new JsonObject();
    config.put("host", getHost());
    config.put("port", getPort());
    config.put("ssl", true);
    PemKeyCertOptions options = new PemKeyCertOptions()
      .addCertPath("target/vault/config/ssl/client-cert.pem")
      .addKeyPath("target/vault/config/ssl/client-privatekey.pem");
    config.put("pemKeyCertOptions", options.toJson());
    JksOptions jks = new JksOptions()
      .setPath("target/vault/config/ssl/truststore.jks");
    config.put("trustStoreOptions", jks.toJson());
    return config;
  }

  public JsonObject getConfigurationWithRootToken() {
    return getConfiguration().put("token", token);
  }

  public void setupBackendAppRole() {
    if ("appRole".equals(backend)) {
      return;
    }
    // Add a "user" policy for testing purpose.
    run("policy write " + CA_CERT_ARG + " user src/test/resources/acl.hcl");
    run("auth enable " + CA_CERT_ARG + " approle");
    run("write " + CA_CERT_ARG + " auth/approle/role/testrole secret_id_ttl=10m token_ttlc=20m " +
      "token_max_ttl=30m secret_id_num_users=40 policies=user");
    backend = "appRole";
  }

  public void setupBackendCert() {
    if ("cert".equalsIgnoreCase(backend)) {
      return;
    }
    // Add a "user" policy for testing purpose.
    run("policy write " + CA_CERT_ARG + " user src/test/resources/acl.hcl");

    run("auth enable " + CA_CERT_ARG + " cert");
    run("write " + CA_CERT_ARG + " auth/cert/certs/web display_name=web " +
      "policies=web,prod,user certificate=@target/vault/config/ssl/client-cert.pem ttl=3600");

    backend = "cert";
  }

  public void setupBackendUserPass() {
    if ("userpass".equalsIgnoreCase(backend)) {
      return;
    }

    // Add a "user" policy for testing purpose.
    run("policy write " + CA_CERT_ARG + " user src/test/resources/acl.hcl");

    run("auth enable " + CA_CERT_ARG + " userpass");
    run("write " + CA_CERT_ARG + " auth/userpass/users/fake-user password=fake-password policies=user");

    backend = "userpass";
  }

  public String getToken() {
    return token;
  }

  public String getUnsealKey() {
    return unseal;
  }

  public void runAndProcess(String command, Consumer<String> processor) {
    String cli = executable.getAbsolutePath() + " " + command;
    System.out.println(">> " + cli);
    CommandLine parse = CommandLine.parse(cli);
    DefaultExecutor executor = new DefaultExecutor();
    PumpStreamHandler pump = new PumpStreamHandler(new VaultOutputStream().addExtractor(processor), System.err);

    ExecuteWatchdog watchDog = new ExecuteWatchdog(ExecuteWatchdog.INFINITE_TIMEOUT);
    executor.setWatchdog(watchDog);
    executor.setStreamHandler(pump);
    try {
      executor.execute(parse);
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }


  private class VaultOutputStream extends OutputStream {

    StringBuilder last = new StringBuilder();
    List<Consumer<String>> extractors = new ArrayList<>();

    public VaultOutputStream addExtractor(Consumer<String> extractor) {
      extractors.add(extractor);
      return this;
    }

    @Override
    public synchronized void write(int b) throws IOException {
      last.append((char) b);

      if ((char) b == '\n') {
        String line = last.toString();
        System.out.println(line);
        for (Consumer f : extractors) {
          f.accept(line);
        }
        last = new StringBuilder();
      }
    }


  }


}
