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.connection.SfaConnection;
import be.iminds.ilabt.jfed.lowlevel.resourceid.ResourceId;
import be.iminds.ilabt.jfed.lowlevel.resourceid.ResourceUrn;
import be.iminds.ilabt.jfed.preferences.JFedPreferences;
import be.iminds.ilabt.jfed.util.GeniUrn;
import be.iminds.ilabt.jfed.util.KeyUtil;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.security.cert.X509Certificate;
import java.util.*;

/**
 * <p>This is a simple wrapper for the methods specified in the {@link <a href="http://www.protogeni.net/ProtoGeni/wiki/SliceAuthorityAPI">ProtoGeni Slice Authority API</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 ProtogeniSliceAuthority.SliceAuthorityReply} which wraps the actual return value of the call, if any.
 *
 * @author Wim Van de Meerssche
 * @see <a href="http://www.protogeni.net/ProtoGeni/wiki/SliceAuthorityAPI">ProtoGeni Slice Authority API</a>
 */
public class ProtogeniSliceAuthority extends AbstractApi {
    private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(ProtogeniSliceAuthority.class);
    protected static final ReplyConverter<Boolean> INTEGER_AS_BOOLEAN_REPLY_CONVERTER = resultValueObject -> {
        if (resultValueObject instanceof Integer) {
            return Integer.compare((Integer) resultValueObject, 0) != 0;
        } else
            throw new BadReplyGeniException("Expected an integer as a result, but got " + resultValueObject);
    };


    /**
     * Construct a new SliceAuthority, 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 the retrySettings to use, or null to use the static globalRetrySettings in AbstractApi
     */
    public ProtogeniSliceAuthority(@Nonnull Logger logger, @Nonnull RetrySettings retrySettings, @Nonnull JFedPreferences jFedPreferences) {
        super(logger, retrySettings, new ApiInfo.Api(ApiInfo.ApiName.PROTOGENI_SA, 1), jFedPreferences);
    }

    /**
     * Construct a new SliceAuthority, 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 ProtogeniSliceAuthority(@Nonnull Logger logger, @Nonnull JFedPreferences jFedPreferences) {
        this(logger, jFedPreferences.getRetrySettings(), jFedPreferences);
    }

    /**
     * The protogeni SA "credentials" argument, takes an array with credential string as argument.
     * So NOT an array with hashes like Geni AM v3 methods, but like AM v2
     */
    public static List createCredentialsList(List<AnyCredential> credentialList) {
        return AggregateManager2.createCredentialsList(credentialList);
    }

    /**
     * A human readable name for the implemented API
     *
     * @return "ProtoGeni Slice Authority API v1"
     */
    static public String getApiName() {
        return "ProtoGeni Slice Authority API v1";
    }

    private static void addCredentialParameter(@Nonnull List<AnyCredential> credentials, boolean forceSingleCredential, Map<String, Object> params) {
        if (forceSingleCredential) {
            if (credentials.isEmpty())
                throw new IllegalArgumentException("Asked to send a single credential, but got none");

            params.put("credential", credentials.get(0).getCredentialXml());
        } else {
            params.put("credentials", createCredentialsList(credentials));
        }
    }

    @Nonnull
    private static AnyCredential parseCredentialResult(String credentialName, Object resultValueObject) throws BadReplyGeniException {
        if (resultValueObject != null) {
            try {
                return
                        AnyCredential.createSfa2(credentialName, resultValueObject.toString());
            } catch (Exception e) {
                throw new BadReplyGeniException("Error parsing result credential '" + credentialName + "':" + e.getMessage(), e);
            }
        } else {
            throw new BadReplyGeniException("Expected credential '" + credentialName + "', but got null");
        }
    }

    /**
     * A human readable name for the implemented API
     *
     * @return "ProtoGeni Slice Authority API v1"
     */
    @Override
    public String getName() {
        return getApiName();
    }

    protected <T> SliceAuthorityReply<T> executeAndLogXmlRpcCommandGeni(@Nullable Map<String, Object> methodParams,
                                                                        @Nonnull SfaConnection con,
                                                                        @Nonnull String methodJavaName,
                                                                        @Nonnull String methodGeniName,
                                                                        @Nonnull List<Object> args,
                                                                        @Nonnull ReplyConverter<T> replyConverter) throws JFedException {
        return executeAndLogXmlRpcCommandGeni(methodParams, con, methodJavaName, methodGeniName, args, replyConverter, null);
    }

    protected <T> SliceAuthorityReply<T> executeAndLogXmlRpcCommandGeni(Map<String, Object> methodParams,
                                                                        SfaConnection con,
                                                                        @Nonnull String methodJavaName,
                                                                        @Nonnull String methodGeniName,
                                                                        @Nonnull List<Object> args,
                                                                        @Nonnull ReplyConverter<T> replyConverter,
                                                                        @Nullable T defaultValue) throws JFedException {

        XMLRPCCallDetailsWithCodeValueError res = executeXmlRpcCommandGeni(con, methodGeniName, args, methodParams);
        Object resultValueObject = res.getResultValueObject();
        SliceAuthorityReply<T> r;
        if (resultValueObject != null && !Objects.equals(resultValueObject, 0)) {
            try {
                r = new SliceAuthorityReply<>(res, replyConverter.convertXMLRPCCall(resultValueObject));
            } catch (AssertionError | Exception t) {
                handleErrorProcessingArguments(res, methodJavaName, methodGeniName, con, t);
                r = null;
            }
        } else
            r = null;

        if (r == null)
            r = new SliceAuthorityReply<>(res, defaultValue);
        log(res, r, methodJavaName, methodGeniName, con, methodParams);
        return r;
    }

