/*-
 * -\-\-
 * Helios Services
 * --
 * Copyright (C) 2016 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.helios.servicescommon;

import static com.google.common.base.Charsets.UTF_8;
import static com.google.common.base.Optional.fromNullable;
import static java.util.concurrent.TimeUnit.SECONDS;
import static net.sourceforge.argparse4j.impl.Arguments.SUPPRESS;
import static net.sourceforge.argparse4j.impl.Arguments.append;
import static net.sourceforge.argparse4j.impl.Arguments.fileType;
import static net.sourceforge.argparse4j.impl.Arguments.storeTrue;

import com.google.common.io.CharStreams;
import com.spotify.helios.common.LoggingConfig;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import net.sourceforge.argparse4j.ArgumentParsers;
import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Argument;
import net.sourceforge.argparse4j.inf.ArgumentParser;
import net.sourceforge.argparse4j.inf.ArgumentParserException;
import net.sourceforge.argparse4j.inf.Namespace;

/**
 * Handles parsing common commandline arguments for the agent and master.
 */
public class ServiceParser {

  private final Namespace options;

  private final Argument nameArg;
  private final Argument sentryDsnArg;
  private final Argument domainArg;
  private final Argument serviceRegistryArg;
  private final Argument serviceRegistrarPluginArg;
  private final Argument zooKeeperConnectStringArg;
  private final Argument zooKeeperSessiontimeoutArg;
  private final Argument zooKeeperConnectiontimeoutArg;
  private final Argument zooKeeperClusterId;
  private final Argument noZooKeeperRegistrationArg;
  private final Argument zooKeeperEnableAcls;
  private final Argument zooKeeperAclMasterUser;
  private final Argument zooKeeperAclAgentUser;
  private final Argument noMetricsArg;
  private final Argument statsdHostPortArg;
  private final Argument verboseArg;
  private final Argument syslogArg;
  private final Argument logconfigArg;
  private final Argument noLogSetupArg;
  private final Argument kafkaArg;
  private final Argument pubsubTopicPrefixArg;
  private final Argument stateDirArg;

  // ffwd arguments:
  private final Argument ffwdEnabled;
  private final Argument ffwdAddress;
  private final Argument ffwdInterval;
  private final Argument ffwdMetricKey;

  public ServiceParser(final String programName, final String description, final String... args)
      throws ArgumentParserException {

    final ArgumentParser parser = ArgumentParsers.newArgumentParser(programName)
        .defaultHelp(true)
        .description(description);

    nameArg = parser.addArgument("--name")
        .setDefault(getHostName())
        .help("hostname to register as");

    domainArg = parser.addArgument("--domain")
        .setDefault(ResolverConfReader.getDomainFromResolverConf("/etc/resolv.conf"))
        .help("Service registration domain.");

    serviceRegistryArg = parser.addArgument("--service-registry")
        .help("Service registry address. Overrides domain.");

    serviceRegistrarPluginArg = parser.addArgument("--service-registrar-plugin")
        .type(fileType().verifyExists().verifyCanRead())
        .help("Service registration plugin.");

    zooKeeperConnectStringArg = parser.addArgument("--zk")
        .setDefault("localhost:2181")
        .help("zookeeper connection string");

    zooKeeperSessiontimeoutArg = parser.addArgument("--zk-session-timeout")
        .type(Integer.class)
        .setDefault((int) SECONDS.toMillis(60))
        .help("zookeeper session timeout");

    zooKeeperConnectiontimeoutArg = parser.addArgument("--zk-connection-timeout")
        .type(Integer.class)
        .setDefault((int) SECONDS.toMillis(15))
        .help("zookeeper connection timeout");

    zooKeeperClusterId = parser.addArgument("--zk-cluster-id")
        .type(String.class)
        .setDefault((String) null)
        .help("Optional cluster ID to ensure we are connected to the right cluster");

    noZooKeeperRegistrationArg = parser.addArgument("--no-zk-registration")
        .setDefault(SUPPRESS)
        .action(storeTrue())
        .help("Do not register this master in zookeeper. Useful for debugging.");

    zooKeeperEnableAcls = parser.addArgument("--zk-enable-acls")
        .action(storeTrue())
        .setDefault(false)
        .help("Enable zookeeper ACLs.");

    zooKeeperAclMasterUser = parser.addArgument("--zk-acl-master-user")
        .type(String.class)
        .setDefault("helios-master")
        .help("zookeeper ACL username used for masters.");

    zooKeeperAclAgentUser = parser.addArgument("--zk-acl-agent-user")
        .type(String.class)
        .setDefault("helios-agent")
        .help("zookeeper ACL username used for agents.");

    noMetricsArg = parser.addArgument("--no-metrics")
        .setDefault(SUPPRESS)
        .action(storeTrue())
        .help("Turn off all collection and reporting of metrics");

    statsdHostPortArg = parser.addArgument("--statsd-host-port")
        .setDefault((String) null)
        .help("host:port of where to send statsd metrics "
              + "(to be useful, --no-metrics must *NOT* be specified)");

    verboseArg = parser.addArgument("-v", "--verbose")
        .action(Arguments.count());

    syslogArg = parser.addArgument("--syslog")
        .help("Log to syslog.")
        .action(storeTrue());

    logconfigArg = parser.addArgument("--logconfig")
        .type(fileType().verifyExists().verifyCanRead())
        .help("Logback configuration file.");

    noLogSetupArg = parser.addArgument("--no-log-setup")
        .action(storeTrue())
        .help(SUPPRESS);

    sentryDsnArg = parser.addArgument("--sentry-dsn")
        .setDefault((String) null)
        .help("The sentry data source name");

    kafkaArg = parser.addArgument("--kafka")
        .action(append())
        .setDefault(new ArrayList<String>())
        .help("Kafka brokers to bootstrap with");

    pubsubTopicPrefixArg = parser.addArgument("--pubsub-topic-prefix")
        .action(append())
        .setDefault(new ArrayList<String>())
        .help("Google pubsub topic prefixes to send to");

    stateDirArg = parser.addArgument("--state-dir")
        .setDefault(".")
        .help("Directory for persisting state locally.");

    ffwdEnabled = parser.addArgument("--ffwd-enabled")
        .action(storeTrue())
        .setDefault(false)
        .help("Enables reporting of metrics to FastForward. "
              + "Only functional when --no-metrics is not set.");

    ffwdAddress = parser.addArgument("--ffwd-address")
        .help("Overrides the default FastForward address, "
              + "should contain host and port like `host:port`.");

    ffwdInterval = parser.addArgument("--ffwd-interval-secs")
        .type(Integer.class)
        .setDefault(30)
        .help("Interval in seconds at which to report metrics to FastForward.");

    ffwdMetricKey = parser.addArgument("--ffwd-key")
        .setDefault(programName)
        .help("Value to use for `key` in metric data sent to FastForward. "
              + "Defaults to '" + programName + "'");

    addArgs(parser);

    try {
      this.options = parser.parseArgs(args);
    } catch (ArgumentParserException e) {
      parser.handleError(e);
      throw e;
    }
  }

