// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// 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 google.registry.tools;

import static com.google.common.base.Preconditions.checkState;
import static google.registry.tools.Injector.injectReflectively;

import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.ParameterException;
import com.beust.jcommander.Parameters;
import com.beust.jcommander.ParametersDelegate;
import com.google.appengine.tools.remoteapi.RemoteApiInstaller;
import com.google.appengine.tools.remoteapi.RemoteApiOptions;
import com.google.common.collect.ImmutableMap;
import google.registry.model.ofy.ObjectifyService;
import google.registry.tools.Command.RemoteApiCommand;
import google.registry.tools.params.ParameterFactory;
import java.security.Security;
import java.util.HashMap;
import java.util.Map;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

/** Container class to create and run remote commands against a Datastore instance. */
@Parameters(separators = " =", commandDescription = "Command-line interface to the registry")
final class RegistryCli {

  @Parameter(
      names = {"-e", "--environment"},
      description = "Sets the default environment to run the command.")
  private RegistryToolEnvironment environment = RegistryToolEnvironment.PRODUCTION;

  @Parameter(
      names = {"-c", "--commands"},
      description = "Returns all command names.")
  private boolean showAllCommands;

  // Do not make this final - compile-time constant inlining may interfere with JCommander.
  @ParametersDelegate
  private AppEngineConnectionFlags appEngineConnectionFlags =
      new AppEngineConnectionFlags();


  // Do not make this final - compile-time constant inlining may interfere with JCommander.
  @ParametersDelegate
  private LoggingParameters loggingParams = new LoggingParameters();

  // The <? extends Class<? extends Command>> wildcard looks a little funny, but is needed so that
  // we can accept maps with value types that are subtypes of Class<? extends Command> rather than
  // literally that type.  For more explanation, see:
  //   http://www.angelikalanger.com/GenericsFAQ/FAQSections/TypeArguments.html#FAQ104
  void run(
      String programName,
      String[] args,
      ImmutableMap<String, ? extends Class<? extends Command>> commands) throws Exception {
    Security.addProvider(new BouncyCastleProvider());
    JCommander jcommander = new JCommander(this);
    jcommander.addConverterFactory(new ParameterFactory());
    jcommander.setProgramName(programName);

    // Store the instances of each Command class here so we can retrieve the same one for the
    // called command later on.  JCommander could have done this for us, but it doesn't.
    Map<String, Command> commandInstances = new HashMap<>();

    HelpCommand helpCommand = new HelpCommand(jcommander);
    jcommander.addCommand("help", helpCommand);
    commandInstances.put("help", helpCommand);

    for (Map.Entry<String, ? extends Class<? extends Command>> entry : commands.entrySet()) {
      Command command = entry.getValue().newInstance();
      jcommander.addCommand(entry.getKey(), command);
      commandInstances.put(entry.getKey(), command);
    }

    try {
      jcommander.parse(args);
    } catch (ParameterException e) {
      // If we failed to fully parse the command but at least found a valid command name, show only
      // the usage for that command. Otherwise, show full usage. Either way, rethrow the error.
      if (jcommander.getParsedCommand() == null) {
        jcommander.usage();
      } else {
        jcommander.usage(jcommander.getParsedCommand());
      }
      // Don't rethrow if we said: nomulus command --help
      if ("Unknown option: --help".equals(e.getMessage())) {
        return;
      }
      throw e;
    }

    if (showAllCommands) {
      for (Map.Entry<String, ? extends Class<? extends Command>> entry : commands.entrySet()) {
        System.out.println(entry.getKey());
      }
      return;
    }

    checkState(RegistryToolEnvironment.get() == environment,
        "RegistryToolEnvironment argument pre-processing kludge failed.");

    Command command = commandInstances.get(jcommander.getParsedCommand());
    if (command == null) {
      jcommander.usage();
      return;
    }
    loggingParams.configureLogging();  // Must be called after parameters are parsed.

    // Create the main component and use it to inject the command class.
    RegistryToolComponent component = DaggerRegistryToolComponent.builder()
        .flagsModule(new AppEngineConnectionFlags.FlagsModule(appEngineConnectionFlags))
        .build();
    injectReflectively(RegistryToolComponent.class, component, command);

    if (!(command instanceof RemoteApiCommand)) {
      // TODO(mmuller): this should be in the try/catch LoginRequiredException in case future
      // commands use our credential.
      command.run();
      return;
    }

    // Get the App Engine connection, advise the user if they are not currently logged in..
    AppEngineConnection connection;
    try {
      connection = component.appEngineConnection();
    } catch (AuthModule.LoginRequiredException ex) {
      System.err.println("===================================================================");
      System.err.println("You must login using 'nomulus login' prior to running this command.");
      System.err.println("===================================================================");
      return;
    }

    if (command instanceof ServerSideCommand) {
      ((ServerSideCommand) command).setConnection(connection);
    }

    // RemoteApiCommands need to have the remote api installed to work.
    RemoteApiInstaller installer = new RemoteApiInstaller();
    RemoteApiOptions options = new RemoteApiOptions();
    options.server(connection.getServer().getHost(), connection.getServer().getPort());
    if (connection.isLocalhost()) {
      // Use dev credentials for localhost.
      options.useDevelopmentServerCredential();
    } else {
      options.useApplicationDefaultCredential();
    }
    installer.install(options);

    // Ensure that all entity classes are loaded before command code runs.
    ObjectifyService.initOfy();

    try {
      command.run();
    } finally {
      installer.uninstall();
    }
  }
}

@@ -4942,16 +4942,484 @@
 arsed.%0A%0A
+    try %7B%0A      runCommand(command);%0A    %7D catch (AuthModule.LoginRequiredException ex) %7B%0A      System.err.println(%22===================================================================%22);%0A      System.err.println(%22You must login using 'nomulus login' prior to running this command.%22);%0A      System.err.println(%22===================================================================%22);%0A      return;%0A    %7D%0A  %7D%0A%0A  private void runCommand(Command command) throws Exception %7B%0A
     // C
@@ -6084,36 +6084,8 @@
 ion 
-connection;%0A    try %7B%0A      
 conn
@@ -6129,372 +6129,8 @@
 n();
-%0A    %7D catch (AuthModule.LoginRequiredException ex) %7B%0A      System.err.println(%22===================================================================%22);%0A      System.err.println(%22You must login using 'nomulus login' prior to running this command.%22);%0A      System.err.println(%22===================================================================%22);%0A      return;%0A    %7D
 %0A%0A  

Refactor command running around auth exceptions