    protected void handleErrorProcessingArguments(XMLRPCCallDetailsWithCodeValueError res,
                                                  String methodJavaName, String methodGeniName,
                                                  SfaConnection con, Throwable t) throws JFedException {
        if (isCallSuccess(res)) {
            //The call was reportedly a success, but the parsing of the call failed. We should throw an error
            log(res, null, methodJavaName, methodGeniName, con, null);
            throw new JFedException("Error parsing " + methodGeniName + " reply: " + (t == null ? "null"
                    : t.getMessage()), t, res);
        } else {
            //If call failed, error may be ignored.
            LOG.error("Error parsing {} reply: {}", methodGeniName, t.getMessage(), t);
        }
    }

    private static boolean isCallSuccess(XMLRPCCallDetailsWithCodeValueError res) {
        Object resultCode = res.getResultCode();
        if (!(resultCode instanceof Integer)) {
            return false;
        }
        try {
            GeniResponseCode genicode = GeniAMResponseCode.getByCode((Integer) resultCode);
            return genicode.isSuccess();
        } catch (Exception ignored) {
            return false;
        }
    }

    @Override
    protected boolean isBusyReply(be.iminds.ilabt.jfed.lowlevel.XMLRPCCallDetails res) {
        if (res instanceof XMLRPCCallDetailsWithCodeValueError) {
            XMLRPCCallDetailsWithCodeValueError geniDetails = (XMLRPCCallDetailsWithCodeValueError) res;
            int code = (Integer) geniDetails.getResultCode();
            try {
                return GeniAMResponseCode.getByCode(code).isBusy();
            } catch (UnknownResponseCodeException e) {
                LOG.warn("Did not recognise AM response code to determine if it is busy", e);
                return false;
            }
        } else
            return false;
    }

