/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF 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 com.qihoo.qsql.env;

import com.google.common.base.Preconditions;
import com.google.common.io.Files;
import java.io.File;
import java.util.Arrays;
import java.util.Collection;
import java.util.Objects;
import org.elasticsearch.action.admin.cluster.node.info.NodeInfo;
import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse;
import org.elasticsearch.client.Client;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.TransportAddress;
import org.elasticsearch.node.InternalSettingsPreparer;
import org.elasticsearch.node.Node;
import org.elasticsearch.node.NodeValidationException;
import org.elasticsearch.painless.PainlessPlugin;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.transport.Netty4Plugin;

/**
 * Represents a single elastic search node which can run embedded in a java application.
 *
 * <p>Intended for unit and integration tests. Settings and plugins are crafted for Calcite.
 */
public class EmbeddedElasticsearchNode implements AutoCloseable {

  private final Node node;
  private volatile boolean  isStarted;

  private EmbeddedElasticsearchNode(Node node) {
    this.node = Objects.requireNonNull(node, "node");
  }

  /**
   * Creates an instance with existing settings
   * @param settings configuration parameters of ES instance
   * @return instance which needs to be explicitly started (using {@link #start()})
   */
  private static EmbeddedElasticsearchNode create(Settings settings) {
    // ensure PainlessPlugin is installed or otherwise scripted fields would not work
    Node node = new LocalNode(settings, Arrays.asList(Netty4Plugin.class, PainlessPlugin.class));
    return new EmbeddedElasticsearchNode(node);
  }

  /**
   * Creates elastic node as single member of a cluster. Node will not be started
   * unless {@link #start()} is explicitly called.
   * <p>Need {@code synchronized} because of static caches inside ES (which are not thread safe).
   * @return instance which needs to be explicitly started (using {@link #start()})
   */
  public static synchronized EmbeddedElasticsearchNode create() {
    File data = Files.createTempDir();
    data.deleteOnExit();
    File home = Files.createTempDir();
    home.deleteOnExit();

    Settings settings = Settings.builder()
        .put("node.name", "fake-elastic")
        .put("path.home", home.getAbsolutePath())
        .put("path.data", data.getAbsolutePath())
        .put("http.type", "netty4")
        // allow multiple instances to run in parallel
        .put("transport.tcp.port", 0)
        .put("http.port", 9025)
        .put("network.host", "localhost")
        .build();

    return create(settings);
  }

  /**
   * Starts current node
   */
  public void start() {
    Preconditions.checkState(!isStarted, "already started");
    try {
      node.start();
      this.isStarted = true;
    } catch (NodeValidationException e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Returns current address to connect to with HTTP client.
   * @return hostname/port for HTTP connection
   */
  public TransportAddress httpAddress() {
    Preconditions.checkState(isStarted, "node is not started");

    NodesInfoResponse response =  client().admin().cluster().prepareNodesInfo()
        .execute().actionGet();
    if (response.getNodes().size() != 1) {
      throw new IllegalStateException("Expected single node but got "
          + response.getNodes().size());
    }
    NodeInfo node = response.getNodes().get(0);
    return node.getHttp().address().boundAddresses()[0];
  }

  /**
   * Exposes elastic
   * <a href="https://www.elastic.co/guide/en/elasticsearch/client/java-api/current/transport-client.html">transport client</a>
   * (use of HTTP client is preferred).
   *
   * @return current elastic search client
   */
  public Client client() {
    Preconditions.checkState(isStarted, "node is not started");
    return node.client();
  }

  @Override public void close() throws Exception {
    node.close();
    // cleanup data dirs
    for (String name: Arrays.asList("path.data", "path.home")) {
      if (node.settings().get(name) != null) {
        File file = new File(node.settings().get(name));
        if (file.exists()) {
          file.delete();
        }
      }
    }
  }

  /**
   * Having separate class to expose (protected) constructor which allows to install
   * different plugins. In our case it is {@code GroovyPlugin} for scripted fields
   * like {@code loc[0]} or {@code loc[1]['foo']}.
   *
   * <p>This class is intended solely for tests
   */
  private static class LocalNode extends Node {

    private LocalNode(Settings settings, Collection<Class<? extends Plugin>> classpathPlugins) {
      super(InternalSettingsPreparer.prepareEnvironment(settings, null),
          classpathPlugins);
    }
  }
}

// End EmbeddedElasticsearchNode.java