package be.iminds.ilabt.jfed.ui.cli;

import be.iminds.ilabt.jfed.rspec.parser.RspecXmlConstants;
import be.iminds.ilabt.jfed.ui.commandline.BaseCli;
import be.iminds.ilabt.jfed.util.*;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.core.joran.spi.JoranException;
import ch.qos.logback.core.util.StatusPrinter;
import org.apache.commons.cli.*;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.*;
import java.util.regex.Pattern;

import static be.iminds.ilabt.jfed.ui.cli.ExperimenterCommandLineParser.Command.*;

/**
 * ExperimenterCommandLineParser
 */
public class ExperimenterCommandLineParser {
    public static final String CHOOSE_PROJECT_AUTOMATICALLY_OPTION = "CHOOSE_AUTOMATICALLY";

    public enum Command {
        CREATE(true, "create", "createsliver", "createslivers"),
        RENEW(true, "renew"),
        DELETE(true, "delete"),
        POA(true, "performOperationalAction", "poa", "perform-operational-action", "perform_operational_action"),
        STATUS(true, "status"),
        MANIFEST(true, "manifest"),
        SHOW_SLICE_INFO(true, "show-slice", Pattern.compile("(show(-)?)?(slice(-)?)?((cred(ential)?)|(info(rmation)?))")),
        CREATE_SLICE_ONLY(true, "create-slice", Pattern.compile("(create(-)?)?slice((-)?only)?")),
        LIST_USER_INFO(false, "userinfo");

        private final List<String> aliases;
        private final Pattern pattern;
        private final boolean requireSlice;

        Command(boolean requireSlice, String... aliases) {
            this.requireSlice = requireSlice;
            this.aliases = Arrays.asList(aliases);
            this.pattern = null;
        }

        Command(boolean requireSlice, String mainAlias, Pattern pattern) {
            this.requireSlice = requireSlice;
            this.aliases = Collections.singletonList(mainAlias);
            this.pattern = pattern;
        }

        public boolean matches(String cliCommand) {
            if (pattern == null) {
                for (String alias : aliases) {
                    if (alias.equalsIgnoreCase(cliCommand))
                        return true;
                }
                return false;
            } else {
                return pattern.matcher(cliCommand).matches();
            }
        }

        public List<String> getAliases() {
            return aliases;
        }

        public boolean isRequireSlice() {
            return requireSlice;
        }

        public static Command parse(String cliCommand) {
            for (Command command : Command.values()) {
                if (command.matches(cliCommand))
                    return command;
            }
            return null;
        }
    }

    private final Command command;
    private final Options options;

    public final CommandLine line;

    public final String sliceIdArg;
    public String projectNameArg;
    public final String sliceExpirationDateArg;
    public final String sliceExpirationHoursArg;
    public final String rspecArg;
    public final String bindRspecArg;
    public final String shareSliceUsersArg;
    public final String outputFormatArg;
    public final String amApiVersionArg;
    public final String sshKeysArg;
    public final String manifestArg;
    public final String callLogArg;
    public final String sliceRecoverInfoArg;
    public final boolean createSliceArg;
    public final boolean rewriteRspecArg;
    public final boolean stitchingArg;
    public final boolean debug;
    public final boolean printCalls;
    public final boolean fake;
    public final boolean logbackLogging;
    public final boolean fakeSliver;
    public final boolean waitUntilReady;
    public final boolean silent;
    public final boolean abortIfSliversExist;
    public final boolean deleteOnCreateFailure;
    public final boolean deleteOnBecomeReadyFailure;
    public final String[] speaksForArgs;
    public final String poaActionArg;
    public final String poaTargetSliverUrnArg;
    public final String ansibleDirName;
    public final String ansiblePlayBookExe;
    public final boolean ansibleExecuteRspecPlaybooks;
    public final String[] ansibleExtraPlaybooks;
    public final boolean noPlaybookToExecute;
    public final boolean ansibleAllowPlaybookInputfile;
    public final boolean ansibleAllowAnyPlaybookOutputFile;
    public final boolean ansibleDebug;

    public static final List<String> actionOptions = Arrays.asList("ReloadOS", "Restart", "ConsoleUrl");
    public static final String actionOptionsString;
    static {
        String actionOptionsStringTmp = "";
        for (String a : actionOptions) {
            if (!actionOptionsStringTmp.isEmpty())
                actionOptionsStringTmp += ", ";
            actionOptionsStringTmp += a;
        }
        actionOptionsString = actionOptionsStringTmp;
    }

