package be.iminds.ilabt.jfed.lowlevel.api;

import be.iminds.ilabt.jfed.log.Logger;
import be.iminds.ilabt.jfed.lowlevel.*;
import be.iminds.ilabt.jfed.lowlevel.api.user_spec.LoginSpec;
import be.iminds.ilabt.jfed.lowlevel.api.user_spec.UserSpec;
import be.iminds.ilabt.jfed.lowlevel.connection.SfaConnection;
import be.iminds.ilabt.jfed.preferences.JFedPreferences;
import be.iminds.ilabt.jfed.util.RFC3339Util;
import be.iminds.ilabt.jfed.util.TextUtil;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;

/**
 * <p>This is a simple wrapper for the methods specified in the {@link <a href="http://groups.geni.net/geni/wiki/GAPI_AM_API_V2">Geni Aggregate Manager API v2</a>}.
 * <p>All functionality provided by the API is available in this implementation, and little processing of results is done.
 * This class stores no internal state, except for the logger used. It sends all results and debug info to this {@link Logger} if it is non null.
 * <p/>
 * <p>All methods require a {@link be.iminds.ilabt.jfed.lowlevel.connection.SfaConnection} argument and return a {@link AggregateManager2.AggregateManagerReply} which wraps the actual return value of the call, if any.
 *
 * @author Wim Van de Meerssche
 * @see <a href="http://groups.geni.net/geni/wiki/GAPI_AM_API_V2">Geni Aggregate Manager API v2</a>
 */
public class AggregateManager2 extends AbstractGeniAggregateManager {
    private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(AggregateManager2.class);

    /**
     * Construct a new AggregateManager2, with a certain logger.
     * <p/>
     * <p>This class stores no internal state, except for the logger used. It sends all results and debug info to this {@link Logger} if it is non null.
     *
     * @param logger        the logger to use. May be null if no logger is used.
     * @param retrySettings retrySettings
     */
    public AggregateManager2(@Nonnull Logger logger, RetrySettings retrySettings, @Nonnull JFedPreferences jFedPreferences) {
        super(logger, retrySettings, new ApiInfo.Api(ApiInfo.ApiName.GENI_AM, 2), jFedPreferences);
    }

    /**
     * Construct a new AggregateManager2, with a certain logger.
     * <p/>
     * <p>This class stores no internal state, except for the logger used. It sends all results and debug info to this {@link Logger} if it is non null.
     * <p/>
     * <p>This constructor sets autoRetryBusy to true, so "busy" replies are retried.
     *
     * @param logger the logger to use. May be null if no logger is used.
     */
    public AggregateManager2(@Nonnull Logger logger, @Nonnull JFedPreferences jFedPreferences) {
        super(logger, jFedPreferences.getRetrySettings(), new ApiInfo.Api(ApiInfo.ApiName.GENI_AM, 2), jFedPreferences);
    }

    /**
     * A human readable name for the implemented API
     *
     * @return "Geni Aggregate Manager API v2";
     */
    static public String getApiName() {
        return "Geni Aggregate Manager API v2";
    }

    public static List<String> createCredentialsList(@Nonnull List<AnyCredential> credentialList) {
        if (credentialList.contains(null))
            throw new IllegalArgumentException("Illegal arguments: no AnyCredential is allowed to be null in credentialList: " + credentialList);
        final List<String> credentialsList =
                credentialList.stream().map(AnyCredential::getCredentialXml).collect(Collectors.toList());

        assert !credentialsList.contains(null); //useful assert when mocking credentials
        return credentialsList;
    }

    /**
     * A human readable name for the implemented API
     *
     * @return "Geni Aggregate Manager API v2";
     */
    @Override
    public String getName() {
        return getApiName();
    }

    /**
     * Get static version and configuration information about this aggregate. Return includes:
     * <p/>
     * The version of the GENI Aggregate Manager API supported by this aggregate manager instance
     * URLs for other versions of this API supported by this aggregate
     * The RSpec formats accepted at this aggregate
     * Other information about the configuration of this aggregate.
     *
     * @param con the {@code GeniConnection} to use for this call. Must be a connection to an aggregate implementing
     *            "GENI Aggregate Manager API Version 2".
     * @return the version info of the aggregate manager, wrapped in a {@code AggregateManager2.AggregateManagerReply}
     */
    @ApiMethod(order = 1, hint = "Get static version and configuration information about this aggregate. Return includes:\n\n" +
            "- The version of the GENI Aggregate Manager API supported by this aggregate manager instance\n" +
            "- URLs for other versions of this API supported by this aggregate\n" +
            "- The RSpec formats accepted at this aggregate\n" +
            "- Other information about the configuration of this aggregate.")
    @Override
    @Nonnull
    public AggregateManagerReply<VersionInfo> getVersion(@Nonnull SfaConnection con) throws JFedException {
        return executeAndLogXmlRpcCommandGeni(null, con, "getVersion", "GetVersion",
                new ArrayList<>(), VersionInfo::new);
    }

    /**
     * listResources without list_leases argument.
     */
    @Nonnull
    public AggregateManagerReply<String> listResources(@Nonnull SfaConnection con,
                                                       @Nonnull List<AnyCredential> credentialList,
                                                       @Nonnull String rspecType,
                                                       @Nonnull String rspecVersion,
                                                       @Nullable Boolean available,
                                                       @Nullable Boolean compressed,
                                                       @Nullable String sliceUrn,
                                                       @Nullable Map<String, Object> extraOptions) throws JFedException {
        return listResources(con, credentialList, rspecType, rspecVersion, available, compressed, sliceUrn, null/*list_leases*/, extraOptions);
    }