    /**
     * GetVersion call: Get info about the version of the API supported at the server.
     *
     * @param con the {@link be.iminds.ilabt.jfed.lowlevel.connection.SfaConnection} to use for this call. Must be a connection to a slice authority.
     * @return The version as a String, wrapped in a {@code SliceAuthority.SliceAuthorityReply}
     * @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 = 1, hint = "GetVersion call: Get info about the version of the API supported at the server.")
    public SliceAuthorityReply<String> getVersion(@Nonnull SfaConnection con) throws JFedException {
        return executeAndLogXmlRpcCommandGeni(null, con, "getVersion", "get_version",
                new ArrayList<>(), STRING_REPLY_CONVERTER);
    }

    /**
     * GetCredential call, without arguments: Get the credential of the current user (= the user used to authorize the {@link be.iminds.ilabt.jfed.lowlevel.connection.SfaConnection}).
     *
     * @param con the {@code GeniConnection} to use for this call. Must be a connection to a slice authority.
     * @return the {@code AnyCredential} for the current user, wrapped in a {@code SliceAuthority.SliceAuthorityReply}
     * @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 = "GetCredential call, without arguments: Get the credential of the current user.")
    public SliceAuthorityReply<AnyCredential> getCredential(@Nonnull SfaConnection con) throws JFedException {
        return executeAndLogXmlRpcCommandGeni(null, con, "getCredential", "GetCredential",
                new ArrayList<>(), resultValueObject -> {
                    if (resultValueObject != null) {
                        try {
                            return AnyCredential.createSfa2("SliceAuthority getCredential", resultValueObject.toString());
                        } catch (CredentialException e) {
                            throw new BadReplyGeniException("Exception processing credential: " + e.getMessage(), e);
                        }
                    } else
                        throw new BadReplyGeniException("Expected a ClearingHouse Credential, but got null");
                });
    }

    /**
     * GetCredential call, with a slice ID arguments: Get the credential of the specified slice.
     *
     * @param con            the {@code GeniConnection} to use for this call. Must be a connection to a slice authority.
     * @param userCredential the credential for the user requesting the slice credential
     * @param slice          a slice ID
     * @return the {@code AnyCredential } for the requested slice, wrapped in a {@code SliceAuthority.SliceAuthorityReply}
     * @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 = "GetCredential call, with a slice ID arguments: Get the credential of the specified slice.")
    public SliceAuthorityReply<AnyCredential> getSliceCredential(@Nonnull SfaConnection con,
                                                                 @ApiMethodParameter(name = "userCredential", hint = "the credential for the user requesting the slice credential")
                                                                 @Nonnull AnyCredential userCredential,
                                                                 @ApiMethodParameter(name = "slice", hint = "a slice ID (typically the slice URN).")
                                                                 @Nonnull ResourceId slice) throws JFedException {
        return getSliceCredential(con, userCredential.toCredentialList(), true, slice);
    }

    @ApiMethod(order = 4, hint = "GetCredential call, with a slice ID arguments: Get the credential of the specified slice.")
    public SliceAuthorityReply<AnyCredential> getSliceCredential(@Nonnull SfaConnection con,
                                                                 @ApiMethodParameter(name = "userCredentials", hint = "a list of credentials for the user requesting the slice credential")
                                                                 @Nullable List<AnyCredential> userCredentials,
                                                                 @ApiMethodParameter(name = "slice", hint = "a slice ID (typically the slice URN).")
                                                                 @Nonnull ResourceId slice) throws JFedException {
        return getSliceCredential(con, userCredentials, false, slice);
    }

    protected SliceAuthorityReply<AnyCredential> getSliceCredential(@Nonnull SfaConnection con,
                                                                    @Nullable List<AnyCredential> userCredentials,
                                                                    boolean forceSendSingleCredential /*if true, a single credential is sent instead of a list*/,
                                                                    @Nonnull ResourceId slice) throws JFedException {
        Map<String, Object> methodParams;
        if (forceSendSingleCredential) {
            methodParams = makeMethodParameters("userCredential",
                    userCredentials != null && !userCredentials.isEmpty() ? userCredentials.get(0) : null,
                    "slice", slice);
        } else
            methodParams = makeMethodParameters("userCredentials", userCredentials,
                    "slice", slice);

        Map<String, Object> params = new HashMap<>();

        params.put(slice.getType(), slice.getValue());
        params.put("type", "Slice");
        if (userCredentials != null) {
            addCredentialParameter(userCredentials, forceSendSingleCredential, params);
        }

        return executeAndLogXmlRpcCommandGeni(methodParams, con, "getSliceCredential", "GetCredential",
                Collections.singletonList(params), resultValueObject -> parseCredentialResult("SliceAuthority getSliceCredential", resultValueObject));
    }

    /**
     * GetCredential call, with any type of argument: Get the credential of the specified type.
     *
     * @param con            the {@code GeniConnection} to use for this call. Must be a connection to a slice authority.
     * @param userCredential the credential for the user requesting the slice credential
     * @param type           the type of object for which to request a credential
     * @param object         a slice ID
     * @return the {@code AnyCredential } for the requested slice, wrapped in a {@code SliceAuthority.SliceAuthorityReply}
     * @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 = "GetCredential call, with any type: Get the credential for an object of the specified type.")
    public SliceAuthorityReply<AnyCredential> getAnyCredential(@Nonnull SfaConnection con,
                                                               @ApiMethodParameter(name = "userCredential", hint = "the credential for the user requesting the slice credential")
                                                               @Nonnull AnyCredential userCredential,
                                                               @ApiMethodParameter(name = "type", hint = "the type of object for which to request a credential (for example \"Slice\")", guiDefault = "Slice", required = false)
                                                               @Nullable String type,
                                                               @ApiMethodParameter(name = "resource", hint = "an ID (typically an URN or UUID).", required = false)
                                                               @Nullable ResourceId object) throws JFedException {

        return getAnyCredential(con, userCredential.toCredentialList(), true, type, object);
    }

    @ApiMethod(order = 6, hint = "GetCredential call, with any type: Get the credential for an object of the specified type.")
    public SliceAuthorityReply<AnyCredential> getAnyCredential(@Nonnull SfaConnection con,
                                                               @ApiMethodParameter(name = "userCredentials",
                                                                       hint = "a list of credentials for the user requesting a credential",
                                                                       parameterType = ApiMethodParameterType.LIST_OF_CREDENTIAL)
                                                                       List<AnyCredential> userCredentials,
                                                               @ApiMethodParameter(name = "type", hint = "the type of object for which to request a credential (for example \"Slice\")", guiDefault = "Slice", required = false)
                                                               @Nullable String type,
                                                               @ApiMethodParameter(name = "object", hint = "an ID (typically an URN or UUID).", required = false)
                                                               @Nullable ResourceId object) throws JFedException {
        return getAnyCredential(con, userCredentials, false, type, object);
    }

    public SliceAuthorityReply<AnyCredential> getAnyCredential(@Nonnull SfaConnection con,
                                                               @Nullable List<AnyCredential> userCredentials,
                                                               boolean forceSendSingleCredential /*if true, a single credential is sent instead of a list*/,
                                                               @Nullable String type,
                                                               @Nullable ResourceId object) throws JFedException {
        Map<String, Object> methodParams;
        if (forceSendSingleCredential) {
            methodParams = makeMethodParameters("userCredential",
                    userCredentials != null && !userCredentials.isEmpty() ? userCredentials.get(0)
                            : null, "type", type, "object", object);
        } else {
            methodParams = makeMethodParameters("userCredentials", userCredentials, "type", type, "object", object);
        }

        Map<String, Object> params = new HashMap<>();

        if (object != null)
            params.put(object.getType()/*example type: "urn" */, object.getValue());
        if (type != null)
            params.put("type", type);
        if (userCredentials != null) {
            addCredentialParameter(userCredentials, forceSendSingleCredential, params);
        }


        return executeAndLogXmlRpcCommandGeni(methodParams, con, "getAnyCredential", "GetCredential",
                Collections.singletonList(params), resultValueObject -> parseCredentialResult("SliceAuthority getAnyCredential for type " + type, resultValueObject));

    }

    /**
     * Resolve call, with a slice ID arguments: Get info about the specified slice.
     *
     * @param con            the {@code GeniConnection} to use for this call. Must be a connection to a slice authority.
     * @param userCredential the credential for the user requesting the info
     * @param slice          a slice ID
     * @return a Map containing info about the slice, wrapped in a {@code SliceAuthority.SliceAuthorityReply}
     * @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 = 10, hint = "Resolve call, with a slice ID arguments: Get info about the specified slice.")
    public SliceAuthorityReply<SliceInfo> resolveSlice(@Nonnull SfaConnection con,
                                                           @ApiMethodParameter(name = "userCredential", hint = "the credential for the user requesting the info")
                                                           @Nonnull AnyCredential userCredential,
                                                           @ApiMethodParameter(name = "slice", hint = "a slice ID (typically the slice URN).")
                                                           @Nonnull ResourceId slice) throws JFedException {

        return resolveSlice(con, userCredential.toCredentialList(), true, slice);
    }

    @ApiMethod(order = 11, hint = "Resolve call, with a slice ID arguments: Get info about the specified slice.")
    public SliceAuthorityReply<SliceInfo> resolveSlice(@Nonnull SfaConnection con,
                                                           @ApiMethodParameter(name = "userCredentials", hint = "a list of credentials for the user requesting the info")
                                                           @Nonnull List<AnyCredential> userCredentials,
                                                           @ApiMethodParameter(name = "slice", hint = "a slice ID (typically the slice URN).")
                                                           @Nonnull ResourceId slice) throws JFedException {

        return resolveSlice(con, userCredentials, false, slice);
    }

    public SliceAuthorityReply<SliceInfo> resolveSlice(@Nonnull SfaConnection con,
                                                           @Nonnull List<AnyCredential> userCredentials,
                                                           boolean forceSendSingleCredential /*if true, a single credential is sent instead of a list*/,
                                                           @Nonnull ResourceId slice) throws JFedException {
        Map<String, Object> methodParams;
        if (forceSendSingleCredential) {
            methodParams =
                    makeMethodParameters("userCredential", !userCredentials.isEmpty() ? userCredentials.get(0) : null,
                            "slice", slice);
        } else {
            methodParams = makeMethodParameters("userCredentials", userCredentials, "slice", slice);
        }

        Map<String, Object> params = new HashMap<>();
        params.put(slice.getType(), slice.getValue());
        addCredentialParameter(userCredentials, forceSendSingleCredential, params);
        params.put("type", "Slice");

        List<Object> args = Collections.singletonList(params);

        return executeAndLogXmlRpcCommandGeni(methodParams, con, "resolveSlice", "Resolve",
                args, SliceInfo::new);
    }

    public static class SliceInfo {
        private final GeniUrn sliceUrn;
        private final String uuid;
        private final String creatorUuid;
        private final GeniUrn creatorUrn;
        private final Gid gid;

        private final List<GeniUrn> componentManagers;


        public SliceInfo(Object resultValueObject) throws BadReplyGeniException {
            Map<String, Object> ht = apiSpecifiesMapStringToObject(resultValueObject);
            this.sliceUrn = apiSpecifiesGeniUrnInMap(ht, "urn");
            this.uuid = apiSpecifiesStringInMap(ht, "uuid");
            this.creatorUuid = apiSpecifiesStringInMap(ht, "creator_uuid");
            this.creatorUrn = apiSpecifiesGeniUrnInMap(ht, "creator_urn");
            this.gid = new Gid(apiSpecifiesStringInMap(ht, "gid"));

            List<String> cmV = apiSpecifiesListOfString(ht.get("component_managers"));

            componentManagers = new ArrayList<>();
            for (String cmUrn : cmV) {
                componentManagers.add(apiSpecifiesGeniUrn(cmUrn));
            }
        }

        public GeniUrn getSliceUrn() {
            return sliceUrn;
        }

        public String getUuid() {
            return uuid;
        }

        public String getCreatorUuid() {
            return creatorUuid;
        }

        public GeniUrn getCreatorUrn() {
            return creatorUrn;
        }

        public Gid getGid() {
            return gid;
        }

        public List<GeniUrn> getComponentManagers() {
            return componentManagers;
        }
    }


    /**
     * Resolve call, with a user ID arguments: Get info about the specified user.
     *
     * @param con            the {@code GeniConnection} to use for this call. Must be a connection to a slice authority.
     * @param userCredential the credential for the user requesting the info
     * @param user           a user ID
     * @return a Map containing info about the user, wrapped in a {@code SliceAuthority.SliceAuthorityReply}
     * @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 = 12, hint = "Resolve call, with a user ID arguments: Get info about the specified user.")
    public SliceAuthorityReply<UserInfo> resolveUser(@Nonnull SfaConnection con,
                                                     @ApiMethodParameter(name = "userCredential", hint = "the credential for the user requesting the info")
                                                     @Nonnull AnyCredential userCredential,
                                                     @ApiMethodParameter(name = "user", hint = "a user ID (typically the user URN).")
                                                     @Nonnull ResourceId user) throws JFedException {

        return resolveUser(con, userCredential.toCredentialList(), true, user);
    }

    @ApiMethod(order = 13, hint = "Resolve call, with a user ID arguments: Get info about the specified user.")
    public SliceAuthorityReply<UserInfo> resolveUser(@Nonnull SfaConnection con,
                                                     @ApiMethodParameter(name = "userCredentials",
                                                             hint = "a list of credentials for the user requesting the info",
                                                             parameterType = ApiMethodParameterType.LIST_OF_CREDENTIAL)
                                                     @Nonnull List<AnyCredential> userCredentials,
                                                     @ApiMethodParameter(name = "user", hint = "a user ID (typically the user URN).")
                                                     @Nonnull ResourceId user) throws JFedException {

        return resolveUser(con, userCredentials, false, user);
    }

    public SliceAuthorityReply<UserInfo> resolveUser(@Nonnull SfaConnection con,
                                                     @Nonnull List<AnyCredential> userCredentials,
                                                     boolean forceSendSingleCredential /*if true, a single credential is sent instead of a list*/,
                                                     @Nonnull ResourceId user) throws JFedException {
        Map<String, Object> methodParams;
        if (forceSendSingleCredential) {
            methodParams =
                    makeMethodParameters("userCredential", !userCredentials.isEmpty() ? userCredentials.get(0) : null,
                            "user", user);
        } else {
            methodParams = makeMethodParameters("userCredentials", userCredentials, "user", user);
        }

        Map<String, Object> params = new HashMap<>();
        params.put(user.getType(), user.getValue());
        addCredentialParameter(userCredentials, forceSendSingleCredential, params);
        params.put("type", "User");

        return executeAndLogXmlRpcCommandGeni(methodParams, con, "resolveUser", "Resolve",
                Collections.singletonList(params), UserInfo::new);
    }

    public static class UserInfo {
        private final String uid;
        private final String name;
        private final String uuid;
        private final String email;
        private final String hrn;
        private final String urn;
        private final List<GeniUrn> slices;
        @Nonnull
        private final Map<GeniUrn, String> subAuthorities;

        @Nullable
        private final X509Certificate certificate;


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

            this.uid = apiSpecifiesNullableString(ht.get("uid"));
            this.name = apiSpecifiesNullableString(ht.get("name"));
            this.uuid = apiSpecifiesNullableString(ht.get("uuid"));
            this.email = apiSpecifiesNullableString(ht.get("email"));
            this.urn = apiSpecifiesNullableString(ht.get("urn"));
            this.hrn = apiSpecifiesNullableString(ht.get("hrn"));

            List<String> slicesVect = apiSpecifiesListOfString(ht.get("slices"));
            slices = new ArrayList<>();
            for (String s : slicesVect) {
                try {
                    slices.add(new GeniUrn(s));
                } catch (GeniUrn.GeniUrnParseException e) {
                    throw new BadReplyGeniException("Not a valid slice urn: '" + s + "'");
                }
            }

            if (ht.containsKey("subauthorities")) {
                Map<String, String> subAuthHt = apiSpecifiesMapStringToString(ht.get("subauthorities"));
                subAuthorities = new HashMap<>();


                for (Map.Entry<String, String> entry : subAuthHt.entrySet()) {
                    String urnStr = entry.getKey();
                    String urlStr = entry.getValue();

                    GeniUrn urn;
                    try {
                        urn = new GeniUrn(urnStr);
                    } catch (GeniUrn.GeniUrnParseException e) {
                        throw new BadReplyGeniException("Not a valid subAuthority urn: '" + urnStr + "'");
                    }
                    String subAuthName = urn.getEncodedSubAuthName();

                    subAuthorities.put(urn, urlStr);
                }
            } else {
                LOG.warn("resolveUser did not return 'subauthorities'");
                subAuthorities = Collections.emptyMap();
            }

            if (ht.containsKey("gid")) {
                String gid = apiSpecifiesNonNullString(ht.get("gid"));
                certificate = KeyUtil.pemToX509Certificate(gid);
            } else {
                LOG.warn("resolveUser did not return 'gid'");
                certificate = null;
            }
        }

        public List<GeniUrn> getSlices() {
            return slices;
        }

        @Nonnull
        public Map<GeniUrn, String> getSubAuthorities() {
            return subAuthorities;
        }

        @Nullable
        public X509Certificate getCertificate() {
            return certificate;
        }
    }

    private static List<GeniUrn> processListOfSlicesReply(Object resultObjectValue) throws BadReplyGeniException {
        Map<String, Object> ht = apiSpecifiesMapStringToObject(resultObjectValue);
        List<String> slicesVect = apiSpecifiesListOfString(ht.get("slices"));
        List<GeniUrn> res = new ArrayList<>();
        for (String s : slicesVect) {
            try {
                res.add(new GeniUrn(s));
            } catch (GeniUrn.GeniUrnParseException e) {
                throw new BadReplyGeniException("Not a slice urn: '" + s + "'");
            }
        }
        return res;
    }

    /**
     * Resolve call, with a any types: Get info about an object of the specified type.
     *
     * @param con            the {@code GeniConnection} to use for this call. Must be a connection to a slice authority.
     * @param userCredential the credential for the user requesting the info
     * @param type           a user ID
     * @param target         a user ID
     * @return a Map containing info about the user, wrapped in a {@code SliceAuthority.SliceAuthorityReply}
     * @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 = 14, hint = "Resolve call, with a free URN and type arguments: Get info about a specified object.")
    public SliceAuthorityReply<Map<String, Object>> resolveAny(@Nonnull SfaConnection con,
                                                                     @ApiMethodParameter(name = "userCredential", hint = "the credential for the user requesting the info")
                                                                     @Nonnull AnyCredential userCredential,
                                                                     @ApiMethodParameter(name = "target", hint = "an URN")
                                                                     @Nonnull ResourceId target,
                                                                     @ApiMethodParameter(name = "type", hint = "the type to resolve.")
                                                                     @Nonnull String type) throws JFedException {
        Map<String, Object> methodParams = makeMethodParameters("userCredential", userCredential,
                "target", target,
                "type", type);

        Map<String, Object> params = new HashMap<>();
        params.put(target.getType(), target.getValue());
        params.put("credential", userCredential.getCredentialXml());
        params.put("type", type);

        return executeAndLogXmlRpcCommandGeni(methodParams, con, "resolveAny", "Resolve",
                Collections.singletonList(params), AbstractApi::apiSpecifiesMapStringToObject);
    }

    /**
     * BindToSlice call: give another user permission to use a slice.
     *
     * @param con             the {@code GeniConnection} to use for this call. Must be a connection to a slice authority.
     * @param sliceCredential the credential for the slice to be shared
     * @param user            the ID of the user which will get permission to use this slice. (urn format: {@code urn:publicid:IDN+<AUTHORITY>+user+<USER>})
     * @return true if successful, wrapped in a {@code SliceAuthority.SliceAuthorityReply}
     * @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 = 20, hint = "BindToSlice call: give another user permission to use a slice.")
    public SliceAuthorityReply<Boolean> bindToSlice(@Nonnull SfaConnection con,
                                                    @ApiMethodParameter(name = "sliceCredential", hint = "the credential for the slice to be shared")
                                                    @Nonnull AnyCredential sliceCredential,
                                                    @ApiMethodParameter(name = "user", hint = "the ID of the user which will get permission to use this slice (typically the other user's URN).")
                                                    @Nonnull ResourceId user) throws JFedException {
        return bindToSlice(con, sliceCredential.toCredentialList(), true, user);

    }

    @ApiMethod(order = 20, hint = "BindToSlice call: give another user permission to use a slice.")
    public SliceAuthorityReply<Boolean> bindToSlice(@Nonnull SfaConnection con,
                                                    @ApiMethodParameter(name = "sliceCredentials", hint = "a list if credentials for the slice to be shared")
                                                    @Nonnull List<AnyCredential> sliceCredentials,
                                                    @ApiMethodParameter(name = "user", hint = "the ID of the user which will get permission to use this slice (typically the other user's URN).")
                                                    @Nonnull ResourceId user) throws JFedException {
        return bindToSlice(con, sliceCredentials, false, user);
    }

    public SliceAuthorityReply<Boolean> bindToSlice(@Nonnull SfaConnection con,
                                                    @Nonnull List<AnyCredential> sliceCredentials,
                                                    boolean forceSendSingleCredential /*if true, a single credential is sent instead of a list*/,
                                                    @Nonnull ResourceId user) throws JFedException {
        Map<String, Object> methodParams;
        if (forceSendSingleCredential)
            methodParams = makeMethodParameters("sliceCredential",
                    !sliceCredentials.isEmpty() ? sliceCredentials.get(0) : null, "user", user);
        else
            methodParams = makeMethodParameters("sliceCredentials", sliceCredentials, "user", user);

        Map<String, Object> params = new HashMap<>();
        params.put(user.getType(), user.getValue());
        addCredentialParameter(sliceCredentials, forceSendSingleCredential, params);

        return executeAndLogXmlRpcCommandGeni(methodParams, con, "bindToSlice", "BindToSlice",
                Collections.singletonList(params), INTEGER_AS_BOOLEAN_REPLY_CONVERTER);
    }

    /**
     * Register call: create a new slice.
     *
     * @param con            the {@code GeniConnection} to use for this call. Must be a connection to a slice authority.
     * @param userCredential the credential of the user creating this slice.
     * @param slice          the URN of the new slice (urn format: {@code urn:publicid:IDN+<AUTHORITY>+slice+<SLICENAME>})
     * @return the {@code AnyCredential} for the created slice, wrapped in a {@code SliceAuthority.SliceAuthorityReply}
     * @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 = 25, hint = "Register call: create a new slice.")
    public SliceAuthorityReply<AnyCredential> register(@Nonnull SfaConnection con,
                                                       @ApiMethodParameter(name = "userCredential", hint = "the credential of the user creating this slice.")
                                                               AnyCredential userCredential,
                                                       @ApiMethodParameter(name = "slice", hint = "the URN of the new slice (urn syntax: urn:publicid:IDN+<AUTHORITY>+slice+<SLICENAME>)")
                                                               ResourceUrn slice) throws JFedException {
        return register(con, userCredential.toCredentialList(), true, slice);
    }

    /**
     * Register call: create a new slice. (this variations allows multiple credentials)
     *
     * @param con             the {@code GeniConnection} to use for this call. Must be a connection to a slice authority.
     * @param userCredentials a list of credentials of the user creating this slice.
     * @param slice           the URN of the new slice (urn format: {@code urn:publicid:IDN+<AUTHORITY>+slice+<SLICENAME>})
     * @return the {@code AnyCredential} for the created slice, wrapped in a {@code SliceAuthority.SliceAuthorityReply}
     * @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 = 26, hint = "Register call: create a new slice.")
    public SliceAuthorityReply<AnyCredential> register(@Nonnull SfaConnection con,
                                                       @ApiMethodParameter(name = "userCredentials",
                                                               hint = "a list of credentials of the user creating this slice.",
                                                               parameterType = ApiMethodParameterType.LIST_OF_CREDENTIAL)
                                                               List<AnyCredential> userCredentials,
                                                       @ApiMethodParameter(name = "slice", hint = "the URN of the new slice (urn syntax: urn:publicid:IDN+<AUTHORITY>+slice+<SLICENAME>)")
                                                               ResourceUrn slice) throws JFedException {
        return register(con, userCredentials, false, slice);
    }

    public SliceAuthorityReply<AnyCredential> register(@Nonnull SfaConnection con,
                                                       @Nonnull List<AnyCredential> userCredentials,
                                                       boolean forceSendSingleCredential /*if true, a single credential is sent instead of a list*/,
                                                       @Nonnull ResourceUrn slice) throws JFedException {
        assert !userCredentials.isEmpty();

        Map<String, Object> methodParams;
        if (forceSendSingleCredential) {
            methodParams =
                    makeMethodParameters("userCredential", !userCredentials.isEmpty() ? userCredentials.get(0) : null,
                            "slice", slice);
        } else
            methodParams = makeMethodParameters("userCredentials", userCredentials,
                    "slice", slice);


        if (!Objects.equals(slice.getType(), "urn"))
            throw new JFedException("Bad type for ResourceId slice (\"" + slice.getType() + "\"). Only \"urn\" is supported here");

        Map<String, Object> params = new HashMap<>();
        params.put("type", "Slice");
        addCredentialParameter(userCredentials, forceSendSingleCredential, params);
        params.put(slice.getType(), slice.getValue());

        return executeAndLogXmlRpcCommandGeni(methodParams, con, "register", "Register",
                Collections.singletonList(params), resultValueObject -> parseCredentialResult("SliceAuthority register " + slice.getValue(), resultValueObject));
    }

    /**
     * RenewSlice call: Change the expiration date of a slice.
     *
     * @param con                the {@code GeniConnection} to use for this call. Must be a connection to a slice authority.
     * @param sliceCredential    the credential for the slice to be renewed
     * @param expiration_rfc3339 a string with the new expiration date, in RFC3339 format. See {@link be.iminds.ilabt.jfed.util.RFC3339Util} for help converting to the correct format.
     * @return the {@code AnyCredential} for the renewed slice, wrapped in a {@code SliceAuthority.SliceAuthorityReply}
     * @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 = 30, hint = "RenewSlice call: Change the expiration date of a slice.")
    public SliceAuthorityReply<AnyCredential> renewSlice(@Nonnull SfaConnection con,
                                                         @ApiMethodParameter(name = "sliceCredential", hint = "the credential for the slice to be renewed")
                                                         @Nonnull AnyCredential sliceCredential,
                                                         @ApiMethodParameter(name = "expiration_rfc3339", hint = "A string with the new experiation date, in RFC3339 format.")
                                                         @Nonnull String expiration_rfc3339) throws JFedException {
        return renewSlice(con, sliceCredential.toCredentialList(), true, expiration_rfc3339);
    }

    @ApiMethod(order = 31, hint = "RenewSlice call: Change the expiration date of a slice.")
    public SliceAuthorityReply<AnyCredential> renewSlice(@Nonnull SfaConnection con,
                                                         @ApiMethodParameter(name = "sliceCredentials",
                                                                 hint = "the credential for the slice to be renewed",
                                                                 parameterType = ApiMethodParameterType.LIST_OF_CREDENTIAL)
                                                         @Nonnull List<AnyCredential> sliceCredentials,
                                                         @ApiMethodParameter(name = "expiration_rfc3339", hint = "A string with the new experiation date, in RFC3339 format.")
                                                         @Nonnull String expiration_rfc3339) throws JFedException {
        return renewSlice(con, sliceCredentials, false, expiration_rfc3339);
    }

    public SliceAuthorityReply<AnyCredential> renewSlice(@Nonnull SfaConnection con,
                                                         @ApiMethodParameter(name = "sliceCredentials",
                                                                 hint = "the credential for the slice to be renewed",
                                                                 parameterType = ApiMethodParameterType.LIST_OF_CREDENTIAL)
                                                         @Nonnull List<AnyCredential> sliceCredentials,
                                                         boolean forceSendSingleCredential /*if true, a single credential is sent instead of a list*/,
                                                         @Nonnull String expiration_rfc3339) throws JFedException {
        Map<String, Object> methodParams;
        if (forceSendSingleCredential) {
            methodParams =
                    makeMethodParameters("sliceCredential",
                            !sliceCredentials.isEmpty() ? sliceCredentials.get(0) : null,
                            "expiration_rfc3339", expiration_rfc3339);
        } else {
            methodParams =
                    makeMethodParameters("sliceCredentials", sliceCredentials, "expiration_rfc3339", expiration_rfc3339);
        }

        Map<String, Object> params = new HashMap<>();
        params.put("expiration", expiration_rfc3339);
        addCredentialParameter(sliceCredentials, forceSendSingleCredential, params);

        return executeAndLogXmlRpcCommandGeni(methodParams, con, "renewSlice", "RenewSlice",
                Collections.singletonList(params), resultValueObject -> parseCredentialResult("SliceAuthority renewSlice", resultValueObject));
    }

    /**
     * <p>Shutdown call:
     * <p/>
     * <p>API description:
     * <p>Perform an emergency shutdown on a slice, by asking the SA (for that slice) to do an emergency shutdown. Operationally, the request is forwarded to the ClearingHouse which knows the full set of Component Managers. The call returns once the ClearingHouse is notified; the ClearingHouse will process the request asynchronously.
     * <p/>
     * <p/>
     * <p/>
     * <p>Clearinghouse API description:
     * <p>Perform an emergency shutdown on a slice. This is typically invoked by the Slice Authority for the slice, but may be invoked by anyone with a clearinghouse credential (this needs to change to allow anyone with a valid slice credential). As it stands, anyone with a slice credential can contact the Slice Authority for the slice, and ask it to do the shutown operation.
     * <p>Since the Clearinghouse must contact each Component Manager to tell it to shutdown the slice, this call will return immediately. There is currently no facility to find out if/when the shutdown has completed.
     *
     * @param con             the {@code GeniConnection} to use for this call. Must be a connection to a slice authority.
     * @param sliceCredential the credential for the slice to be shut down
     * @return a boolean (success or failure), wrapped in a {@code SliceAuthority.SliceAuthorityReply}
     * @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 = 40, hint = "API description:\n" +
            "Perform an emergency shutdown on a slice, by asking the SA (for that slice) to do an emergency shutdown. Operationally, the request is forwarded to the ClearingHouse which knows the full set of Component Managers. The call returns once the ClearingHouse is notified; the ClearingHouse will process the request asynchronously.\n" +
            "\n" +
            "Clearinghouse API description:\n" +
            "Perform an emergency shutdown on a slice. This is typically invoked by the Slice Authority for the slice, but may be invoked by anyone with a clearinghouse credential (this needs to change to allow anyone with a valid slice credential). As it stands, anyone with a slice credential can contact the Slice Authority for the slice, and ask it to do the shutown operation.\n" +
            "Since the Clearinghouse must contact each Component Manager to tell it to shutdown the slice, this call will return immediately. There is currently no facility to find out if/when the shutdown has completed.\n")
    public SliceAuthorityReply<Boolean> shutdown(@Nonnull SfaConnection con,
                                                 @ApiMethodParameter(name = "sliceCredential", hint = "The credential of the slice to shut down.")
                                                 @Nonnull AnyCredential sliceCredential) throws JFedException {
        Map<String, Object> methodParams = makeMethodParameters("sliceCredential", sliceCredential);


        Map<String, Object> params = new HashMap<>();
        params.put("credential", sliceCredential.getCredentialXml());

        return executeAndLogXmlRpcCommandGeni(methodParams, con, "shutdown", "Shutdown",
                Collections.singletonList(params), INTEGER_AS_BOOLEAN_REPLY_CONVERTER);
    }

    /**
     * GetKeys call: Get the public SSH key()s of a user.
     *
     * @param con            the {@code GeniConnection} to use for this call. Must be a connection to a slice authority.
     * @param userCredential the credential of the user retrieving ssh keys
     * @return a list of keys, wrapped in a {@code SliceAuthority.SliceAuthorityReply}
     * @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 = 50, hint = "GetKeys call: Get the public SSH key()s of a user.")
    public SliceAuthorityReply<List<String>> getKeys(@Nonnull SfaConnection con,
                                                     @ApiMethodParameter(name = "userCredential", hint = "the credential of the user retrieving ssh keys")
                                                     @Nonnull AnyCredential userCredential) throws JFedException {
        Map<String, Object> methodParams = makeMethodParameters("userCredential", userCredential);


        Map<String, Object> params = new HashMap<>();
        params.put("credential", userCredential.getCredentialXml());

        return executeAndLogXmlRpcCommandGeni(methodParams, con, "getKeys", "GetKeys",
                Collections.singletonList(params), resultValueObject -> {
                    List<Object> keysV = apiSpecifiesListOfT(Object.class, resultValueObject);

                    List<String> keyList = new ArrayList<>();
                    for (Object key : keysV) {
                        if (key instanceof String)
                            keyList.add((String) key);
                        else if (key instanceof Map) {
                            Map t = (Map) key;
                            String type = (String) t.get("type");
                            if (!Objects.equals(type, "ssh"))
                                LOG.warn("Key is Map with non 'ssh' type: " + t);
                            keyList.add((String) t.get("key"));
                        } else
                            throw new BadReplyGeniException("Unexpected type " + key.getClass().getName() + " for user key (val=" + key.toString() + ")");

                    }

                    return keyList;
                });
    }

    /**
     * Remove call: remove a slice.
     * <p/>
     * Note: at the time of writing this documentation, remove was not implemented at the server.
     *
     * @param con            the {@code GeniConnection} to use for this call. Must be a connection to a slice authority.
     * @param userCredential the credential of the user removing a slice.
     * @param slice          the ID of the slice to be removed.
     * @return a boolean (success or failure), wrapped in a {@code SliceAuthority.SliceAuthorityReply}
     * @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 = 60, hint = " Remove call: remove a slice.\n\nNote: at the time of writing this documentation, remove was not implemented at the server.")
    public SliceAuthorityReply<Boolean> remove(@Nonnull SfaConnection con,
                                               @ApiMethodParameter(name = "userCredential", hint = "The credential of the user removing a slice.")
                                               @Nonnull AnyCredential userCredential,
                                               @ApiMethodParameter(name = "slice", hint = "The ID of the slice to be removed (typically the slice URN).")
                                               @Nonnull ResourceId slice) throws JFedException {
        Map<String, Object> methodParams = makeMethodParameters("userCredential", userCredential, "slice", slice);

        Map<String, Object> params = new HashMap<>();
        params.put("type", "Slice");
        params.put("credential", userCredential.getCredentialXml());
        params.put(slice.getType(), slice.getValue());

        return executeAndLogXmlRpcCommandGeni(methodParams, con, "remove", "Remove",
                Collections.singletonList(params), INTEGER_AS_BOOLEAN_REPLY_CONVERTER);
    }

    public static class SliceAuthorityReply<T> implements SfaApiCallReply<T> {
        private final GeniAMResponseCode genicode;
        private final T val;
        /* output is typically set only on error*/
        private final String output;

        private final Map rawResult;
        private final XMLRPCCallDetailsWithCodeValueError xmlrpcCallDetails;

        public SliceAuthorityReply(XMLRPCCallDetailsWithCodeValueError res, T val) {
            this.xmlrpcCallDetails = res;
            this.rawResult = res.getResult();

            int intCode = (Integer) res.getResultCode();
            GeniAMResponseCode genicode = null;
            try {
                genicode = GeniAMResponseCode.getByCode(intCode);
            } catch (UnknownResponseCodeException e) {
                LOG.warn("Did not recognise AM response code", e);
            }
            this.genicode = genicode;
            this.val = val;
            this.output = res.getResultOutput();
        }

        public int getCode() {
            return genicode.getCode();
        }

        public GeniAMResponseCode getGeniResponseCode() {
            return genicode;
        }

        public Map getRawResult() {
            return rawResult;
        }

        public T getValue() {
            return val;
        }

        public String getOutput() {
            return output;
        }

        @Override
        public XMLRPCCallDetailsWithCodeValueError getXMLRPCCallDetailsWithCodeValueError() {
            return xmlrpcCallDetails;
        }

        @Override
        public XMLRPCCallDetails getXMLRPCCallDetails() {
            return xmlrpcCallDetails;
        }


        @Nullable
        public Object getRawValue() {
            if (rawResult == null) return null;
            return rawResult.get("value");
        }
    }


    //TODO: add DiscoverResources  (ignored because same as ClearingHouse ListComponents)
}