    private final PrintStream out;
    private final PrintStream err;
    private final InputStream in;
    private final BaseCli baseCli;

    public final String manifestFilename;
    public final GeniUrn poaTargetSliverUrn;
    public final File ansibleDir;
    public final boolean isJson;
    public final boolean isTextOutput;
    public final boolean sshKeysUserCert;
    public final boolean sshKeysUserServerKeys;
    public final boolean sshKeysSharedUserKeys;
    public final boolean sshKeysRspec;
    public final String sliceName;
    public final Date requestedExpirationDate;

    public ExperimenterCommandLineParser(String[] args,
                      final PrintStream out, final PrintStream err, final InputStream in,
                      BaseCli baseCli) throws IOException, ClassNotFoundException, CLIParseException {
        this.out = out;
        this.err = err;
        this.in = in;
        this.baseCli = baseCli;

        Command cliCommand = null;
        LinkedList<String> argList = new LinkedList<>(Arrays.asList(args));
        if (argList.isEmpty()) {
            generalHelp(err);
            throw new CLIParseException(true, "No arguments");
        } else {
            if (Objects.equals(argList.get(0), "version") || Objects.equals(argList.get(0), "--version") || Objects.equals(argList.get(0), "-version") || Objects.equals(argList.get(0), "-v")) {
                showVersion(out);
                throw new CLIParseException(false, "Version requested");
            } else {
                String cliCommandString = argList.removeFirst();
                command = Command.parse(cliCommandString);

                if (command == null) {
                    err.println("ERROR: unknown command \"" + cliCommandString + "\"");
                    generalHelp(err);
                    throw new CLIParseException(false, "Unknown command \"" + cliCommandString + "\"");
                }
            }
        }

        CommandLineParser parser = new BasicParser();

        // create the Options
        options = new Options();
        BaseCli.addUserConfig(options, "The properties file containing context details (only the login info in the context file is used by this tool)", false);

        BaseCli.addAuthoritiesConfig(options);
        if (command.isRequireSlice())
            options.addOption(Option.builder("s").longOpt("slice")
                    .desc("The URN or name of the slice to use. (auto detected)")
                    .hasArg()
                    .argName("SLICE URN OR NAME")
                    .required()
                    .build());
        options.addOption(Option.builder().longOpt("speaks-for")
                .desc("The speaksFor certificate (in a file), enabling you make calls on behalf of another user.")
                .hasArg()
                .argName("CERTIFICATE FILENAME")
                .build());
        if (command.isRequireSlice())
            options.addOption(Option.builder("S").longOpt("project-name")
                    .desc("The name of the project (= sub authority) of the slice. This is an optional argument (however some authorities might require a project!). " +
                            "You can also use \"" + CHOOSE_PROJECT_AUTOMATICALLY_OPTION + "\" as project name, in that case, the last (determined by your SA) project you are a member of will be used.")
                    .hasArg()
                    .argName("PROJECT NAME")
                    .build());
        if (command == Command.CREATE || command == Command.RENEW || command == Command.CREATE_SLICE_ONLY) {
            options.addOption(Option.builder().longOpt("expiration-date")
                    .desc("The date and time at which this slice expires. In RFC3339 format. This is an optional argument. default: 2 hours in the future")
                    .hasArg()
                    .argName("RFC3339 DATE")
                    .build());
            options.addOption(Option.builder("e").longOpt("expiration-hours")
                    .desc("The number of hours after which this slice expires. This is an optional argument. default: 2 hours in the future")
                    .hasArg()
                    .argName("INTEGER")
                    .build());
        }
        if (command == Command.CREATE || command == Command.CREATE_SLICE_ONLY) {
            options.addOption(Option.builder().longOpt("share-slice")
                    .desc("List of users to share slice with. Either use the URN of each user, or the short username. " +
                            "You can also specify the special value \"PROJECTUSERS\", which will automatically share the slice with all users in the project. " +
                            "Multiple users can be specified by separating them with a comma.")
                    .hasArg()
                    .argName("USERNAME(S)")
                    .build());
        }
        if (command == Command.LIST_USER_INFO || command == Command.SHOW_SLICE_INFO || command == Command.CREATE_SLICE_ONLY || command == Command.STATUS)
            options.addOption(Option.builder().longOpt("output-format")
                    .desc("The output format to use (for user information). Choices: \"text\" or \"json\". Default:json")
                    .hasArg()
                    .argName("FORMAT")
                    .build());
        if (command == Command.POA) {
            options.addOption(Option.builder("a").longOpt("action")
                    .desc("The operational action to perform. (Options: " + actionOptionsString + ")")
                    .hasArg()
                    .argName("ACTION")
                    .required()
                    .build());
            options.addOption(Option.builder("t").longOpt("target-sliver")
                    .desc("The URN of the sliver to perform the action on.")
                    .hasArg()
                    .argName("SLIVER URN")
                    .required()
                    .build());
        }
        if (command == Command.CREATE)
            options.addOption(Option.builder("r").longOpt("rspec")
                    .desc("The rspec file to use for creating a sliver")
                    .hasArg()
                    .argName("RSPEC XML FILE")
                    .required()
                    .build());
        if (command == Command.STATUS || command == Command.RENEW || command == Command.POA)
            options.addOption(Option.builder("r").longOpt("rspec")
                    .desc("The rspec file to get the needed authority information from (manifest or request)  (not mandatory, but might not work without)")
                    .hasArg()
                    .argName("RSPEC XML FILE")
                    .build());
        if (command == Command.CREATE || command == Command.MANIFEST)
            options.addOption(Option.builder().longOpt("ansible-dir")
                    .desc("The dir in which to save ansible config files. Will be created if it doesn't exist. Will overwrite any existing files.")
                    .hasArg()
                    .argName("DIR")
                    .build());
        if (command == Command.CREATE || command == Command.MANIFEST) {
            options.addOption(Option.builder().longOpt("ansible-execute-rspec-playbooks")
                    .desc("Execute all ansible playbooks found within request Rspec " + RspecXmlConstants.Q_JFED_EXECUTE_ANSIBLE_PLAYBOOK.getLocalPart() +
                            " elements. (default: ignore them)")
                    .build());
            options.addOption(Option.builder().longOpt("ansible-add-playbook")
                    .desc("Add an ansible playbook to execute. The argument specifies the source, " +
                            "and optionally (after a space) the target (this option can be specified multiple times)")
                    .numberOfArgs(2)
                    .argName(RspecXmlConstants.Q_ATT_JFED_EXECUTE_ANSIBLE_PLAYBOOK_SOURCE.getLocalPart() + " [" +
                            RspecXmlConstants.Q_ATT_JFED_EXECUTE_ANSIBLE_PLAYBOOK_OUTPUT_FILE.getLocalPart() + "]")
                    .build());
            options.addOption(Option.builder().longOpt("ansible-allow-playbook-inputfile")
                    .desc("Allow local files in " + RspecXmlConstants.Q_JFED_EXECUTE_ANSIBLE_PLAYBOOK.getLocalPart() + " " +
                            RspecXmlConstants.Q_ATT_JFED_EXECUTE_ANSIBLE_PLAYBOOK_SOURCE.getLocalPart() + ". (default: only allow URLs as " +
                            RspecXmlConstants.Q_ATT_JFED_EXECUTE_ANSIBLE_PLAYBOOK_SOURCE.getLocalPart() + ")")
                    .build());
            options.addOption(Option.builder().longOpt("ansible-allow-any-playbook-outputfile")
                    .desc("Allow any local file as " + RspecXmlConstants.Q_JFED_EXECUTE_ANSIBLE_PLAYBOOK.getLocalPart() + " " +
                            RspecXmlConstants.Q_ATT_JFED_EXECUTE_ANSIBLE_PLAYBOOK_OUTPUT_FILE.getLocalPart() + ". (default: only use the basename of the specified " +
                            RspecXmlConstants.Q_ATT_JFED_EXECUTE_ANSIBLE_PLAYBOOK_OUTPUT_FILE.getLocalPart() + ", and use the --ansible-dir as directory)")
                    .build());
            options.addOption(Option.builder().longOpt("ansible-playbook-exe")
                    .desc("Executable for the ansible playbook. Default is \"ansible-playbook\" and is usually fine. " +
                            "On some systems might need to be \"/usr/bin/python2 /usr/bin/ansible-playbook\"")
                    .hasArg()
                    .build());
            options.addOption(Option.builder().longOpt("ansible-debug")
                    .desc("Set debug flags when calling ansible playbook.")
                    .build());
        }
        options.addOption(Option.builder().longOpt("manifest")
                .desc("The file in which the manifest must be stored. (default: manifest-<SLICENAME>.rspec)")
                .hasArg()
                .argName("FILE")
                .build());
        if (command == Command.DELETE)
            options.addOption(Option.builder("r").longOpt("rspec")
                    .desc("The rspec file to get the needed authority information from (manifest or request) (not mandatory, but might not work without)")
                    .hasArg()
                    .argName("RSPEC XML FILE")
                    .build());
        if (command == Command.CREATE) {
            options.addOption(Option.builder().longOpt("delete-on-create-failure")
                    .desc("If there is a failure in a call made to create the sliver(s), delete all resources everywhere before exiting.")
                    .build());
            options.addOption(Option.builder().longOpt("delete-on-become-ready-failure")
                    .desc("If there is a failure in a call while waiting for the sliver(s) to become ready, delete all resources everywhere before exiting.")
                    .build());
            options.addOption(Option.builder().longOpt("abort-if-slivers-exist")
                    .desc("Do not create any slivers if any sliver already exists on any of the authorities.")
                    .build());
            options.addOption(Option.builder().longOpt("create-slice")
                    .desc("Create the slice if it doesn't exist. (if it exists, this option is ignored)")
                    .build());
            options.addOption(Option.builder().longOpt("rewrite-rspec")
                    .desc("Parse the provided RSpec, and reconstruct it again, before sending it to the server. This can help prevent some server sides bug caused by valid but atypical RSpecs.")
                    .build());
            options.addOption(Option.builder().longOpt("bind-rspec")
                    .desc("Bind any unbound nodes in the RSpec to a specified authority. This leaves bound nodes as they are. (this imples --rewrite-rspec)")
                    .hasArg()
                    .argName("COMPONENT MANAGER URN")
                    .build());
            options.addOption(Option.builder("k").longOpt("ssh-keys")
                    .desc("Specify which ssh keys to add. The argument is a (comma separated) list of options.\n" +
                            "Available options: usercert,userkeys,rspec,shareduserallkeys\n" +
                            "   usercert: add the user making the calls, with the ssh key from the certificate in the login PEM file.\n" +
                            "   userkeys: add the user making the calls, with the ssh keys that are stored on the user authority MA server. (usercert and userkeys can be combined to add all keys)\n" +
                            "   shareduserallkeys: add the ssh keys that are stored on the user authority MA server for the users the slice is shared with AND the ssh keys from that user's login certificate.\n" +
                            "   rspec: add the users and keys specified in the RSpec itself.\n" +
                            "This option is optional, default: usercert,rspec,shareduserallkeys")
                    .hasArg()
                    .argName("OPTION LIST")
                    .build());
            options.addOption(Option.builder().longOpt("stitching")
                    .desc("If the RSpec requires stitching, allow it. By default, RSpecs that require stitching cause an abort")
                    .build());
            options.addOption(Option.builder().longOpt("slice-recover-info")
                    .desc("A file into which slice recover info will be stored. This can be used by the jFed GUI (if copied to the correct directory).")
                    .hasArg()
                    .argName("FILE")
                    .build());
            options.addOption(Option.builder().longOpt("nowait")
                    .desc("Do not wait until sliver is ready (default: wait until ready)")
                    .build());
        }
        options.addOption(Option.builder().longOpt("call-log")
                .desc("A file into which all call details will be stored")
                .hasArg()
                .argName("FILE")
                .build());
        options.addOption(Option.builder().longOpt("print-calls")
                .desc("Print all calls to stdout")
                .build());
        options.addOption(Option.builder("am").longOpt("am-api")
                .desc("The AM Api version to use (\"2\" and \"3\" supported, default: choose automatically)")
                .hasArg()
                .argName("AM API VERSION")
                .build());
        options.addOption(Option.builder().longOpt("fake")
                .desc("Do everything, expect actually making the slice and sliver calls. Useful for debugging syntax. (will make calls relating to retrieving user data)")
                .build());
        if (command != Command.CREATE_SLICE_ONLY && command != Command.SHOW_SLICE_INFO)
            options.addOption(Option.builder().longOpt("fake-sliver")
                    .desc("Do everything, expect actually making the sliver calls. (will make calls relating to retrieving user data and creating or retrieving slice)")
                    .build());
        options.addOption("q", "quiet", false, "less output");
        options.addOption(null, "silent", false, "less output (same as --quiet)");
        options.addOption("d", "debug", false, "extra debugging output");
        options.addOption("l", "logging", false, "activate logback logging output");
        options.addOption("v", "version", false, "show version and exit");

        try {
            // parse the command line arguments
            String[] leftoverArgs = new String[argList.size()];
            int i = 0;
            for (String arg : argList)
                leftoverArgs[i++] = arg;
            line = parser.parse(options, leftoverArgs);
        } catch (ParseException exp) {
            err.println("Command line argument Syntax error: " + exp.getMessage());
            commandHelp(err, options, command);
            throw new CLIParseException(true, "Command line argument Syntax error: " + exp.getMessage());
        }

        if (line.hasOption("version")) {
            showVersion(out);
            throw new CLIParseException(false, "Show version");
        }

        sliceIdArg = line.getOptionValue("slice");
        projectNameArg = line.getOptionValue("project-name");
        sliceExpirationDateArg = line.getOptionValue("expiration-date");
        sliceExpirationHoursArg = line.getOptionValue("expiration-hours");
        rspecArg = line.getOptionValue("rspec");
        bindRspecArg = line.getOptionValue("bind-rspec");
        shareSliceUsersArg = line.getOptionValue("share-slice");
        outputFormatArg = line.getOptionValue("output-format") == null ? "json" : line.getOptionValue("output-format");
        amApiVersionArg = line.getOptionValue("am-api");
        sshKeysArg = line.getOptionValue("ssh-keys");
        manifestArg = line.getOptionValue("manifest");
        callLogArg = line.getOptionValue("call-log");
        sliceRecoverInfoArg = line.getOptionValue("slice-recover-info");
        createSliceArg = line.hasOption("create-slice");
        rewriteRspecArg = line.hasOption("rewrite-rspec") || bindRspecArg != null;
        stitchingArg = line.hasOption("stitching");
        debug = line.hasOption("debug");
        printCalls = line.hasOption("print-calls");
        fake = line.hasOption("fake");
        logbackLogging = line.hasOption("logging");
        fakeSliver = line.hasOption("fake-sliver");
        waitUntilReady = !line.hasOption("nowait");
        silent = !debug && (line.hasOption("quiet") || line.hasOption("silent"));
        abortIfSliversExist = line.hasOption("abort-if-slivers-exist");
        deleteOnCreateFailure = line.hasOption("--delete-on-create-failure");
        deleteOnBecomeReadyFailure = line.hasOption("--delete-on-become-ready-failure");
        speaksForArgs = line.getOptionValues("speaks-for");
        poaActionArg = line.getOptionValue("action");
        poaTargetSliverUrnArg = line.getOptionValue("target-sliver");
        ansibleDirName = line.getOptionValue("ansible-dir");

        ansiblePlayBookExe = line.getOptionValue("ansible-playbook-exe", "ansible-playbook");
        ansibleExecuteRspecPlaybooks = line.hasOption("ansible-execute-rspec-playbooks");
        ansibleExtraPlaybooks = line.getOptionValues("ansible-add-playbook");
        noPlaybookToExecute = !ansibleExecuteRspecPlaybooks && (ansibleExtraPlaybooks == null || ansibleExtraPlaybooks.length == 0);
        ansibleAllowPlaybookInputfile = line.hasOption("ansible-allow-playbook-inputfile");
        ansibleAllowAnyPlaybookOutputFile = line.hasOption("ansible-allow-any-playbook-outputfile");
        ansibleDebug = line.hasOption("ansible-debug");

        if (debug)
            for (Option o : line.getOptions())
                out.println("ARG: " + o.getOpt() + " " + o.getLongOpt() + " " + o.getValue() + " " + o.getValuesList() + " " + o.getValuesList().size());





        if (isCommand(POA) && poaActionArg != null) {
            boolean validAction = false;
            for (String o : actionOptions) {
                if (o.equalsIgnoreCase(poaActionArg.trim()))
                    validAction = true;
            }
            if (!validAction) {
                err.println("Unsupported action option \""+poaActionArg+"\" specified. " +
                        "Valid options are: "+actionOptionsString);
                commandHelp();
                throw new CLIParseException(true, "Argument error.");
            }
        }
        if (isCommand(POA) && poaTargetSliverUrnArg != null) {
            poaTargetSliverUrn = GeniUrn.parse(poaTargetSliverUrnArg);
            if (poaTargetSliverUrn == null) {
                err.println("Invalid URN specified for option target sliver: \""+poaTargetSliverUrnArg+"\"");
                commandHelp();
                throw new CLIParseException(true, "Invalid URN specified for option target sliver: \""+poaTargetSliverUrnArg+"\"");
            }
            if (!Objects.equals(poaTargetSliverUrn.getResourceType(), "sliver")) {
                err.println("\n\n**WARNING** URN specified for option target sliver is not a sliver URN: \""+poaTargetSliverUrnArg+"\"\n" +
                        "            This will be ignored, but will likely not cause the desired results.\n\n");
            }
        } else {
            poaTargetSliverUrn = null;
        }

        if (ansibleDirName != null) {
            ansibleDir = new File(ansibleDirName);
            if (!ansibleDir.exists() && !ansibleDir.getParentFile().exists()) {
                err.println("Specified ansible-dir does not exist, and neither does parent dir: \""+ansibleDirName+"\"");
                commandHelp();
                throw new CLIParseException(true, "Argument error.");
            }
        } else
            ansibleDir = null;

        if ((!noPlaybookToExecute) && ansibleDirName == null) {
            err.println("the --ansible-execute-rspec-playbooks and --ansible-add-playbook options requires that the "+
                    "ansible-dir option is specified.");
            throw new CLIParseException(true, "Argument error.");
        }

        if (debug) {
            out.println("Showing version because debug is enabled:");
            showVersion();
        }

        if (logbackLogging) {
            LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
            loggerContext.reset();
            JoranConfigurator configurator = new JoranConfigurator();
            InputStream configStream = ExperimenterCommandLineParser.class.getResourceAsStream("/logback_debug.xml");
            configurator.setContext(loggerContext);
            try {
                configurator.doConfigure(configStream); // loads logback file
            } catch (JoranException e) {
                err.println("Failed to configure logback! "+e.getMessage());
                e.printStackTrace();
            }
            StatusPrinter.printInCaseOfErrorsOrWarnings(loggerContext);

            if (configStream != null)
                configStream.close();
        }

        if (getCommand().isRequireSlice()) {
            //should not be required to check this here, as it is a required option, but let's do it anyway to prevent any issues.
            if (sliceIdArg == null) {
                err.println("The slice option is mandatory");
                commandHelp();
                throw new CLIParseException(true, "Argument error.");
            }
        }

        if (isCommand(LIST_USER_INFO,
                SHOW_SLICE_INFO,
                CREATE_SLICE_ONLY,
                STATUS)) {
            boolean knownFormat = false;
            if (outputFormatArg.trim().equalsIgnoreCase("json")) {
                isJson = true;
                knownFormat = true;
            } else {
                isJson = false;
            }
            if (outputFormatArg.trim().equalsIgnoreCase("text")) {
                isTextOutput = true;
                knownFormat = true;
            } else {
                isTextOutput = false;
            }
            if (!knownFormat) {
                err.println("Output format \"" + outputFormatArg + "\" is unknown");
                commandHelp();
                throw new CLIParseException(true, "Argument error.");
            }
        } else {
            isJson = false;
            isTextOutput = false;
        }

        ExtraInfoCallback.setAnyLoginCallback(new ExtraInfoCallback.AnyLoginCallback() {
            private JFedPasswordManager.LoginInfo cachedLogin = null;

            @Override
            public JFedPasswordManager.LoginInfo getLogin(String serviceName, String friendlyName) {
                err.println("Login callback called.");
                if (cachedLogin == null) {
                    out.println("\n");
                    out.println("==============================================================");
                    String username = IOUtils.askCommandLineInput("Please enter "+friendlyName+" username:", in);
                    String password = new String(IOUtils.askCommandLinePassword("Please enter "+friendlyName+" password:", in));
                    cachedLogin = new JFedPasswordManager.LoginInfo(serviceName, username, password);
                    out.println("==============================================================\n");
                }
                return cachedLogin;
            }
        });


        if (sshKeysArg != null) {
            String[] opts = sshKeysArg.split(",");

            boolean tmpSshKeysUserCert = false;
            boolean tmpSshKeysUserServerKeys = false;
            boolean tmpSshKeysSharedUserKeys = false;
            boolean tmpSshKeysRspec = false;

            for (String opt : opts) {
                boolean validOpt = false;

                if (opt.equalsIgnoreCase("usercert")) {
                    tmpSshKeysUserCert = true;
                    validOpt = true;
                }
                if (opt.equalsIgnoreCase("userkeys")) {
                    tmpSshKeysUserServerKeys = true;
                    validOpt = true;
                }
                if (opt.equalsIgnoreCase("shareduserallkeys")) {
                    tmpSshKeysSharedUserKeys = true;
                    validOpt = true;
                }
                if (opt.equalsIgnoreCase("rspec")) {
                    tmpSshKeysRspec = true;
                    validOpt = true;
                }

                if (!validOpt) {
                    err.println("The ssh-keys option you have specified (" + opt + ") is not valid");
                    commandHelp();
                    throw new CLIParseException(true, "Argument error.");
                }
            }

            sshKeysUserCert = tmpSshKeysUserCert;
            sshKeysUserServerKeys = tmpSshKeysUserServerKeys;
            sshKeysSharedUserKeys = tmpSshKeysSharedUserKeys;
            sshKeysRspec = tmpSshKeysRspec;
        } else {
            //default: usercert,rspec
            sshKeysUserCert = true;
            sshKeysUserServerKeys = false;
            sshKeysSharedUserKeys = true;
            sshKeysRspec = true;
        }

        if (getCommand().isRequireSlice() && sliceIdArg.startsWith("urn:")) {
            GeniUrn sliceUrn = GeniUrn.parse(sliceIdArg);
            if (sliceUrn == null) {
                err.println("Error parsing provided slice URN: " + sliceIdArg);
                throw new CLIParseException(true, "Argument error.");
            }
            String sliceUrnprojectName = sliceUrn.getEncodedSubAuthName();
            if (((projectNameArg != null) != (sliceUrnprojectName != null)) ||
                    (projectNameArg != null && sliceUrnprojectName != null &&
                            !Objects.equals(projectNameArg, sliceUrnprojectName))) {
                if (projectNameArg != null) {
                    if (Objects.equals(projectNameArg, CHOOSE_PROJECT_AUTOMATICALLY_OPTION)) {
                        projectNameArg = sliceUrnprojectName;
                    } else {
                        err.println("The project-name you have specified (" + projectNameArg + ") does not match the sub authority in the slice URN you have specified (" + sliceIdArg + ")");
                        throw new CLIParseException(true, "Argument error.");
                    }
                } else
                    projectNameArg = sliceUrnprojectName;
            }
        }
        sliceName = !getCommand().isRequireSlice() ? null :
                (sliceIdArg.startsWith("urn:") ? GeniUrn.parse(sliceIdArg).getEncodedResourceName() : sliceIdArg);

        if (manifestArg == null)
            manifestFilename = "manifest-" + sliceName + ".rspec";
        else
            manifestFilename = manifestArg;

        if (sliceExpirationDateArg != null && sliceExpirationHoursArg != null) {
            err.println("You may not specify both expiration-date and expiration-hours. You need to specify only one.");
            throw new CLIParseException(true, "Argument error.");
        }

        if (sliceExpirationDateArg == null) {
            if (sliceExpirationHoursArg == null)
                requestedExpirationDate = new Date(System.currentTimeMillis() + (2 * 60 * 60 * 1000L)); //2 hours
            else {
                try {
                    int hours = Integer.parseInt(sliceExpirationHoursArg);
                    requestedExpirationDate = new Date(System.currentTimeMillis() + (hours * 60 * 60 * 1000L));
                } catch (NumberFormatException e) {
                    err.println("Specified slice expiration hours (\"" + sliceExpirationDateArg + "\") is not a valid number.");
                    throw new CLIParseException(true, "Argument error.");
                }
            }
        } else {
            try {
                requestedExpirationDate = RFC3339Util.rfc3339StringToDate(sliceExpirationDateArg);
            } catch (java.text.ParseException e) {
                err.println("Specified slice expiration date (\"" + sliceExpirationDateArg + "\") is not in RFC3339 format. The current date in RFC3339 format is: " +
                        RFC3339Util.dateToRFC3339String(new Date()));
                throw new CLIParseException(true, "Argument error.");
            }
        }

        if (amApiVersionArg != null && !Objects.equals(amApiVersionArg, "2") && !Objects.equals(amApiVersionArg, "3")) {
            err.println("Unsupported AM api version: \"" + amApiVersionArg + "\"");
            commandHelp();
            throw new CLIParseException(true, "Argument error.");
        }
    }