    /**
     * Return a listing and description of available resources at this aggregate, or resources allocated to a named
     * slice at this aggregate. The resource listing and description provides sufficient information for clients to
     * select among available resources, or to use reserved resources. These listings are known as RSpecs.
     *
     * @param con            the {@code GeniConnection} to use for this call. Must be a connection to an aggregate implementing
     *                       "GENI Aggregate Manager API Version 2".
     * @param credentialList mandatory list of credentials. For an advertisement rspec request, this must includea valid
     *                       user credential. For a slice manifest rspec request, this must include a valid credential
     *                       for the slice.
     * @param rspecType      mandatory rspec type (example: "geni")
     * @param rspecVersion   mandatory rspec version (example for type "geni": "3")
     * @param available      optional (not specified = null): if true, show only available local resources, otherwise show
     *                       everything
     * @param compressed     optional (not specified = null): if true compress result (RFC 1950).      NOTE: if compressed
     *                       is set, this will decompress the result.
     * @param sliceUrn       optional (not specified = null): if set, show manifest rspec for the slice
     * @return requested rspec, wrapped in a {@code AggregateManager2.AggregateManagerReply}
     * @throws be.iminds.ilabt.jfed.lowlevel.JFedException if something went wrong while performing the call: either a server-error or an unexpected server response
     */
    @ApiMethod(order = 2, hint = "Return a listing and description of available resources at this aggregate, or resources allocated to a named " +
            "slice at this aggregate. The resource listing and description provides sufficient information for clients to " +
            "select among available resources, or to use reserved resources. These listings are known as RSpecs.")
    @Nonnull
    public AggregateManagerReply<String> listResources(@Nonnull SfaConnection con,
                                                       @ApiMethodParameter(name = "credentialList", hint = "mandatory list of credentials. For an advertisement rspec request, this must includea valid user credential. For a slice manifest rspec request, this must include a valid credential for the slice.")
                                                       @Nonnull List<AnyCredential> credentialList,
                                                       @ApiMethodParameter(name = "rspecType", guiDefault = "geni", hint = "mandatory rspec type (example: \"geni\")")
                                                       @Nonnull String rspecType,
                                                       @ApiMethodParameter(name = "rspecVersion", guiDefault = "3", hint = "mandatory rspec version (example for type \"geni\": \"3\")")
                                                       @Nonnull String rspecVersion,
                                                       @ApiMethodParameter(name = "available", required = false, hint = "optional: if true, show only available local resources, otherwise show everything")
                                                       @Nullable Boolean available,
                                                       @ApiMethodParameter(name = "compressed", required = false, guiDefault = "true", guiDefaultOptional = true, hint = "optional: if true compress result (RFC 1950).\nNOTE: if compressed is set, this API will decompress the result automatically.")
                                                       @Nullable Boolean compressed,
                                                       @ApiMethodParameter(name = "sliceUrn", required = false, hint = "optional: if set, show manifest RSpec for the slice instead of the advertisement RSpec for the AM.")
                                                       @Nullable String sliceUrn,
                                                       @ApiMethodParameter(name = "list_leases", required = false, guiDefault = "leases",
                                                               hint = "optional and PLE specific: possible values: \"leases\" or \"resources\".")
                                                       @Nullable String list_leases,
                                                       @ApiMethodParameter(name = "extraOptions", hint = "extra options", required = false, parameterType = ApiMethodParameterType.GENI_EXTRA_OPTIONS)
                                                       @Nullable Map<String, Object> extraOptions) throws JFedException {
        Map<String, Object> methodParams =
                makeMethodParameters("credentialList", credentialList,
                        "rspecType", rspecType,
                        "rspecVersion", rspecVersion);
        if (available != null) methodParams.put("available", available);
        if (compressed != null) methodParams.put("compressed", compressed);
        if (sliceUrn != null) methodParams.put("sliceUrn", sliceUrn);
        if (list_leases != null) methodParams.put("list_leases", list_leases);

        Objects.requireNonNull(credentialList, "Illegal argument: " + "credentialList" + " is not allowed to be null");
        Objects.requireNonNull(rspecType, "Illegal argument: " + "rspecType" + " is not allowed to be null");
        Objects.requireNonNull(rspecVersion, "Illegal argument: " + "rspecVersion" + " is not allowed to be null");

        Map<String, Object> rspecVersionOpt = new HashMap<>();
        rspecVersionOpt.put("type", rspecType);
        rspecVersionOpt.put("version", rspecVersion);

        Map<String, Object> options = new HashMap<>();
        options.put("geni_rspec_version", rspecVersionOpt);
        if (sliceUrn != null)
            options.put("geni_slice_urn", sliceUrn);
        if (available != null)
            options.put("geni_available", available);
        if (compressed != null)
            options.put("geni_compressed", compressed);
        if (list_leases != null)
            options.put("list_leases", list_leases);
        if (extraOptions != null) options.putAll(extraOptions);

        List<String> credentialsList = createCredentialsList(credentialList);

        List<Object> args = new ArrayList<>(2);
        args.add(credentialsList);
        args.add(options);

        return executeAndLogXmlRpcCommandGeni(methodParams, con, "listResources", "ListResources",
                args, new CompressedStringConverter(compressed));
    }

