/*-
 * -\-\-
 * Helios Tools
 * --
 * 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.cli;

import static com.google.common.base.Objects.equal;
import static com.google.common.base.Predicates.equalTo;
import static com.google.common.base.Predicates.not;
import static com.google.common.collect.Iterables.addAll;
import static com.google.common.collect.Iterables.filter;
import static java.lang.String.format;
import static java.util.Arrays.asList;
import static net.sourceforge.argparse4j.impl.Arguments.SUPPRESS;
import static net.sourceforge.argparse4j.impl.Arguments.append;
import static net.sourceforge.argparse4j.impl.Arguments.storeTrue;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.spotify.helios.cli.command.CliCommand;
import com.spotify.helios.cli.command.DeploymentGroupCreateCommand;
import com.spotify.helios.cli.command.DeploymentGroupInspectCommand;
import com.spotify.helios.cli.command.DeploymentGroupListCommand;
import com.spotify.helios.cli.command.DeploymentGroupRemoveCommand;
import com.spotify.helios.cli.command.DeploymentGroupStatusCommand;
import com.spotify.helios.cli.command.DeploymentGroupStopCommand;
import com.spotify.helios.cli.command.DeploymentGroupWatchCommand;
import com.spotify.helios.cli.command.HostDeregisterCommand;
import com.spotify.helios.cli.command.HostListCommand;
import com.spotify.helios.cli.command.HostRegisterCommand;
import com.spotify.helios.cli.command.JobCreateCommand;
import com.spotify.helios.cli.command.JobDeployCommand;
import com.spotify.helios.cli.command.JobHistoryCommand;
import com.spotify.helios.cli.command.JobInspectCommand;
import com.spotify.helios.cli.command.JobListCommand;
import com.spotify.helios.cli.command.JobRemoveCommand;
import com.spotify.helios.cli.command.JobStartCommand;
import com.spotify.helios.cli.command.JobStatusCommand;
import com.spotify.helios.cli.command.JobStopCommand;
import com.spotify.helios.cli.command.JobUndeployCommand;
import com.spotify.helios.cli.command.JobWatchCommand;
import com.spotify.helios.cli.command.MasterListCommand;
import com.spotify.helios.cli.command.RollingUpdateCommand;
import com.spotify.helios.cli.command.VersionCommand;
import com.spotify.helios.common.LoggingConfig;
import com.spotify.helios.common.Version;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import net.sourceforge.argparse4j.ArgumentParsers;
import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Argument;
import net.sourceforge.argparse4j.inf.ArgumentGroup;
import net.sourceforge.argparse4j.inf.ArgumentParser;
import net.sourceforge.argparse4j.inf.ArgumentParserException;
import net.sourceforge.argparse4j.inf.FeatureControl;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import net.sourceforge.argparse4j.inf.Subparsers;

public class CliParser {

  private static final String NAME_AND_VERSION = "Spotify Helios CLI " + Version.POM_VERSION;
  private static final String TESTED_DOCKER_VERSION = "17.05.0-ce";
  private static final String HELP_ISSUES =
      "Report improvements/bugs at https://github.com/spotify/helios/issues";
  private static final String HELP_WIKI =
      "For documentation see https://github.com/spotify/helios/tree/master/docs";

  private final Namespace options;
  private final CliCommand command;
  private final LoggingConfig loggingConfig;
  private final Subparsers commandParsers;
  private final CliConfig cliConfig;
  private final List<Target> targets;
  private final String username;
  private boolean json;

  public CliParser(final String... args)
      throws ArgumentParserException, IOException, URISyntaxException {

    final ArgumentParser parser = ArgumentParsers.newArgumentParser("helios")
        .defaultHelp(true)
        .version(format("%s%nTested on Docker %s", NAME_AND_VERSION, TESTED_DOCKER_VERSION))
        .description(format("%s%n%n%s%n%s", NAME_AND_VERSION, HELP_ISSUES, HELP_WIKI));

    cliConfig = CliConfig.fromUserConfig(System.getenv());

    final GlobalArgs globalArgs = addGlobalArgs(parser, cliConfig, true);

    commandParsers = parser.addSubparsers()
        .metavar("COMMAND")
        .title("commands");

    setupCommands();

    if (args.length == 0) {
      parser.printHelp();
      throw new ArgumentParserException(parser);
    }

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

    this.command = options.get("command");
    final String username = options.getString(globalArgs.usernameArg.getDest());
    this.username = (username == null) ? cliConfig.getUsername() : username;
    this.json = equal(options.getBoolean(globalArgs.jsonArg.getDest()), true);
    this.loggingConfig = new LoggingConfig(options.getInt(globalArgs.verbose.getDest()),
        false, null,
        options.getBoolean(globalArgs.noLogSetup.getDest()));

    // Merge domains and explicit endpoints into master endpoints
    final List<String> explicitEndpoints = options.getList(globalArgs.masterArg.getDest());
    final List<String> domains = options.getList(globalArgs.domainsArg.getDest());
    final String srvName = options.getString(globalArgs.srvNameArg.getDest());

    // Order of target precedence:
    // 1. endpoints from command line
    // 2. domains from command line
    // 3. endpoints from config file
    // 4. domains from config file

    // TODO (dano): complex, refactor and unit test it
    this.targets = computeTargets(parser, explicitEndpoints, domains, srvName);
  }

  private List<Target> computeTargets(final ArgumentParser parser,
                                      final List<String> explicitEndpoints,
                                      final List<String> domainsArguments,
                                      final String srvName) {

    if (explicitEndpoints != null && !explicitEndpoints.isEmpty()) {
      final List<Target> targets = Lists.newArrayListWithExpectedSize(explicitEndpoints.size());
      for (final String endpoint : explicitEndpoints) {
        targets.add(Target.from(URI.create(endpoint)));
      }
      return targets;
    } else if (domainsArguments != null && !domainsArguments.isEmpty()) {
      final Iterable<String> domains = parseDomains(domainsArguments);
      return Target.from(srvName, domains);
    } else if (!cliConfig.getMasterEndpoints().isEmpty()) {
      final List<URI> cliConfigMasterEndpoints = cliConfig.getMasterEndpoints();
      final List<Target> targets = Lists.newArrayListWithExpectedSize(
          cliConfigMasterEndpoints.size());
      for (final URI endpoint : cliConfigMasterEndpoints) {
        targets.add(Target.from(endpoint));
      }
      return targets;
    } else if (!cliConfig.getDomainsString().isEmpty()) {
      final Iterable<String> domains = parseDomainsString(cliConfig.getDomainsString());
      return Target.from(srvName, domains);
    }

    handleError(parser, new ArgumentParserException(
        "no masters specified.  Use the -z or -d option to specify which helios "
        + "cluster/master to connect to", parser));
    return ImmutableList.of();
  }

  private Iterable<String> parseDomains(final List<String> domainStrings) {
    final Set<String> domains = Sets.newLinkedHashSet();
    for (final String s : domainStrings) {
      addAll(domains, parseDomainsString(s));
    }
    return domains;
  }

  private Iterable<String> parseDomainsString(final String domainsString) {
    return filter(asList(domainsString.split(",")), not(equalTo("")));
  }

  private void setupCommands() {
    // Job commands
    new JobCreateCommand(parse("create"));
    new JobRemoveCommand(parse("remove"));
    new JobInspectCommand(parse("inspect"));
    new JobDeployCommand(parse("deploy"));
    new JobUndeployCommand(parse("undeploy"));
    new JobStartCommand(parse("start"));
    new JobStopCommand(parse("stop"));
    new JobHistoryCommand(parse("history"));
    new JobListCommand(parse("jobs"));
    new JobStatusCommand(parse("status"));
    new JobWatchCommand(parse("watch"));

    // Host commands
    new HostListCommand(parse("hosts"));
    new HostRegisterCommand(parse("register"));
    new HostDeregisterCommand(parse("deregister"));

    // Master commands
    new MasterListCommand(parse("masters"));

    // Deployment group commands
    new DeploymentGroupCreateCommand(parse("create-deployment-group"));
    new DeploymentGroupRemoveCommand(parse("remove-deployment-group"));
    new DeploymentGroupListCommand(parse("list-deployment-groups"));
    new DeploymentGroupInspectCommand(parse("inspect-deployment-group"));
    new DeploymentGroupStatusCommand(parse("deployment-group-status"));
    new DeploymentGroupWatchCommand(parse("watch-deployment-group"));
    new RollingUpdateCommand(parse("rolling-update"));
    new DeploymentGroupStopCommand(parse("stop-deployment-group"));

    // Version Command
    final Subparser version = parse("version").help("print version of master and client");
    new VersionCommand(version);
  }

  /**
   * Use this instead of calling parser.handle error directly. This will print a header with
   * links to jira and documentation before the standard error message is printed.
   *
   * @param parser the parser which will print the standard error message
   * @param ex     the exception that will be printed
   */
  @SuppressWarnings("UseOfSystemOutOrSystemErr")
  private void handleError(ArgumentParser parser, ArgumentParserException ex) {
    System.err.println("# " + HELP_ISSUES);
    System.err.println("# " + HELP_WIKI);
    System.err.println("# ---------------------------------------------------------------");
    parser.handleError(ex);
  }

  public List<Target> getTargets() {
    return targets;
  }

  public String getUsername() {
    return username;
  }

  public boolean getJson() {
    return json;
  }

  private static class GlobalArgs {

    private final Argument masterArg;
    private final Argument domainsArg;
    private final Argument srvNameArg;
    private final Argument usernameArg;
    private final Argument googleCredentialsArg;
    private final Argument verbose;
    private final Argument noLogSetup;
    private final Argument jsonArg;
    private final ArgumentGroup globalArgs;
    private final boolean topLevel;


    GlobalArgs(final ArgumentParser parser, final CliConfig cliConfig) {
      this(parser, cliConfig, false);
    }

    GlobalArgs(final ArgumentParser parser, final CliConfig cliConfig, final boolean topLevel) {
      this.globalArgs = parser.addArgumentGroup("global options");
      this.topLevel = topLevel;

      masterArg = addArgument("-z", "--master")
          .action(append())
          .help("master endpoints");

      domainsArg = addArgument("-d", "--domains")
          .setDefault(new ArrayList<>())
          .action(append())
          .help("domains");

      srvNameArg = addArgument("--srv-name")
          .setDefault(cliConfig.getSrvName())
          .help("master srv name");

      usernameArg = addArgument("-u", "--username")
          .setDefault(System.getProperty("user.name"))
          .help("username");

      googleCredentialsArg = addArgument("--google-credentials")
          .type(Boolean.class)
          .setDefault(true)
          .help("enable authentication using access tokens derived from Google Credentials");

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

      addArgument("--version")
          .action(Arguments.version())
          .help("print version");

      jsonArg = addArgument("--json")
          .action(storeTrue())
          .help("json output");

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

      // note: because of the way the HeliosClient is constructed, these next arguments are
      // read indirectly in cli/Utils.java:

      addArgument("-k", "--insecure")
          .action(storeTrue())
          .help("Disables hostname verification of HTTPS connections. "
                + "Similar to 'curl -k'. "
                + "Useful when using -z flag to connect directly to a master using HTTPS which "
                + "presents a certificate whose subject does not match the actual hostname.");

      // for http-timeout and retry-timeout, do not set a default value in the argument, so that
      // envrionment variables can be inspected in the Utils client factory.
      addArgument("--http-timeout")
          .type(Integer.class)
          .help("Timeout (in seconds) for each HTTP/S request to the master. "
                + "If this flag is not set, the value in the environment variable "
                + Utils.HTTP_TIMEOUT_ENV_VAR + " will be used. "
                + "If this environment variable is not set, then the default is "
                + Utils.DEFAULT_HTTP_TIMEOUT_SECS + " seconds.");

      addArgument("--retry-timeout")
          .type(Integer.class)
          .help("Total timeout (in seconds) for all of the requests that helios makes to the "
                + "master. If an individual request fails, helios will retry the request again "
                + "until successful or until this timeout elapses. "
                + "If this flag is not set, the value in the environment variable "
                + Utils.TOTAL_TIMEOUT_ENV_VAR + " will be used. "
                + "If this environment variable is not set, then the default is "
                + Utils.DEFAULT_TOTAL_TIMEOUT_SECS + " seconds.");
    }

    private Argument addArgument(final String... nameOrFlags) {
      final FeatureControl defaultControl = topLevel ? null : SUPPRESS;
      return globalArgs.addArgument(nameOrFlags).setDefault(defaultControl);
    }
  }

  private GlobalArgs addGlobalArgs(final ArgumentParser parser, final CliConfig cliConfig) {
    return new GlobalArgs(parser, cliConfig);
  }

  private GlobalArgs addGlobalArgs(final ArgumentParser parser, final CliConfig cliConfig,
                                   final boolean topLevel) {
    return new GlobalArgs(parser, cliConfig, topLevel);
  }

  public Namespace getNamespace() {
    return options;
  }

  public CliCommand getCommand() {
    return command;
  }

  public LoggingConfig getLoggingConfig() {
    return loggingConfig;
  }

  private Subparser parse(final String name) {
    return parse(commandParsers, name);
  }

  private Subparser parse(final Subparsers subparsers, final String name) {
    final Subparser subparser = subparsers.addParser(name, true);
    addGlobalArgs(subparser, cliConfig);
    return subparser;
  }
}