    public Command getCommand() {
        return command;
    }
    /** returns true if the user provided command is any of the given commands */
    public boolean isCommand(Command ... anyCommmands) {
        for (Command c : anyCommmands) {
            if (Objects.equals(command, c))
                return true;
        }
        return false;
    }

    private static void commandHelp(final PrintStream err, Options options, Command command) {
        HelpFormatter formatter = new HelpFormatter();
        formatter.printHelp(200, "jfed-experimenter-cli <command> [command_options ... ]",
                "Available commands: create,delete,status,renew,manifest,userinfo,sliceinfo,createslice\n" +
                command.getAliases().get(0) + " options:", options,
                "\n\nTemplate Context Properties File:\n  username = <username>\n" +
                        "  passwordFilename = <filename of file containing password>\n" +
                        "  pemKeyAndCertFilename = <filename of file containing user certificate and private key in PEM format>\n" +
                        "  userAuthorityUrn = <URN of test user's authority>\n"
        );
    }
    public void commandHelp() {
        HelpFormatter formatter = new HelpFormatter();
        formatter.printHelp(200, "jfed-experimenter-cli <command> [command_options ... ]",
                "Available commands: create,delete,status,renew,manifest,userinfo,sliceinfo,createslice\n" +
                command.getAliases().get(0) + " options:", options,
                "\n\nTemplate Context Properties File:\n  username = <username>\n" +
                        "  passwordFilename = <filename of file containing password>\n" +
                        "  pemKeyAndCertFilename = <filename of file containing user certificate and private key in PEM format>\n" +
                        "  userAuthorityUrn = <URN of test user's authority>\n"
        );
    }