    /**
     * Allocate resources as described in a request RSpec argument to a slice with the named URN. This operation is
     * expected to start the allocated resources asynchronously after the operation has successfully completed.
     * Callers can check on the status of the resources using SliverStatus. Resources will be reserved until a
     * particular time, set by the aggregate according to policy. That expiration time will be no later than the
     * expiration time of the provided slice credential. This method returns a listing and description of the resources
     * reserved for the slice by this operation, in the form of a manifest RSpec.
     *
     * @param con            the {@code GeniConnection} to use for this call. Must be a connection to an aggregate implementing
     *                       "GENI Aggregate Manager API Version 2".
     * @param credentialList mandatory list of credentials. This must include a valid credential for the slice.
     * @param sliceUrn       urn for the slice on which this sliver needs to be created
     * @param rspec          An RSpec matching the GENI standard request RSpec schema containing the resources that the caller is
     *                       requesting for allocation to the slice specified in slice_urn.
     * @param users          An array of user structs, which contain information (typically SSH public keys) about needed to allow users to login on allocated nodes.
     * @param extraOptions   optional (not specified = null): Map with extra options.
     * @return The manifest Rspec for the created sliver, wrapped in a {@code AggregateManager2.AggregateManagerReply}
     * @throws be.iminds.ilabt.jfed.lowlevel.JFedException if something went wrong while performing the call: either a server-error or an unexpected server response
     */
    @ApiMethod(order = 3, hint = "Allocate resources as described in a request RSpec argument to a slice with the named URN. This operation is " +
            "expected to start the allocated resources asynchronously after the operation has successfully completed. " +
            "Callers can check on the status of the resources using SliverStatus. Resources will be reserved until a " +
            "particular time, set by the aggregate according to policy. That expiration time will be no later than the " +
            "expiration time of the provided slice credential. This method returns a listing and description of the resources " +
            "reserved for the slice by this operation, in the form of a manifest RSpec.")
    @Nonnull
    public AggregateManagerReply<String> createSliver(@Nonnull SfaConnection con,
                                                      @ApiMethodParameter(name = "credentialList", hint = "mandatory list of credentials. This must include a valid credential for the slice.")
                                                      @Nonnull List<AnyCredential> credentialList,
                                                      @ApiMethodParameter(name = "sliceUrn", hint = "urn for the slice on which this sliver needs to be created")
                                                      @Nonnull String sliceUrn,
                                                      @ApiMethodParameter(name = "rspec", hint = " An RSpec matching the GENI standard request RSpec schema containing the resources that the caller is requesting for allocation to the slice specified in slice_urn.")
                                                      @Nonnull String rspec,
                                                      @ApiMethodParameter(name = "UserSpecList", hint = "An array of user structs, which contain information (typically SSH public keys) about needed to allow users to login on allocated nodes.")
                                                      @Nullable List<UserSpec> users,
                                                      @ApiMethodParameter(name = "extraOptions", hint = "extra options", required = false, parameterType = ApiMethodParameterType.GENI_EXTRA_OPTIONS)
                                                      @Nullable Map<String, Object> extraOptions) throws JFedException {
        Map<String, Object> methodParams =
                makeMethodParameters("credentialList", credentialList,
                        "sliceUrn", sliceUrn,
                        "rspec", rspec,
                        "users", users);

        Objects.requireNonNull(credentialList, "Illegal argument: " + "credentialList" + " is not allowed to be null");
        Objects.requireNonNull(sliceUrn, "Illegal argument: " + "sliceUrn" + " is not allowed to be null");
        Objects.requireNonNull(rspec, "Illegal argument: " + "rspec" + " is not allowed to be null");

        List<String> credentialsList = createCredentialsList(credentialList);

        List<Map> usersList;
        if (users != null) {
            usersList = users.stream().map(UserSpec::getAsMap).collect(Collectors.toList());
        } else
            usersList = Collections.emptyList();

        Map<String, Object> options = createOptionsMap(extraOptions);

        List<Object> args = new ArrayList<>(5);
        args.add(sliceUrn);
        args.add(credentialsList);
        args.add(rspec);
        args.add(usersList);
        args.add(options);

        return executeAndLogXmlRpcCommandGeni(methodParams, con, "createSliver", "CreateSliver",
                args, STRING_REPLY_CONVERTER);
    }

