/**
 * 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
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * 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 org.jooby.consul;

import java.text.MessageFormat;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

import org.jooby.Env;
import org.jooby.Jooby;

import com.google.inject.Binder;
import com.orbitz.consul.AgentClient;
import com.orbitz.consul.Consul;
import com.orbitz.consul.model.agent.ImmutableRegistration;
import com.orbitz.consul.model.agent.Registration;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;

/**
 * <p>
 * Consul client module.
 * </p>
 *
 * <p>
 * Exports the {@link Consul} client.
 * </p>
 *
 * <p>
 * Also register the application as a service and setup a health check.
 * </p>
 *
 * <h1>usage</h1>
 *
 * <pre>{@code
 * {
 *   use(new Consulby());
 *
 *   get("/myservice/health", req {@literal ->} {
 *     Consul consul = require(Consul.class);
 *     List<ServiceHealth> serviceHealths = consul.healthClient()
 *       .getHealthyServiceInstances("myservice")
 *       .getResponse();
 *     return serviceHealths;
 *   });
 * }
 * }</pre>
 *
 * <h1>configuration</h1>
 *
 * <p>
 * Configuration is done via <code>.conf</code>.
 * </p>
 *
 * <p>
 * For example, one can change the consul endpoint url,
 * change the advertised service host, and disable registration health check:
 * </p>
 *
 * <pre>
 * consul.default.url = "http://consul.internal.domain.com:8500"
 * consul.default.register.host = 10.0.0.2
 * consul.default.register.check = null
 * </pre>
 *
 * <p>
 * or, disable the automatic registration feature completely:
 * </p>
 *
 * <pre>
 * consul.default.register = null
 * </pre>
 *
 * @since 1.1.1
 */
public class Consulby implements Jooby.Module {

  private final String name;

  private Consumer<Consul.Builder> consulBuilderConsumer;
  private Consumer<ImmutableRegistration.Builder> registrationBuilderConsumer;

  /**
   * A new {@link Consulby} instance, with the default config name: <code>default</code>.
   */
  public Consulby() {
    this("default");
  }

  /**
   * <p>
   * A new {@link Consulby} instance, with a provided config name.
   * </p>
   *
   * <p>
   * The module can be instantiated more than one time to allow connecting to many Consul
   * installations:
   * </p>
   *
   * <pre>
   * {
   *   use(new Consulby("consul1"));
   *   use(new Consulby("consul2"));
   * }
   * </pre>
   *
   * <p>
   * Since the module will fallback on the <code>consul.default</code> config prefix,
   * it is possible to only override the desired properties in the <code>.conf</code>,
   * for example, here, disabling health check only for `consul2`:
   * </p>
   *
   * <pre>
   * consul.consul1.url = "http://consul1.internal.domain.com:8500"
   *
   * consul.consul2.url = "http://consul2.internal.domain.com:8500"
   * consul.consul2.register.check = null
   * </pre>
   *
   * @param name A config name
   */
  public Consulby(final String name) {
    this.name = Objects.requireNonNull(name, "A consul config name is required.");
  }

  /**
   * <p>
   * {@link Consul} object can be configured programmatically:
   * </p>
   *
   * <pre>{@code
   * {
   *   use(new Consulby()
   *     .withConsulBuilder(consulBuilder -> {
   *       consulBuilder.withPing(false);
   *       consulBuilder.withBasicAuth("admin", "changeme");
   *     }));
   * }
   * }</pre>
   *
   * @param consulBuilderConsumer A {@link Consumer} that accepts {@link Consul.Builder}
   * @return This {@link Consulby} to allow chaining
   */
  public Consulby withConsulBuilder(final Consumer<Consul.Builder> consulBuilderConsumer) {
    this.consulBuilderConsumer = consulBuilderConsumer;
    return this;
  }

  /**
   * <p>
   * {@link Registration} object can be configured programmatically:
   * </p>
   *
   * <pre>{@code
   * {
   *   use(new Consulby()
   *     .withRegistrationBuilder(registrationBuilder -> {
   *       registrationBuilder.enableTagOverride(true);
   *       registrationBuilder.id("custom-service-id");
   *     }));
   * }
   * }</pre>
   *
   * @param registrationBuilderConsumer A {@link Consumer} that accepts
   *        {@link ImmutableRegistration.Builder}
   * @return This {@link Consulby} to allow chaining
   */
  public Consulby withRegistrationBuilder(
      final Consumer<ImmutableRegistration.Builder> registrationBuilderConsumer) {
    this.registrationBuilderConsumer = registrationBuilderConsumer;
    return this;
  }

  @Override
  public void configure(final Env env, final Config config, final Binder binder) throws Throwable {

    Config consulConfig = config.getConfig("consul.default");
    if (!name.equals("default") && config.hasPath("consul." + name)) {
      consulConfig = config.getConfig("consul." + name).withFallback(consulConfig);
    }

    Consul.Builder consulBuilder = Consul.builder()
        .withUrl(consulConfig.getString("url"));

    if (consulBuilderConsumer != null) {
      consulBuilderConsumer.accept(consulBuilder);
    }

    Consul consul = consulBuilder.build();

    env.onStop(consul::destroy);

    env.serviceKey().generate(Consul.class, name, k -> binder.bind(k).toInstance(consul));

    if (consulConfig.hasPath("register")) {

      Config registerConfig = consulConfig.getConfig("register");

      ImmutableRegistration.Builder registrationBuilder = ImmutableRegistration.builder()
          .name(registerConfig.getString("name"))
          .address(registerConfig.getString("host"))
          .port(registerConfig.getInt("port"))
          .tags(registerConfig.getStringList("tags"))
          .id(UUID.randomUUID().toString());

      if (registerConfig.hasPath("check")) {

        Config checkConfig = registerConfig.getConfig("check");

        String http = MessageFormat.format("http://{0}:{1,number,####}{2}",
            registerConfig.getString("host"),
            registerConfig.getInt("port"),
            checkConfig.getString("path"));

        Registration.RegCheck check = Registration.RegCheck.http(http,
            checkConfig.getDuration("interval", TimeUnit.SECONDS),
            checkConfig.getDuration("timeout", TimeUnit.SECONDS));

        registrationBuilder.check(check);

        String response = checkConfig.getString("response");
        env.router().get(checkConfig.getString("path"), () -> response);
      }

      if (registrationBuilderConsumer != null) {
        registrationBuilderConsumer.accept(registrationBuilder);
      }

      Registration registration = registrationBuilder.build();

      AgentClient agentClient = consul.agentClient();
      env.onStarted(() -> agentClient.register(registration));
      env.onStop(() -> agentClient.deregister(registration.getId()));
    }
  }

  @Override
  public Config config() {
    return ConfigFactory.parseResources(getClass(), "consul.conf");
  }
}
