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.preferences.JFedPreferences;
import be.iminds.ilabt.jfed.util.GeniUrn;
import be.iminds.ilabt.jfed.util.KeyUtil;
import be.iminds.ilabt.jfed.util.TextUtil;
import com.google.common.collect.ForwardingMap;
import org.slf4j.LoggerFactory;

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

/**
 * AbstractGeniClearingHouseApi
 */
public abstract class AbstractFederationApi extends AbstractApi {
    private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(AbstractFederationApi.class);
    /*
     * How to handle errors due to malformed replies
     * if true, will try to handle errors as much as possible.
     * if false, will throw errors on malformed replies.
     *
     *  true is useful when creating a client tool that needs to be robust
     *  false is useful when creating a test
     * */
    protected boolean handleMalformedReplies = true;


    protected <T> FederationApiReply<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> FederationApiReply<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();
        FederationApiReply<T> r;
        if (resultValueObject != null && !Objects.equals(resultValueObject, 0)) {
            try {
                r = new FederationApiReply<>(res, replyConverter.convertXMLRPCCall(resultValueObject));
            } catch (AssertionError | Exception t) {
                handleErrorProcessingArguments(res, methodJavaName, methodGeniName, con, t);
                r = null;
            }
        } else {
            r = null;
        }

        if (r == null)
            r = new FederationApiReply<>(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 (!handleMalformedReplies && isCallSuccess(res)) {
            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 boolean isCallSuccess(XMLRPCCallDetailsWithCodeValueError res) {
        Object resultCode = res.getResultCode();

        if (resultCode == null) {
            return false;
        }
        if (!(resultCode instanceof Integer)) {
            return false;
        }
        try {
            GeniResponseCode genicode = GeniUniformFederationApiResponseCode.getByCode((Integer) resultCode);
            return genicode.isSuccess();
        } catch (Exception ignored) {
            return false;
        }

    }

    @Override
    protected boolean isPossibleTemporaryErrorReply(@Nullable be.iminds.ilabt.jfed.lowlevel.XMLRPCCallDetails res) {
        if (res instanceof XMLRPCCallDetailsWithCodeValueError) {
            XMLRPCCallDetailsWithCodeValueError geniDetails = (XMLRPCCallDetailsWithCodeValueError) res;
            if (!(geniDetails.getResultCode() instanceof Map))
                return false;

            Map codeStruct = (Map) geniDetails.getResultCode();

            if (codeStruct == null || !codeStruct.containsKey("geni_code"))
                return false;

            int code = (Integer) codeStruct.get("geni_code");

            if (code == 3 &&
                    Objects.equals(geniDetails.getResultOutput().trim(), "Credential owner does not match speaksfor target")) {
                LOG.warn("Received \"Credential owner does not match speaksfor target\" error. Will handle this as \"possible temporary error\" and try again.");
                return true;
            }
        }
        return false;
    }

    public static class GetVersionResult {
        protected String version;

        public static class CredentialType {
            private final String typeName;
            private final String version;

            public CredentialType(String typeName, String version) {
                this.typeName = typeName;
                this.version = version;
            }

            public String getTypeName() {
                return typeName;
            }

            public String getVersion() {
                return version;
            }
        }

        protected List<CredentialType> supportedCredentialTypes;
        protected Map<String, FieldInfo> fieldInfos;//field name -> multiple attributes (attributes have name and value)

        public static class FieldInfo {
            public enum FieldType {
                URN, URL, UID, STRING, DATETIME, EMAIL, KEY, BOOLEAN, CERTIFICATE, CREDENTIALS, INTEGER,
                PEERS/*List of Hastables containing "url" and "version"*/
            }

            public enum CreationAllowed {REQUIRED, ALLOWED, NOT_ALLOWED}

            public enum Protect {PUBLIC, PRIVATE, IDENTIFYING}

            private Map<String, Object> attributes; //all attributes
            private Map<String, Object> extraAttributes; //only non recognized

            private final String name;
            //known fields with defaults
            private FieldType fieldType;
            private CreationAllowed creationAllowed = CreationAllowed.NOT_ALLOWED;
            private boolean match = true;
            private boolean updateable = false;
            private Protect protect = Protect.PUBLIC; //default
            private String object = null;

            @Override
            public String toString() {
                return "FieldInfo{" +
                        "name=" + name +
                        ", fieldType=" + fieldType +
                        ", creationAllowed=" + creationAllowed +
                        ", match=" + match +
                        ", updateable=" + updateable +
                        ", protect=" + protect +
                        ", object='" + object + '\'' +
                        '}';
            }

            public FieldInfo(String name, FieldType fieldType) {
                this.name = name;
                this.fieldType = fieldType;
                //TODO initialise and fill in attributes and extraAttributes
            }

            public FieldInfo(String object, String name, FieldType fieldType, CreationAllowed creationAllowed, boolean match, boolean updateable) {
                this.name = name;
                this.fieldType = fieldType;
                this.creationAllowed = creationAllowed;
                this.match = match;
                this.updateable = updateable;
                this.object = object;
                //TODO initialise and fill in attributes and extraAttributes
            }

            public FieldInfo(String object, String name, FieldType fieldType, boolean match, Protect protect) {
                this.name = name;
                this.fieldType = fieldType;
                this.match = match;
                this.protect = protect;
                this.object = object;
                //TODO initialise and fill in attributes and extraAttributes
            }

            public FieldInfo(String object, String name, FieldType fieldType, CreationAllowed creationAllowed, boolean match, boolean updateable, Protect protect) {
                this.name = name;
                this.fieldType = fieldType;
                this.creationAllowed = creationAllowed;
                this.match = match;
                this.updateable = updateable;
                this.protect = protect;
                this.object = object;
                //TODO initialise and fill in attributes and extraAttributes
            }

            public FieldInfo(String name, Map<String, Object> attributesTable) throws BadReplyGeniException {
                attributes = new HashMap<>();
                extraAttributes = new HashMap<>();

                this.name = name;
                for (Map.Entry<String, Object> entry : attributesTable.entrySet()) {
                    boolean known = false;

                    assert entry != null;
                    assert entry.getKey() != null;
                    assert entry.getValue() != null;

                    if (Objects.equals(entry.getKey(), "TYPE")) {
                        try {
                            fieldType =
                                    FieldType.valueOf((String) entry.getValue()); //throws exception when unknown value
                        } catch (IllegalArgumentException e) {
                            throw new BadReplyGeniException("field TYPE with value \"" + entry.getValue() + "\" is unknown");
                        }
                        known = true;
                    }

                    if (Objects.equals(entry.getKey(), "CREATE")) {
                        try {
                            creationAllowed =
                                    CreationAllowed.valueOf(((String) entry.getValue()).replace(' ', '_')); //throws exception when unknown value
                        } catch (IllegalArgumentException e) {
                            throw new BadReplyGeniException("field CREATE with value \"" + entry.getValue() + "\" is unknown");
                        }
                        known = true;
                    }

                    if (Objects.equals(entry.getKey(), "UPDATE")) {
                        if (entry.getValue() instanceof String && ((String) entry.getValue()).equalsIgnoreCase("true")) {
                            updateable = true;
                            known = true;
                        }
                        if (entry.getValue() instanceof Boolean) {
                            updateable = (Boolean) entry.getValue();
                            known = true;
                        }
                        if (entry.getValue() instanceof String && ((String) entry.getValue()).equalsIgnoreCase("false")) {
                            updateable = false;
                            known = true;
                        }
                        if (!known)
                            throw new BadReplyGeniException("field UPDATE with value \"" + entry.getValue() + "\" is unknown");
                    }

                    if (Objects.equals(entry.getKey(), "MATCH")) {
                        if (entry.getValue() instanceof String && ((String) entry.getValue()).equalsIgnoreCase("true")) {
                            match = true;
                            known = true;
                        }
                        if (entry.getValue() instanceof Boolean) {
                            match = (Boolean) entry.getValue();
                            known = true;
                        }
                        if (entry.getValue() instanceof String && ((String) entry.getValue()).equalsIgnoreCase("false")) {
                            match = false;
                            known = true;
                        }
                        if (!known)
                            throw new BadReplyGeniException("field MATCH with value \"" + entry.getValue() + "\" is unknown");
                    }

                    if (Objects.equals(entry.getKey(), "PROTECT")) {
                        try {
                            protect = Protect.valueOf((String) entry.getValue()); //throws exception when unknown value
                        } catch (IllegalArgumentException e) {
                            throw new BadReplyGeniException("field PROTECT with value \"" + entry.getValue() + "\" is unknown");
                        }
                        known = true;
                    }

                    if (Objects.equals(entry.getKey(), "OBJECT")) {
                        object = (String) entry.getValue();
                        known = true;
                    }

                    if (!known)
                        extraAttributes.put(entry.getKey(), entry.getValue());
                    attributes.put(entry.getKey(), entry.getValue());
                }


            }

            /**
             * query all attributes, including the known ones (TYPE, CREATE, UPDATE, PROTECT).
             */
            public Object getAttribute(String name) {
                return attributes.get(name);
            }

            /**
             * query only unknown attributes, excluding the known ones (TYPE, CREATE, UPDATE, PROTECT).
             */
            public Object getExtraAttribute(String name) {
                return extraAttributes.get(name);
            }

            public FieldType getType() {
                return fieldType;
            }

            public CreationAllowed getCreationAllowed() {
                return creationAllowed;
            }

            public boolean isUpdateable() {
                return updateable;
            }

            public Protect getProtect() {
                return protect;
            }

            public String getName() {
                return name;
            }

            public boolean isMatchable() {
                return match;
            }

            /**
             * @return null if OBJECT not specified. In that case it is the default for the API
             * (i.e. SLICE for Slice Authority, MEMBER for Member Authority, SERVICE for Clearinghouse)
             */
            public String getObject() {
                return object;
            }
        }

        public GetVersionResult(Object resultValueObject, boolean v2) throws BadReplyGeniException {
            Map<String, Object> versionInfo = apiSpecifiesMapStringToObject(resultValueObject);
            version = apiSpecifiesNonNullString(versionInfo.get("VERSION"));

            if (versionInfo.containsKey("CREDENTIAL_TYPES")) {
                if (!v2) {
                    List<String> strings = apiSpecifiesListOfString(versionInfo.get("CREDENTIAL_TYPES"));
                    supportedCredentialTypes =
                            strings.stream().map(s -> new CredentialType(s, "unknown")).collect(Collectors.toList());
                } else {
                    List<Map> credType =
                            apiSpecifiesListOfT(Map.class, versionInfo.get("CREDENTIAL_TYPES"));

                    supportedCredentialTypes = new ArrayList<>();
                    for (Map ht : credType) {
                        CredentialType credentialType = new CredentialType(
                                apiSpecifiesNonNullString(ht.get("type")),
                                apiSpecifiesNonNullString(ht.get("version"))
                        );
                        supportedCredentialTypes.add(credentialType);
                    }
                }
            }

            fieldInfos = new HashMap<>(); //always initialize, even if FIELDS not specified
            if (versionInfo.containsKey("FIELDS")) {
                Map<String, Object> fields = apiSpecifiesMapStringToObject(versionInfo.get("FIELDS"));
                for (Map.Entry<String, Object> entry : fields.entrySet()) {
                    FieldInfo fieldInfo =
                            new FieldInfo(entry.getKey(), apiSpecifiesMapStringToObject(entry.getValue()));
                    fieldInfos.put(entry.getKey(), fieldInfo);
                }
            }
        }

        public String getVersion() {
            return version;
        }

        public List<CredentialType> getSupportedCredentialTypes() {
            return supportedCredentialTypes;
        }

        public Map<String, FieldInfo> getFields() {
            return fieldInfos;
        }

        public Map<String, FieldInfo> getFieldsForObject(String objectName) {
            if (fieldInfos == null) return null;
            Map<String, FieldInfo> objectFields = new HashMap<>();
            for (Map.Entry<String, FieldInfo> e : fieldInfos.entrySet()) {
                String fieldName = e.getKey();
                FieldInfo fieldInfo = e.getValue();
                if (fieldInfo.getObject() != null && objectName != null && Objects.equals(fieldInfo.getObject(), objectName))
                    objectFields.put(fieldName, fieldInfo);
            }
            return objectFields;
        }

        @Override
        public String toString() {
            return "GetVersionResult{" +
                    "version='" + version + '\'' +
                    ", supportedCredentialTypes=" + supportedCredentialTypes +
                    ", fieldInfos=" + fieldInfos +
                    '}';
        }
    }

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

        private final Map rawResult;

        private final XMLRPCCallDetailsWithCodeValueError xmlRpcCallDetailsWithCodeValueError;

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

        @Nonnull
        public GeniUniformFederationApiResponseCode getGeniResponseCode() {
            return genicode;
        }

        public T getValue() {
            return val;
        }

        /**
         * can be null
         */
        public String getOutput() {
            return output;
        }

        public XMLRPCCallDetailsWithCodeValueError getXmlRpcCallDetailsWithCodeValueError() {
            return xmlRpcCallDetailsWithCodeValueError;
        }

        public static boolean isSuccess(XMLRPCCallDetails res) {
            if (res instanceof XMLRPCCallDetailsWithCodeValueError) {
                XMLRPCCallDetailsWithCodeValueError geniDetails = (XMLRPCCallDetailsWithCodeValueError) res;
                if (!(geniDetails.getResultCode() instanceof Integer))
                    return false;
                int code = (Integer) geniDetails.getResultCode();
                GeniResponseCode genicode;
                try {
                    genicode = GeniUniformFederationApiResponseCode.getByCode(code);
                } catch (UnknownResponseCodeException e) {
                    LOG.error("UnknownResponseCodeException: \"" + e.getMessage() + "\" -> will be treated as unsuccessful", e);
                    genicode = GeniUniformFederationApiResponseCode.SERVER_REPLY_ERROR;
                }
                return genicode.isSuccess();
            }
            return false; //not a geni reply, so not successful
        }

        public FederationApiReply(XMLRPCCallDetailsWithCodeValueError res, T val) {
            this.xmlRpcCallDetailsWithCodeValueError = res;
            this.rawResult = res.getResult();

            this.val = val;
            Object codeObject = res.getResultCode();
            int code;
            if (codeObject != null) {
                if (codeObject instanceof Integer) {
                    code = (Integer) codeObject;
                } else {
                    LOG.error("Reply parse error: codeObject is not Integer but " + codeObject.getClass().getName());
                    code = GeniUniformFederationApiResponseCode.SERVER_REPLY_ERROR.getCode();
                }
            } else {
                LOG.error("Reply parse error: codeObject=null");
                code = GeniUniformFederationApiResponseCode.SERVER_REPLY_ERROR.getCode();
            }
            GeniUniformFederationApiResponseCode c;
            try {
                c = GeniUniformFederationApiResponseCode.getByCode(code);
            } catch (UnknownResponseCodeException e) {
                LOG.error("UnknowResponseCodeException: \"" + e.getMessage() + "\"  codeObject=" + codeObject, e);
                c = GeniUniformFederationApiResponseCode.SERVER_REPLY_ERROR;
            }
            this.genicode = c;

            String o = null;
            Map r = res.getResult();
            if (r != null && r.get("output") != null) {
                o = r.get("output").toString();
                //any empty structure is interpreted as an empty string
                if (r.get("output") instanceof Map && ((Map) r.get("output")).isEmpty())
                    o = "";
                if (r.get("output") instanceof Object[] && ((Object[]) r.get("output")).length == 0)
                    o = "";
            }
            this.output = o; //this should be a string, but we allow more. This may be null on success.
        }

        public Map getRawResult() {
            return rawResult;
        }

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

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


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

    public static boolean isBusyApiReply(@Nullable be.iminds.ilabt.jfed.lowlevel.XMLRPCCallDetails res) {
        if (res == null) return false;
        if (res instanceof XMLRPCCallDetailsWithCodeValueError) {
            XMLRPCCallDetailsWithCodeValueError geniDetails = (XMLRPCCallDetailsWithCodeValueError) res;

            if (geniDetails.getResultCode() == null)
                return false;

            int code = GeniUniformFederationApiResponseCode.NONE.getCode();
            if (geniDetails.getResultCode() instanceof Map) {
                Map codeStruct = (Map) geniDetails.getResultCode();
                code = (Integer) codeStruct.get("geni_code");
            }
            if (geniDetails.getResultCode() instanceof Integer) {
                code = (Integer) geniDetails.getResultCode();
            }


            try {
                return GeniUniformFederationApiResponseCode.getByCode(code).isBusy();
            } catch (UnknownResponseCodeException e) {
                LOG.warn("Unknown response code {} encountered while checking for busy", code, e);
            }
        }
        return false;
    }

    @Override
    protected boolean isBusyReply(@Nullable be.iminds.ilabt.jfed.lowlevel.XMLRPCCallDetails res) {
        return isBusyApiReply(res);
    }


    public AbstractFederationApi(Logger logger, RetrySettings retrySettings, ApiInfo.Api serverType, JFedPreferences jFedPreferences) {
        super(logger, retrySettings, serverType, jFedPreferences);
    }


    public static List<AnyCredential> apiSpecifiesListOfCredentials(String credentialBaseName, Object o) throws BadReplyGeniException {
        List<Map> vectOfDicts = AbstractApi.apiSpecifiesListOfT(Map.class, o);

        int i = 0;

        List<AnyCredential> res = new ArrayList<>();
        for (Map ht : vectOfDicts) {

            String credName = credentialBaseName;
            if (vectOfDicts.size() > 1)
                credName += " " + i;

            AnyCredential cred;
            try {
                cred = AnyCredential.createFromV3Map(credName, ht);
            } catch (CredentialException e) {
                throw new BadReplyGeniException("Response contains an invalid credential", e);
            }

            //assert below is useful test during debugging, but off course not to be assumed in all cases!
//            assert cred instanceof SfaCredential : "credential is not SfaCredential but "+cred.getClass().getName()+" ht="+ht;

            res.add(cred);

            i++;
        }

        return res;
    }


    public static class HTLookupResult extends LookupResult<Map<String, Object>> {

        public HTLookupResult(@Nonnull Object res) throws BadReplyGeniException {
            super(res, (o) -> o);
        }

        public HTLookupResult(@Nonnull Map<String, Object> res) throws BadReplyGeniException {
            super(res, (o) -> o);
        }

        public Map<String, Object> get(GeniUrn urn) {
            return get(urn.getValue());
        }

        public boolean containsKey(GeniUrn urn) {
            return containsKey(urn.getValue());
        }
    }

    /**
     * The result of a lookup operation.
     * <p>
     * This is indexed in 2 ways:
     * - by urn (or ID) (String or GeniUrn)
     * - by index nr
     * <p>
     * Note: some lookups (for KEY for example) will not use URNs! Be aware of that.
     * <p>
     * The objects returned are accessable as either:
     * - Map<String, Object>
     * - an object of the class "T"
     */
    public static class LookupResult<T> extends ForwardingMap<GeniUrn, T> {

        //        private final Map<String, Map<String, Object>> rawMap = new HashMap<>();
        private final Map<GeniUrn, T> map = new HashMap<>();

        public LookupResult(@Nonnull Object res, @Nonnull LookupResultConverter<T> converter) throws BadReplyGeniException {
            Map<String, Object> ht = apiSpecifiesMapStringToObject(res);

            for (Entry<String, Object> entry : ht.entrySet()) {
                GeniUrn key;
                try {
                    key = new GeniUrn(entry.getKey());
                } catch (GeniUrn.GeniUrnParseException e) {
                    throw new BadReplyGeniException("Expecting result to lookup to be a mapping of URNs to Dicts. However, key '" + entry.getKey() + "' is not a valid URN", e);
                }

                if (entry.getValue() == null) {
                    throw new BadReplyGeniException("expecting result to lookup to be a mapping of URN to Dictionaries of field/value pairs. However, instead of a dictionary, a null was found for key \"" + entry.getKey() + "\"!");
                }
                if (entry.getValue() instanceof Object[]) {
                    throw new BadReplyGeniException("expecting result to lookup to be a mapping of URN to Dictionaries of field/value pairs. However, instead of a dictionary, a List was found for key \"" + entry.getKey() + "\"!");
                }
                if (!(entry.getValue() instanceof Map)) {
                    throw new BadReplyGeniException("expecting result to lookup to be a mapping of URN to Dictionaries of field/value pairs. However, instead of a dictionary, a " + (entry.getValue().getClass().getName()) + " was found for key \"" + entry.getKey() + "\"!");
                }

                final Map<String, Object> rawValue = apiSpecifiesMapStringToObject(entry.getValue());
                map.put(key, converter.convert(rawValue));
            }
        }

        @Override
        public String toString() {
            return "ConvertibleLookupResult{" + map + '}';
        }

        @Override
        protected Map<GeniUrn, T> delegate() {
            return map;
        }
    }

    @FunctionalInterface
    public interface LookupResultConverter<T> {
        T convert(Map<String, Object> ht) throws BadReplyGeniException;
    }


    @Nonnull
    public abstract List<GetVersionResult.FieldInfo> getMinimumFields(String objectName);

    @Nonnull
    public List<String> getMinimumFieldNames(@Nonnull String objectName) {
        return getMinimumFields(objectName).stream().map(GetVersionResult.FieldInfo::getName).collect(Collectors.toList());

    }

    @Nonnull
    public Map<String, String> getMinimumFieldsMap(@Nonnull String objectName) {
        return getMinimumFieldNames(objectName).stream().collect(Collectors.toMap(key -> key, key -> key + " value"));
    }

    public abstract List<String> getApiObjects();

    public abstract List<String> getOptionalApiServices();

    public abstract List<String> getRequiredApiServices();


    public void setHandleMalformedReplies(boolean handleErrors) {
        this.handleMalformedReplies = handleErrors;
    }

    public static class SliceInfo {
        private static final String SLICE_UID = "SLICE_UID";
        private static final String SLICE_CREATION = "SLICE_CREATION";
        private static final String SLICE_EXPIRATION = "SLICE_EXPIRATION";
        private static final String EMULAB_SLICE_HAS_SLIVERS = "_EMULAB_SLICE_HAS_SLIVERS";
        private static final String SLICE_URN = "SLICE_URN";

        @Nonnull
        private final GeniUrn sliceUrn;
        private final Date expirationDate;
        private final Date creationDate;
        private final Boolean emulabHasSlivers;
        private final String uuid;

        public SliceInfo(Object resultValueObject) throws BadReplyGeniException {
            this(apiSpecifiesMapStringToObject(resultValueObject));
        }

        public SliceInfo(Map<String, Object> raw) throws BadReplyGeniException {
            this.sliceUrn = apiSpecifiesGeniUrnInMap(raw, SLICE_URN);
            this.expirationDate = apiSpecifiesDateInMap(raw, SLICE_EXPIRATION);

            if (raw.containsKey(SLICE_CREATION))
                this.creationDate = apiSpecifiesDateInMap(raw, SLICE_CREATION);
            else
                this.creationDate = null;

            this.emulabHasSlivers = TextUtil.objectToBoolean(raw.get(EMULAB_SLICE_HAS_SLIVERS));
            this.uuid = apiSpecifiesNullableString(raw.get(SLICE_UID));
        }


        @Nonnull
        public GeniUrn getSliceUrn() {
            return sliceUrn;
        }

        public Date getExpirationDate() {
            return expirationDate;
        }

        public Boolean getEmulabHasSlivers() {
            return emulabHasSlivers;
        }

        public Date getCreationDate() {
            return creationDate;
        }

        public String getUuid() {
            return uuid;
        }

        @Override
        public String toString() {
            return "SliceInfo{" +
                    "sliceUrn=" + sliceUrn +
                    ", expirationDate=" + expirationDate +
                    ", creationDate=" + creationDate +
                    ", emulabHasSlivers=" + emulabHasSlivers +
                    ", uuid='" + uuid + '\'' +
                    '}';
        }
    }

    public static class SliceInfoList extends LookupResult<SliceInfo> {

        public SliceInfoList(@Nonnull Object resultValueObject) throws BadReplyGeniException {
            super(resultValueObject, SliceInfo::new);
        }
    }

    public static class SliverInfo {
        public static final String SLIVER_INFO_SLICE_URN = "SLIVER_INFO_SLICE_URN";
        /*
        example:

        "urn:publicid:IDN+wall2.ilabt.iminds.be+sliver+43869": {
                                 "SLIVER_INFO_AGGREGATE_URN": "urn:publicid:IDN+wall2.ilabt.iminds.be+authority+cm",
                                 "SLIVER_INFO_SLICE_URN": "urn:publicid:IDN+ch.geni.net:fed4firetest3+slice+tstW",
                                 "SLIVER_INFO_EXPIRATION": "2015-01-12T14:10:30Z",
                                 "SLIVER_INFO_CREATOR_URN": "urn:publicid:IDN+ch.geni.net+user+wimvande",
                                 "SLIVER_INFO_CREATION": "",
                                 "SLIVER_INFO_URN": "urn:publicid:IDN+wall2.ilabt.iminds.be+sliver+43869"
                                 }
                          }
         */

        private final GeniUrn sliverUrn;
        private final GeniUrn aggregateUrn;
        private final GeniUrn sliceUrn;
        private final Date expirationDate;
        private final GeniUrn creatorUrn;
        private final String creation;

        public SliverInfo(Object resultValueObject) throws BadReplyGeniException {
            this(apiSpecifiesMapStringToObject(resultValueObject));
        }

        public SliverInfo(Map<String, Object> raw) throws BadReplyGeniException {
            this.sliverUrn = apiSpecifiesGeniUrnInMap(raw, "SLIVER_INFO_URN");
            this.aggregateUrn = apiSpecifiesGeniUrnInMap(raw, "SLIVER_INFO_AGGREGATE_URN");
            this.sliceUrn = apiSpecifiesGeniUrnInMap(raw, SLIVER_INFO_SLICE_URN);
            this.expirationDate = apiSpecifiesDateInMap(raw, "SLIVER_INFO_EXPIRATION");
            this.creatorUrn = apiSpecifiesGeniUrnInMap(raw, "SLIVER_INFO_CREATOR_URN");
            this.creation = apiSpecifiesNonNullString(raw.get("SLIVER_INFO_CREATION"));
        }

        public GeniUrn getAggregateUrn() {
            return aggregateUrn;
        }

        public GeniUrn getSliceUrn() {
            return sliceUrn;
        }

        public Date getExpirationDate() {
            return expirationDate;
        }

        public GeniUrn getCreatorUrn() {
            return creatorUrn;
        }

        public String getCreation() {
            return creation;
        }

        public GeniUrn getSliverUrn() {
            return sliverUrn;
        }

        @Override
        public String toString() {
            return "SliverInfo{" +
                    "sliverUrn=" + sliverUrn +
                    ", aggregateUrn=" + aggregateUrn +
                    ", sliceUrn=" + sliceUrn +
                    ", expirationDate=" + expirationDate +
                    ", creatorUrn=" + creatorUrn +
                    ", creation='" + creation + '\'' +
                    '}';
        }
    }

    public static class SliverInfoList extends LookupResult<SliverInfo> {

        public SliverInfoList(@Nonnull Object res) throws BadReplyGeniException {
            super(res, SliverInfo::new);
        }
    }

    public static class ProjectInfo {

        private final GeniUrn projectUrn;
        private final String projectUid;
        private final Date creationDate;
        private final Date expirationDate;
        private final Boolean expired;
        private final String name;
        private final String description;

        public ProjectInfo(@Nonnull Object resultObjectValue) throws BadReplyGeniException {
            this(apiSpecifiesMapStringToObject(resultObjectValue));
        }

        public ProjectInfo(@Nonnull Map<String, Object> raw) throws BadReplyGeniException {
            this.projectUrn = apiSpecifiesGeniUrnInMap(raw, "PROJECT_URN");
            this.projectUid = apiSpecifiesStringInMap(raw, "PROJECT_UID");
            this.creationDate = apiSpecifiesDateInMap(raw, "PROJECT_CREATION");
            this.expirationDate = apiSpecifiesDateInMap(raw, "PROJECT_EXPIRATION");
            this.expired = TextUtil.objectToBoolean(raw.get("PROJECT_EXPIRED"));
            this.name = apiSpecifiesStringInMap(raw, "PROJECT_NAME");
            this.description = apiSpecifiesStringInMap(raw, "PROJECT_DESCRIPTION");
        }

        /**
         * @return URN of given project
         */
        public GeniUrn getProjectUrn() {
            return projectUrn;
        }

        /**
         * @return UID (unique within authority) of project
         */
        public String getProjectUid() {
            return projectUid;
        }

        /**
         * @return Creation time of project
         */
        public Date getCreationDate() {
            return creationDate;
        }

        /**
         * @return Expiration time of project
         */
        public Date getExpirationDate() {
            return expirationDate;
        }

        /**
         * @return Whether project has expired
         */
        public Boolean getExpired() {
            return expired;
        }

        /**
         * @return Short name of Project
         */
        public String getName() {
            return name;
        }

        /**
         * @return Description of Project
         */
        public String getDescription() {
            return description;
        }

        @Override
        public String toString() {
            return "ProjectInfo{" +
                    "projectUrn=" + projectUrn +
                    ", projectUid='" + projectUid + '\'' +
                    ", creationDate=" + creationDate +
                    ", expirationDate=" + expirationDate +
                    ", expired=" + expired +
                    ", name='" + name + '\'' +
                    ", description='" + description + '\'' +
                    '}';
        }
    }

    public static class ProjectInfoList extends LookupResult<ProjectInfo> {
        public ProjectInfoList(@Nonnull Object res) throws BadReplyGeniException {
            super(res, ProjectInfo::new);
        }
    }

    public static class MemberInfo {

        private final GeniUrn memberUrn;
        private final String memberUid;
        private final String firstname;
        private final String lastname;
        private final String username;
        private final String email;

        private final List<X509Certificate> x509pemCerts;

        public MemberInfo(Map<String, Object> raw) throws BadReplyGeniException {
            this.memberUrn = apiSpecifiesGeniUrnInMap(raw, "MEMBER_URN");
            this.memberUid = apiSpecifiesStringInMap(raw, "MEMBER_UID");
            if (raw.containsKey("MEMBER_FIRSTNAME"))
                this.firstname = apiSpecifiesStringInMap(raw, "MEMBER_FIRSTNAME");
            else
                this.firstname = null;

            if (raw.containsKey("MEMBER_LASTNAME"))
                this.lastname = apiSpecifiesStringInMap(raw, "MEMBER_LASTNAME");
            else
                this.lastname = null;
            this.username = apiSpecifiesStringInMap(raw, "MEMBER_USERNAME");
            this.email = apiSpecifiesStringInMap(raw, "MEMBER_EMAIL");

            String x509pemCertsString = null;
            if (raw.get("_GENI_MEMBER_SSL_CERTIFICATE") != null) {
                x509pemCertsString = (String) raw.get("_GENI_MEMBER_SSL_CERTIFICATE");
            } else if (raw.get("_EMULAB_MEMBER_SSL_CERTIFICATE") != null) {
                x509pemCertsString = (String) raw.get("_EMULAB_MEMBER_SSL_CERTIFICATE");
            }

            if (x509pemCertsString != null) {
                x509pemCerts = KeyUtil.pemToX509CertificateChain(x509pemCertsString);
                if (x509pemCerts == null) {
                    throw new BadReplyGeniException("Received an invalid x509 certificate chain");
                }
            } else
                x509pemCerts = null;

        }

        public GeniUrn getMemberUrn() {
            return memberUrn;
        }

        public String getMemberUid() {
            return memberUid;
        }

        public String getFirstname() {
            return firstname;
        }

        public String getLastname() {
            return lastname;
        }

        public String getUsername() {
            return username;
        }

        public String getEmail() {
            return email;
        }


        public List<X509Certificate> getX509pemCerts() {
            return x509pemCerts;
        }

        @Override
        public String toString() {
            return "MemberInfo{" +
                    "memberUrn=" + memberUrn +
                    ", memberUid='" + memberUid + '\'' +
                    ", firstname='" + firstname + '\'' +
                    ", lastname='" + lastname + '\'' +
                    ", username='" + username + '\'' +
                    ", email='" + email + '\'' +
                    ", x509pemCerts=" + x509pemCerts +
                    '}';
        }
    }

    public static class MemberInfoList extends LookupResult<MemberInfo> {

        public MemberInfoList(@Nonnull Object res) throws BadReplyGeniException {
            super(res, MemberInfo::new);
        }
    }

    public static class MemberKeyInfo {
        private final String keyId;
        private final GeniUrn keyMember;
        private final String publicKey;
        private final String privateKey;
        private final String keyDescription;

        public MemberKeyInfo(@Nonnull Object resultObjectValue) throws BadReplyGeniException {
            this(apiSpecifiesMapStringToObject(resultObjectValue));
        }

        public MemberKeyInfo(Map<String, Object> raw) throws BadReplyGeniException {

            this.keyId = apiSpecifiesStringInMap(raw, "KEY_ID");
            this.keyMember = apiSpecifiesGeniUrnInMap(raw, "KEY_MEMBER");
            this.publicKey = apiSpecifiesStringInMap(raw, "KEY_PUBLIC");
            if (raw.containsKey("KEY_PRIVATE"))
                this.privateKey = apiSpecifiesStringInMap(raw, "KEY_PRIVATE");
            else
                this.privateKey = null;
            this.keyDescription = apiSpecifiesStringInMap(raw, "KEY_DESCRIPTION");
        }

        public String getKeyId() {
            return keyId;
        }

        public String getPublicKey() {
            return publicKey;
        }

        public String getPrivateKey() {
            return privateKey;
        }

        /**
         * @return Unique identifier of key: typically a fingerprint or hash of public key
         */
        public String getKeyDescription() {
            return keyDescription;
        }

        /**
         * @return URN of member associated with key pair
         */
        public GeniUrn getKeyMember() {
            return keyMember;
        }

        @Override
        public String toString() {
            return "MemberKeyInfo{" +
                    "keyId='" + keyId + '\'' +
                    ", keyMember=" + keyMember +
                    ", publicKey='" + publicKey + '\'' +
                    ", privateKey='" + privateKey + '\'' +
                    ", keyDescription='" + keyDescription + '\'' +
                    '}';
        }
    }

    public static class MemberKeyInfoList extends LookupResult<MemberKeyInfo> {

        public MemberKeyInfoList(@Nonnull Object res) throws BadReplyGeniException {
            super(res, MemberKeyInfo::new);
        }
    }
}