    /**
     * Get the status of a sliver or slivers belonging to the given slice at the given aggregate. Status may include
     * other dynamic reservation or instantiation information as required by the resource type and aggregate. This
     * method is used to provide updates on the state of the resources after the completion of CreateSliver, which began
     * to asynchronously provision and start the resources.
     *
     * @param con            the {@code GeniConnection} to use for this call. Must be a connection to an aggregate implementing
     *                       "GENI Aggregate Manager API Version 2".
     * @param credentialList mandatory list of credentials. This must include a valid credential for the slice.
     * @param sliceUrn       urn for the slice of which to request the status of the sliver at this aggregate
     * @param extraOptions   optional (not specified = null): Map with extra options.
     * @return status of the slice, wrapped in a {@code AggregateManager2.AggregateManagerReply}
     * @throws be.iminds.ilabt.jfed.lowlevel.JFedException if something went wrong while performing the call: either a server-error or an unexpected server response
     */
    @ApiMethod(order = 5, hint = "Get the status of a sliver or slivers belonging to the given slice at the given aggregate. Status may include " +
            "other dynamic reservation or instantiation information as required by the resource type and aggregate. This " +
            "method is used to provide updates on the state of the resources after the completion of CreateSliver, which began " +
            "to asynchronously provision and start the resources.")
    @Nonnull
    public AggregateManagerReply<SliverStatus> sliverStatus(@Nonnull SfaConnection con,
                                                            @ApiMethodParameter(name = "credentialList", hint = "mandatory list of credentials. This must include a valid credential for the slice.")
                                                            @Nonnull List<AnyCredential> credentialList,
                                                            @ApiMethodParameter(name = "sliceUrn", hint = "URN for the slice of which to request the status of the sliver at this aggregate")
                                                            @Nonnull String sliceUrn,
                                                            @ApiMethodParameter(name = "extraOptions", hint = "extra options", required = false, parameterType = ApiMethodParameterType.GENI_EXTRA_OPTIONS)
                                                            @Nullable Map<String, Object> extraOptions) throws JFedException {
        Map<String, Object> methodParams = makeMethodParameters("credentialList", credentialList,
                "sliceUrn", sliceUrn);

        Objects.requireNonNull(credentialList, "Illegal argument: " + "credentialList" + " is not allowed to be null");
        Objects.requireNonNull(sliceUrn, "Illegal argument: " + "sliceUrn" + " is not allowed to be null");

        List<String> credentialsList = createCredentialsList(credentialList);

        Map<String, Object> options = createOptionsMap(extraOptions);

        List<Object> args = new ArrayList<>(3);
        args.add(sliceUrn);
        args.add(credentialsList);
        args.add(options);
        return executeAndLogXmlRpcCommandGeni(methodParams, con, "sliverStatus", "SliverStatus",
                args, SliverStatus::new);
    }

    /**
     * Renews the resources in all slivers at this aggregate belonging to the given slice until the given time,
     * extending the lifetime of the slice. Aggregates may limit how long reservations may be extended. Initial sliver
     * expiration is set by aggregate policy, no later than the slice credential expiration time.
     *
     * @param con                   the {@code GeniConnection} to use for this call. Must be a connection to an aggregate implementing
     *                              "GENI Aggregate Manager API Version 2".
     * @param credentialList        mandatory list of credentials. This must include a valid credential for the slice.
     * @param sliceUrn              urn for the slice which sliver should be renewed
     * @param expirationTimeRfc3339 new time at which the sliver is to expire, in RFC3339 format. See {@link be.iminds.ilabt.jfed.util.RFC3339Util} for help converting to the correct format.
     * @param extraOptions          optional (not specified = null): Map with extra options.
     * @return a boolean (success or failure), wrapped in a {@code AggregateManager2.AggregateManagerReply}
     * @throws be.iminds.ilabt.jfed.lowlevel.JFedException if something went wrong while performing the call: either a server-error or an unexpected server response
     * @see <a href="https://tools.ietf.org/html/rfc3339">RFC 3339</a>
     */
    @ApiMethod(order = 6, hint = "Renews the resources in all slivers at this aggregate belonging to the given slice until the given time, " +
            "extending the lifetime of the slice. Aggregates may limit how long reservations may be extended. Initial sliver " +
            "expiration is set by aggregate policy, no later than the slice credential expiration time.")
    @Nonnull
    public AggregateManagerReply<Boolean> renewSliver(@Nonnull SfaConnection con,
                                                      @ApiMethodParameter(name = "credentialList", hint = "mandatory list of credentials. This must include a valid credential for the slice.")
                                                      @Nonnull List<AnyCredential> credentialList,
                                                      @ApiMethodParameter(name = "sliceUrn", hint = "URN for the slice which sliver should be renewed")
                                                      @Nonnull String sliceUrn,
                                                      @ApiMethodParameter(name = "expirationTime", hint = "New time at which the sliver is to expire, in RFC3339 format.")
                                                      @Nonnull String expirationTimeRfc3339,
                                                      @ApiMethodParameter(name = "extraOptions", hint = "extra options", required = false, parameterType = ApiMethodParameterType.GENI_EXTRA_OPTIONS)
                                                      @Nullable Map<String, Object> extraOptions) throws JFedException {
        Map<String, Object> methodParams = makeMethodParameters("credentialList", credentialList,
                "sliceUrn", sliceUrn,
                "expirationTimeRfc3339", expirationTimeRfc3339);

        Objects.requireNonNull(credentialList, "Illegal argument: " + "credentialList" + " is not allowed to be null");
        Objects.requireNonNull(expirationTimeRfc3339, "Illegal argument: " + "expirationTime" + " is not allowed to be null");
        Objects.requireNonNull(sliceUrn, "Illegal argument: " + "sliceUrn" + " is not allowed to be null");

        List<String> credentialsList = createCredentialsList(credentialList);

        Map<String, Object> options = createOptionsMap(extraOptions);

        List<Object> args = new ArrayList<>(4);
        args.add(sliceUrn);
        args.add(credentialsList);
        args.add(expirationTimeRfc3339); //date in RFC 3339
        args.add(options);

        return executeAndLogXmlRpcCommandGeni(methodParams, con, "renewSliver", "RenewSliver",
                args, resultValueObject -> {
                    if (resultValueObject instanceof Integer) {
                        Integer rawResultInteger = (Integer) resultValueObject;
                        if (rawResultInteger == 1)
                            return true;
                        else if (rawResultInteger == 0)
                            return false;
                        else
                            throw new BadReplyGeniException("Unexpected integer to signify a boolean operation: " + rawResultInteger);
                    } else if (resultValueObject instanceof String) {
                        return TextUtil.stringToBoolean((String) resultValueObject);
                    } else
                        throw new BadReplyGeniException("Expected an Integer or String as result, but got " + resultValueObject);
                });
    }