  protected void addArgs(final ArgumentParser parser) {
  }

  public Namespace getNamespace() {
    return options;
  }

  public LoggingConfig getLoggingConfig() {
    return new LoggingConfig(options.getInt(verboseArg.getDest()),
        options.getBoolean(syslogArg.getDest()),
        (File) options.get(logconfigArg.getDest()),
        options.getBoolean(noLogSetupArg.getDest()));
  }

  public String getDomain() {
    return options.getString(domainArg.getDest());
  }

  public String getServiceRegistryAddress() {
    return options.getString(serviceRegistryArg.getDest());
  }

  public Path getServiceRegistrarPlugin() {
    final File plugin = options.get(serviceRegistrarPluginArg.getDest());
    return plugin != null ? plugin.toPath() : null;
  }

  public String getZooKeeperConnectString() {
    return options.getString(zooKeeperConnectStringArg.getDest());
  }

  public String getSentryDsn() {
    return options.getString(sentryDsnArg.getDest());
  }

  public Boolean getInhibitMetrics() {
    return fromNullable(options.getBoolean(noMetricsArg.getDest())).or(false);
  }

  public String getName() {
    return options.getString(nameArg.getDest());
  }

  public String getStatsdHostPort() {
    return options.getString(statsdHostPortArg.getDest());
  }

  public int getZooKeeperConnectionTimeoutMillis() {
    return options.getInt(zooKeeperConnectiontimeoutArg.getDest());
  }

  public int getZooKeeperSessionTimeoutMillis() {
    return options.getInt(zooKeeperSessiontimeoutArg.getDest());
  }

  public String getZooKeeperClusterId() {
    return options.getString(zooKeeperClusterId.getDest());
  }

  public Boolean getNoZooKeeperRegistration() {
    return fromNullable(options.getBoolean(noZooKeeperRegistrationArg.getDest())).or(false);
  }

  public boolean getZooKeeperEnableAcls() {
    return options.getBoolean(zooKeeperEnableAcls.getDest());
  }

  public String getZooKeeperAclMasterUser() {
    return options.getString(zooKeeperAclMasterUser.getDest());
  }

  public String getZooKeeperAclAgentUser() {
    return options.getString(zooKeeperAclAgentUser.getDest());
  }

  public List<String> getKafkaBrokers() {
    final List<String> kafkaBrokers = options.getList(kafkaArg.getDest());
    return kafkaBrokers.isEmpty() ? null : kafkaBrokers;
  }

  public List<String> getPubsubPrefixes() {
    return options.getList(pubsubTopicPrefixArg.getDest());
  }

  public Path getStateDirectory() {
    return Paths.get(options.getString(stateDirArg.getDest()));
  }

  private static String getHostName() {
    return exec("uname -n").trim();
  }

  private static String exec(final String command) {
    try {
      final Process process = Runtime.getRuntime().exec(command);
      return CharStreams.toString(new InputStreamReader(process.getInputStream(), UTF_8));
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  protected InetSocketAddress parseSocketAddress(final String addressString) {
    final InetSocketAddress address;
    try {
      final URI u = new URI(addressString);
      address = new InetSocketAddress(u.getHost(), u.getPort());
    } catch (URISyntaxException e) {
      throw new IllegalArgumentException("Bad address: " + addressString, e);
    }
    return address;
  }

  protected FastForwardConfig ffwdConfig(final Namespace options) {
    if (!options.getBoolean(ffwdEnabled.getDest())) {
      return null;
    }

    return new FastForwardConfig(
        Optional.ofNullable(options.getString(ffwdAddress.getDest())),
        options.getInt(ffwdInterval.getDest()),
        options.getString(ffwdMetricKey.getDest()));
  }
}