    public void showVersion() {
        showVersion(out);
    }
    public static void showVersion(PrintStream out) {
        out.println("jFed Experimenter CLI -- http://jfed.iminds.be/");
        out.println("Send bugreports to: jfed-bugreports@atlantis.ugent.be");

        final String VERSION_PROPERTIES_FILE = "/version.properties";
        final String PROP_VERSION = "version";
        final String PROP_GIT_COMMIT = "build.git_commit";
        final String PROP_GIT_BRANCH = "build.git_branch";
        final String PROP_BUILD_JENKINS = "build.jenkins_build";

        Properties prop = new Properties();
        //now load the version-properties file
        try {
            prop.load(ExperimenterCli.class.getResourceAsStream(VERSION_PROPERTIES_FILE));
        } catch (IOException e) {
            throw new RuntimeException("Something went wrong while retrieving the version info: " + e.getMessage(), e);
        }

        StringBuilder version = new StringBuilder();
        version.append(prop.getProperty(PROP_VERSION, "Unknown"));
        final String jenkinsBuildNr = prop.getProperty(PROP_BUILD_JENKINS);
        if (jenkinsBuildNr != null && !jenkinsBuildNr.isEmpty())
            version.append("  - build #").append(jenkinsBuildNr);

        final String gitCommit = prop.getProperty(PROP_GIT_COMMIT);
        if (gitCommit != null && !gitCommit.isEmpty())
            version.append("  - git commit #").append(gitCommit);

        final String gitBranch = prop.getProperty(PROP_GIT_BRANCH);
        if (gitBranch != null && !gitBranch.isEmpty())
            version.append(" on ").append(gitBranch);
        out.println("Version: " + version.toString());

        out.println("Environment: "
                + System.getProperty("os.name") + " " + System.getProperty("os.version") + " " + System.getProperty("os.arch")
                + " - Java " + System.getProperty("java.version") + " (" + System.getProperty("java.vendor") + ")");
    }

    public static void generalHelp(final PrintStream err) {
        err.println("Syntax: jfed-experimenter-cli <command> [command_options ... ]\n" +
                "Available Commands: create,delete,status,renew,userinfo,sliceinfo,createslice\n");
    }

    public static class CLIParseException extends Exception {
        private final boolean error;

        public CLIParseException(boolean error) { this.error = error; }
        public CLIParseException(boolean error, String message) { super(message); this.error = error; }
        public CLIParseException(boolean error, String message, Throwable cause) { super(message, cause); this.error = error; }
        public CLIParseException(boolean error, Throwable cause) { super(cause); this.error = error; }
        public CLIParseException(boolean error, String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
            super(message, cause, enableSuppression, writableStackTrace);
            this.error = error;
        }

        public boolean isError() {
            return error;
        }
    }
}