    /**
     * Convenience method calling {@link AggregateManager2#renewSliver(be.iminds.ilabt.jfed.lowlevel.connection.SfaConnection, java.util.List, String, String, java.util.Map)},
     * The expiration data is a Java data, which is automatically converted to RFC3339.
     *
     * @param expirationTime a Java data, which is automatically converted to RFC3339
     * @see AggregateManager2#renewSliver(be.iminds.ilabt.jfed.lowlevel.connection.SfaConnection, java.util.List, String, String, java.util.Map)
     */
    @Nonnull
    public AggregateManagerReply<Boolean> renewSliver(@Nonnull SfaConnection con,
                                                      @ApiMethodParameter(name = "credentialList")
                                                      @Nonnull List<AnyCredential> credentialList,
                                                      @ApiMethodParameter(name = "sliceUrn")
                                                      @Nonnull String sliceUrn,
                                                      @ApiMethodParameter(name = "expirationTime")
                                                      @Nonnull Date expirationTime,
                                                      @ApiMethodParameter(name = "extraOptions", hint = "extra options", required = false, parameterType = ApiMethodParameterType.GENI_EXTRA_OPTIONS)
                                                      @Nullable Map<String, Object> extraOptions) throws JFedException {
        Objects.requireNonNull(expirationTime, "Illegal argument: " + "expirationTime" + " is not allowed to be null");

        final String expirationTimeRfc3339 = RFC3339Util.dateToRFC3339String(expirationTime, true, true, true);

        return renewSliver(con, credentialList, sliceUrn, expirationTimeRfc3339, extraOptions);
    }

    /**
     * Delete any slivers at the given aggregate belonging to the given slice, by stopping the resources if they are
     * still running, and then deallocating the resources associated with the slice. When complete, this slice will own
     * no resources on this aggregate - any such resources will have been stopped.
     *
     * @param con            the {@code GeniConnection} to use for this call. Must be a connection to an aggregate implementing
     *                       "GENI Aggregate Manager API Version 2".
     * @param credentialList mandatory list of credentials. This must include a valid credential for the slice.
     * @param sliceUrn       urn for the slice of which delete the sliver at this aggregate
     * @param extraOptions   optional (not specified = null): Map with extra options.
     * @return a boolean (success or failure), wrapped in a {@code AggregateManager2.AggregateManagerReply}
     * @throws be.iminds.ilabt.jfed.lowlevel.JFedException if something went wrong while performing the call: either a server-error or an unexpected server response
     */
    @ApiMethod(order = 7, hint = "Delete any slivers at the given aggregate belonging to the given slice, by stopping the resources if they are " +
            "still running, and then deallocating the resources associated with the slice. When complete, this slice will own " +
            "no resources on this aggregate - any such resources will have been stopped.")
    @Nonnull
    public AggregateManagerReply<Boolean> deleteSliver(@Nonnull SfaConnection con,
                                                       @ApiMethodParameter(name = "credentialList", hint = "mandatory list of credentials. This must include a valid credential for the slice.")
                                                       @Nonnull List<AnyCredential> credentialList,
                                                       @ApiMethodParameter(name = "sliceUrn", hint = "urn for the slice of which delete the sliver at this aggregate")
                                                       @Nonnull String sliceUrn,
                                                       @ApiMethodParameter(name = "extraOptions", hint = "extra options", required = false, parameterType = ApiMethodParameterType.GENI_EXTRA_OPTIONS)
                                                       @Nullable Map<String, Object> extraOptions) throws JFedException {
        Map<String, Object> methodParams = makeMethodParameters("credentialList", credentialList,
                "sliceUrn", sliceUrn);

        Objects.requireNonNull(credentialList, "Illegal argument: " + "credentialList" + " is not allowed to be null");
        Objects.requireNonNull(sliceUrn, "Illegal argument: " + "sliceUrn" + " is not allowed to be null");

        List<String> credentialsList = createCredentialsList(credentialList);

        Map<String, Object> options = createOptionsMap(extraOptions);

        List<Object> args = new ArrayList<>(3);
        args.add(sliceUrn);
        args.add(credentialsList);
        args.add(options);
        return executeAndLogXmlRpcCommandGeni(methodParams, con, "deleteSliver", "DeleteSliver",
                args, BOOLEAN_REPLY_CONVERTER);
    }

    /**
     * Perform an emergency shut down of a sliver or slivers at this aggregate belonging to the given slice. This
     * operation is intended for administrative use. The sliver is shut down but remains available for further forensics.
     *
     * @param con            the {@code GeniConnection} to use for this call. Must be a connection to an aggregate implementing
     *                       "GENI Aggregate Manager API Version 2".
     * @param credentialList mandatory list of credentials, containing a credential authorizing this shutdown.
     * @param sliceUrn       urn for the slice of which shutdown the slivers at this aggregate
     * @param extraOptions   optional (not specified = null): Map with extra options.
     * @return a boolean (success or failure), wrapped in a {@code AggregateManager2.AggregateManagerReply}
     * @throws be.iminds.ilabt.jfed.lowlevel.JFedException if something went wrong while performing the call: either a server-error or an unexpected server response
     */
    @ApiMethod(order = 8, hint = "Perform an emergency shut down of a sliver or slivers at this aggregate belonging to the given slice. This " +
            "operation is intended for administrative use. The sliver is shut down but remains available for further forensics.")
    @Nonnull
    public AggregateManagerReply<Boolean> shutdown(@Nonnull SfaConnection con,
                                                   @ApiMethodParameter(name = "credentialList", hint = "Mandatory list of credentials, containing a credential authorizing this shutdown.")
                                                   @Nonnull List<AnyCredential> credentialList,
                                                   @ApiMethodParameter(name = "sliceUrn", hint = "URN for the slice of which shutdown the slivers at this aggregate")
                                                   @Nonnull String sliceUrn,
                                                   @ApiMethodParameter(name = "extraOptions", hint = "extra options", required = false, parameterType = ApiMethodParameterType.GENI_EXTRA_OPTIONS)
                                                   @Nullable Map<String, Object> extraOptions) throws JFedException {
        Map<String, Object> methodParams = makeMethodParameters("credentialList", credentialList,
                "sliceUrn", sliceUrn);

        Objects.requireNonNull(credentialList, "Illegal argument: " + "credentialList" + " is not allowed to be null");
        Objects.requireNonNull(sliceUrn, "Illegal argument: " + "sliceUrn" + " is not allowed to be null");

        List<String> credentialsList = createCredentialsList(credentialList);

        Map<String, Object> options = createOptionsMap(extraOptions);

        List<Object> args = new ArrayList<>(3);
        args.add(sliceUrn);
        args.add(credentialsList);
        args.add(options);

        return executeAndLogXmlRpcCommandGeni(methodParams, con, "shutdown", "Shutdown",
                args, BOOLEAN_REPLY_CONVERTER);
    }

    public static class VersionInfo extends AbstractVersionInfo {
        private int api;
        private List<VersionPair> apiVersions;
        private List<RspecVersion> requestRspecVersions;
        private List<RspecVersion> adRspecVersions;

        public VersionInfo(Object resultValueObject) throws BadReplyGeniException {
            Map<String, Object> raw = apiSpecifiesMapStringToObject(resultValueObject);

            Object apiObj = raw.get("geni_api");
            if (apiObj instanceof Integer) {
                api = (Integer) apiObj;
            } else
                api = -1;

            Map<String, String> apiVersionHT = apiSpecifiesMapStringToString(raw.get("geni_api_versions"));

            this.apiVersions = new ArrayList<>();

            for (Map.Entry<String, String> entry : apiVersionHT.entrySet()) {
                apiVersions.add(new VersionPair(entry.getKey(), entry.getValue()));
            }

            List<Map> requestRspecVersionsV = apiSpecifiesListOfT(Map.class, raw.get("geni_request_rspec_versions"));
            this.requestRspecVersions = new ArrayList<>();
            for (Map h : requestRspecVersionsV) {
                try {
                    requestRspecVersions.add(new RspecVersion(h));
                } catch (BadReplyGeniException e) {
                    throw new GeniSpecificationNonconformityException("GetVersion", getApiName(), "rspec geni_request_rspec_versions struct does not contain all needed fields", e);
                }
            }

            List<Map> adRspecVersionsV = apiSpecifiesListOfT(Map.class, raw.get("geni_ad_rspec_versions"));
            this.adRspecVersions = new ArrayList<>();
            for (Map h : adRspecVersionsV) {
                try {
                    adRspecVersions.add(new RspecVersion(h));
                } catch (BadReplyGeniException e) {
                    throw new GeniSpecificationNonconformityException("GetVersion", getApiName(), "rspec geni_ad_rspec_versions struct does not contain all needed fields", e);
                }
            }
        }

        @Override
        public int getApi() {
            return api;
        }

        public List<VersionPair> getApiVersions() {
            return apiVersions;
        }

        public List<RspecVersion> getRequestRspecVersions() {
            return requestRspecVersions;
        }

        public List<RspecVersion> getAdRspecVersions() {
            return adRspecVersions;
        }

        @Override
        public String toString() {
            return "VersionInfo{" +
                    "api=" + api +
                    ", apiVersions=" + apiVersions +
                    ", requestRspecVersions=" + requestRspecVersions +
                    ", adRspecVersions=" + adRspecVersions +
                    '}';
        }

        public static class VersionPair {
            private final String url;
            private final String versionNr;

            private VersionPair(String versionNr, String url) {
                this.versionNr = versionNr;
                this.url = url;
            }

            public String getUrl() {
                return url;
            }

            public String getVersionNr() {
                return versionNr;
            }

            @Override
            public String toString() {
                return "VersionPair{" +
                        "url='" + url + '\'' +
                        ", versionNr='" + versionNr + '\'' +
                        '}';
            }

            @Override
            public boolean equals(Object o) {
                if (this == o) return true;
                if (o == null || getClass() != o.getClass()) return false;

                VersionPair that = (VersionPair) o;

                if (url != null ? !Objects.equals(url, that.url) : that.url != null) return false;
                if (versionNr != null ? !Objects.equals(versionNr, that.versionNr) : that.versionNr != null)
                    return false;

                return true;
            }

            @Override
            public int hashCode() {
                int result = url != null ? url.hashCode() : 0;
                result = 31 * result + (versionNr != null ? versionNr.hashCode() : 0);
                return result;
            }
        }

        public static class RspecVersion {
            private final String type;
            private final String version;
            private final String schema;
            private final String namespace;
            private final List<String> extensions;

            private RspecVersion(Map ht) throws BadReplyGeniException {
                Map<String,Object> raw = apiSpecifiesMapStringToObject(ht);
                type = apiSpecifiesStringInMap(raw,"type");
                version = apiSpecifiesStringInMap(raw,"version");
                schema = apiSpecifiesStringInMap(raw,"schema");
                namespace = apiSpecifiesStringInMap(raw,"namespace");
                extensions = apiSpecifiesListOfT(String.class, ht.get("extensions"));
            }

            private RspecVersion(String type, String version, String schema, String namespace, List<String> extensions) {
                this.type = type;
                this.version = version;
                this.schema = schema;
                this.namespace = namespace;
                this.extensions = extensions;
            }

            public String getType() {
                return type;
            }

            public String getVersion() {
                return version;
            }

            public String getSchema() {
                return schema;
            }

            public String getNamespace() {
                return namespace;
            }

            public List<String> getExtensions() {
                List<String> res = new ArrayList<>();
                for (Object o : extensions)
                    res.add((String) o);
                return res;
            }

            @Override
            public String toString() {
                return "RspecVersion{" +
                        "type='" + type + '\'' +
                        ", version='" + version + '\'' +
                        ", schema='" + schema + '\'' +
                        ", namespace='" + namespace + '\'' +
                        ", extensions=" + extensions +
                        '}';
            }

            public boolean equalTypeAndVersion(RspecVersion o) {
                if (this == o) return true;
                if (o == null || getClass() != o.getClass()) return false;

                if (type != null ? !Objects.equals(type, o.type) : o.type != null) return false;
                if (version != null ? !Objects.equals(version, o.version) : o.version != null) return false;

                return true;
            }
        }
    }

    public static class SliverStatus {
        /**
         * Date example: Thu Aug 21 11:22:00 EDT 2014
         * <p>
         * if Locale.US is not set, the users locale might mess up parsing. See FEDIBBTDEV-948
         */
        protected static final SimpleDateFormat ORCA_EXPIRES_DATE_FORMAT =
                new SimpleDateFormat("EEE MMM d HH:mm:ss zzz yyyy", Locale.US);
        private final String urn;    //according to the API, this is sliver URN. But on emulab, this is the slice URN.
        private final String status;
        private final List<ResourceStatus> resources;
        private Date pgExpires;
        private Date orcaExpires;
        private Date geniExpires;
        private Date foamExpires;
        private Date plExpires;
        private List<UserSpec> users = null;

        public SliverStatus(Object resultValueObject) throws BadReplyGeniException {
            Map<String, Object> val = apiSpecifiesMapStringToObject(resultValueObject);
            urn = (String) val.get("geni_urn");
            status = (String) val.get("geni_status");
            String pgExpiresString = (String) val.get("pg_expires");
            if (pgExpiresString != null) {
                try {
                    //might not have final Z
                    int tIndex = pgExpiresString.indexOf('T');
                    String afterT = tIndex > 0 ? pgExpiresString.substring(tIndex) : pgExpiresString;
                    this.pgExpires = RFC3339Util.rfc3339StringToDate(
                            afterT.contains("Z") || afterT.contains("-") || afterT.contains("+") ?
                                    pgExpiresString :
                                    pgExpiresString + "Z");
                } catch (ParseException e) {
                    LOG.warn("Note: AMv2 SliverStatus reply contained invalid date for pg_expires: \"" + pgExpiresString + "\" (note: assuming it is zulu time by appending Z!)", e);
                    pgExpires = null;
                }
            }

            String foamExpiresString = (String) val.get("foam_expires");
            if (foamExpiresString != null) {
                try {
                    //
                    this.foamExpires = RFC3339Util.iso8601StringToDate(foamExpiresString);
                } catch (ParseException e) {
                    try {
                        LOG.debug("Note: AMv2 SliverStatus reply contained invalid date for foam_expires: \"" + foamExpiresString + "\" -> will try fallback assuming timezone is missing", e);
                        this.foamExpires = RFC3339Util.iso8601StringToDate(foamExpiresString + "Z");
                    } catch (ParseException e2) {
                        LOG.warn("Note: AMv2 SliverStatus reply contained invalid date for foam_expires: \"" + foamExpiresString + "\"", e2);
                        foamExpires = null;
                    }
                }
            }

            String plExpiresString = (String) val.get("pl_expires");
            if (plExpiresString != null) {
                try {
                    //
                    this.plExpires = RFC3339Util.rfc3339StringToDate(plExpiresString + "Z");
                } catch (ParseException e) {
                    LOG.warn("Note: AMv2 SliverStatus reply contained invalid date for pl_expires: \"" + plExpiresString + "\"", e);
                    plExpires = null;
                }
            }

            // example:   "geni_expires": "2014-09-06T11:58:58.000Z"
            String geniExpiresString = (String) val.get("geni_expires");
            if (geniExpiresString != null) {
                try {
                    //
                    this.geniExpires = RFC3339Util.rfc3339StringToDate(geniExpiresString);
                } catch (ParseException e) {
                    LOG.warn("Note: AMv2 SliverStatus reply contained invalid date for geni_expires: \"" + geniExpiresString + "\"", e);
                    geniExpires = null;
                }
            }

            resources = new ArrayList<>();
            List<Map> vect = apiSpecifiesListOfT(Map.class, val.get("geni_resources"));
            for (Map resourceHT : vect) {
                String u = (String) resourceHT.get("geni_urn");
                String s = (String) resourceHT.get("geni_status");
                String e = (String) resourceHT.get("geni_error");
                resources.add(new ResourceStatus(u, s, e));

                String orcaExpiresString = (String) resourceHT.get("orca_expires");
                if (orcaExpiresString != null) {
                    try {
                        this.orcaExpires = ORCA_EXPIRES_DATE_FORMAT.parse(orcaExpiresString);
                    } catch (ParseException ex) {
                        LOG.warn("Note: AMv2 SliverStatus reply date for orca_expires could not be parsed: \"" + orcaExpiresString + "\"", ex);
                        orcaExpires = null;
                    }

                    if (orcaExpires == null) {
                        //fallback to RFC3339 parsing.
                        try {
                            this.orcaExpires = RFC3339Util.rfc3339StringToDate(orcaExpiresString);
                        } catch (ParseException ex) {
                            LOG.warn("Note: AMv2 SliverStatus reply contained invalid RFC3339 date for orca_expires: \"" + orcaExpiresString + "\"", ex);
                            orcaExpires = null;
                        }
                    }

                    LOG.info("processed orca_expires date \"" + orcaExpiresString + "\" to " + orcaExpires);
                }
            }

            //this does not seem to be included in the API, and there are different version available.
            // -> we will handle this very carefully, and not propagate any failure
            Object usersObject = val.get("users");
            if (usersObject != null) {
                try {
                    List<Map> usersVect = apiSpecifiesListOfT(Map.class, usersObject);

                    users = new ArrayList<>();
                    for (Map userHt : usersVect) {
                        String u = (String) userHt.get("geni_urn");
                        String l = (String) userHt.get("login");
                        List<String> keysResult = null;

                        if (u == null) {
                            u = (String) userHt.get("urn");
                        }

                        if (u != null) {
                            final Object keysObject = userHt.get("keys");
                            if (keysObject != null) {
                                List<Map> keys = apiSpecifiesListOfT(Map.class, keysObject);
                                keysResult = new ArrayList<>();

                                for (Map keyHt : keys) {
                                    String key = (String) keyHt.get("key");
                                    String type = (String) keyHt.get("type");

                                    if (!Objects.equals(type, "ssh")) {
                                        LOG.warn("Ignoring key of unknown type '{}' for user {}", type, u);
                                    }

                                    keysResult.add(key);
                                }
                            }

                            users.add(new LoginSpec(u, keysResult, l));
                        }
                    }
                } catch (/*BadReplyGeni*/Exception e) {
                    LOG.warn("Error parsing API extension. This is not a big problem. It will be ignored.", e);
                }
            }
        }

        public String getUrn() {
            return urn;
        }

        public String getStatus() {
            return status;
        }

        public List<ResourceStatus> getResources() {
            return resources;
        }

        /**
         * custom field added by emulab. (so null on other aggregates!)
         */
        public Date getPgExpires() {
            return pgExpires;
        }

        /**
         * custom field added by foam. (so null on other aggregates!)
         */
        public Date getFoamExpires() {
            return foamExpires;
        }

        /**
         * custom field added by planetlab. (so null on other aggregates!)
         */
        public Date getPlExpires() {
            return plExpires;
        }

        /**
         * custom field added by ION and GRAM. (so null on other aggregates!)
         */
        public Date getGeniExpires() {
            return geniExpires;
        }

        /**
         * custom field added by orca. (so null on other aggregates!)
         */
        public Date getOrcaExpires() {
            return orcaExpires;
        }

        /**
         * @return expiration date of any AM specific expiration extension, or null if none found.
         */
        public Date getAnyExtExpires() {
            if (pgExpires != null) return pgExpires;
            if (orcaExpires != null) return orcaExpires;
            if (foamExpires != null) return foamExpires;
            if (geniExpires != null) return geniExpires;
            if (plExpires != null) return plExpires;
            return null;
        }

        @Override
        public String toString() {
            return "SliverStatus{" +
                    "urn='" + urn + '\'' +
                    ", status='" + status + '\'' +
                    ", resources=" + resources +
                    (pgExpires == null ? "" : (", pgExpires=" + pgExpires)) +
                    (foamExpires == null ? "" : (", foamExpires=" + foamExpires)) +
                    (plExpires == null ? "" : (", plExpires=" + plExpires)) +
                    (geniExpires == null ? "" : (", geniExpires=" + geniExpires)) +
                    (orcaExpires == null ? "" : (", orcaExpires=" + orcaExpires)) +
                    '}';
        }

        @Nullable
        public List<UserSpec> getUsers() {
            return users;
        }

        public static class ResourceStatus {
            private final String urn;
            //according to API this is sliver or component URN. In practice, this is sliver URN
            private final String status;
            private final String error;

            public ResourceStatus(String urn, String status, String error) {
                this.urn = urn;
                this.status = status;
                this.error = error;
            }

            public String getUrn() {
                return urn;
            }

            public String getStatus() {
                return status;
            }

            public String getError() {
                return error;
            }

            @Override
            public String toString() {
                return "ResourceStatus{" +
                        "urn='" + urn + '\'' +
                        ", status='" + status + '\'' +
                        ", error='" + error + '\'' +
                        '}';
            }
        }
    }

}
