package be.iminds.ilabt.jfed.lowlevel.api_wrapper.impl;

import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Server;
import be.iminds.ilabt.jfed.log.Logger;
import be.iminds.ilabt.jfed.lowlevel.*;
import be.iminds.ilabt.jfed.lowlevel.api.AbstractFederationApi;
import be.iminds.ilabt.jfed.lowlevel.api.AbstractFederationApi2;
import be.iminds.ilabt.jfed.lowlevel.api.FederationMemberAuthorityApi2;
import be.iminds.ilabt.jfed.lowlevel.api.FederationSliceAuthorityApi2;
import be.iminds.ilabt.jfed.lowlevel.api_wrapper.UserAndSliceApiWrapper;
import be.iminds.ilabt.jfed.lowlevel.connection.JFedConnectionProvider;
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.RFC3339Util;
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;

/**
 * ProtoGeniSAUserAndSliceApiWrapper
 */
public class UniformFederationApi2UserAndSliceApiWrapper extends UserAndSliceApiWrapper {
    private static final org.slf4j.Logger LOG =
            LoggerFactory.getLogger(UniformFederationApi2UserAndSliceApiWrapper.class);
    List<AnyCredential> userCredentials;

    public UniformFederationApi2UserAndSliceApiWrapper(Logger logger, GeniUserProvider geniUserProvider, JFedConnectionProvider connectionProvider, JFedPreferences jFedPreferences) {
        super(logger, geniUserProvider, connectionProvider, jFedPreferences);
    }

    private FederationMemberAuthorityApi2 ma(Logger logger) {
        return new FederationMemberAuthorityApi2(logger, jFedPreferences);
    }

    private FederationSliceAuthorityApi2 sa(Logger logger) {
        return new FederationSliceAuthorityApi2(logger, jFedPreferences);
    }

    private SfaConnection getSaConnection() throws JFedException {
        return getConnection(new ApiInfo.Api(ApiInfo.ApiName.GENI_CH_SA, 2));
    }

    private SfaConnection getMaConnection() throws JFedException {
        return getConnection(new ApiInfo.Api(ApiInfo.ApiName.GENI_CH_MA, 2));
    }

    @Nonnull
    public FederationSliceAuthorityApi2.GetVersionSAResult getGetVersionSAResult(Logger logger) throws JFedException {
        //TODO: buffer

        AbstractFederationApi2.FederationApiReply<FederationSliceAuthorityApi2.GetVersionSAResult> reply =
                sa(logger).getVersion(getSaConnection());

        if (!reply.getGeniResponseCode().isSuccess())
            throw new JFedException("GetVersion call to SA not successful: code=" +
                    reply.getGeniResponseCode() + " output=" + reply.getOutput(),
                    reply
            );

        return reply.getValue();
    }

    @Override
    public synchronized List<AnyCredential> getUserCredentials(@Nonnull Logger logger, @Nonnull GeniUrn user) throws JFedException {
        userCredentials = null;

        List<AnyCredential> reqCredCreds = new ArrayList<>();

        if (speaksForCredentials != null && !speaksForCredentials.isEmpty()) {
            reqCredCreds.addAll(speaksForCredentials);
            assert speaksForUserUrn != null;
            user = speaksForUserUrn;
        }

        AbstractFederationApi2.FederationApiReply<List<AnyCredential>> reply =
                ma(logger).getCredentials(getMaConnection(), reqCredCreds,
                        user, addCredentialExtraOptions(null));

        if (!reply.getGeniResponseCode().isSuccess())
            throw new JFedException("Could not retrieve user credential from server");

        userCredentials = new ArrayList<>();
        if (speaksForCredentials != null && !speaksForCredentials.isEmpty())
            userCredentials.addAll(speaksForCredentials);
        userCredentials.addAll(reply.getValue());

        return new ArrayList<>(userCredentials);
    }

    @Override
    public boolean hasUserCredentials() {
        return hasUserCredentials(userCredentials);
    }

    @Override
    public boolean hasSpeaksForSupport() {
        return true;
    }

    @Override
    public List<AnyCredential> getCachedUserCredentialsForAM() {
        if (userCredentials == null)
            return Collections.emptyList();
        else
            return new ArrayList<>(userCredentials);
    }

    @Override
    public List<AnyCredential> getSliceCredentials(@Nonnull Logger logger, @Nonnull GeniUrn sliceUrn) throws JFedException {
        assert userCredentials != null && !userCredentials.isEmpty();

        AbstractFederationApi2.FederationApiReply<List<AnyCredential>> reply =
                sa(logger).getSliceCredentials(getSaConnection(), addSpeaksForCredentials(userCredentials),
                        sliceUrn, addCredentialExtraOptions(null));

        if (!reply.getGeniResponseCode().isSuccess())
            throw new JFedException("Could not retrieve slice credential from server", reply);

        List<AnyCredential> sliceCredentials = new ArrayList<>();
        if (speaksForCredentials != null && !speaksForCredentials.isEmpty())
            sliceCredentials.addAll(speaksForCredentials);
        sliceCredentials.addAll(reply.getValue());
        return sliceCredentials;
    }

    @Override
    public List<GeniUrn> getSlicesForUser(@Nonnull Logger logger, @Nonnull GeniUrn user) throws JFedException {
        assert getLoggedInUserAuthorityServer() != null;
        boolean serverSupportsLookupForMemberMatch =
                getLoggedInUserAuthorityServer().hasFlag(Server.Flag.featureFedSaLookupSliceForMemberMatchExpired);

        //match is officially not supported in the API for this call, but it works on the geni portal and is convenient.
        Map<String, Object> extraOptions = null;
        if (serverSupportsLookupForMemberMatch) {
            Map<String, Boolean> match = new HashMap<>();
            match.put("SLICE_EXPIRED", false);

            extraOptions = new HashMap<>();
            extraOptions.put("match", match);
        }

        AbstractFederationApi2.FederationApiReply<List<FederationSliceAuthorityApi2.UrnRoleTuple>> reply =
                sa(logger).lookupForMember(getSaConnection(), "SLICE", user, addSpeaksForCredentials(userCredentials), addCredentialExtraOptions(extraOptions));
        if (!reply.getGeniResponseCode().isSuccess())
            throw new JFedException("Could not retrieve slices for user from server", reply);

        List<GeniUrn> res = new ArrayList<>();
        List<String> resStrings = new ArrayList<>();

        List<FederationSliceAuthorityApi2.UrnRoleTuple> urnRoleTuples = reply.getValue();
        boolean sawExpiredSlice = false;
        for (FederationSliceAuthorityApi2.UrnRoleTuple urnRoleTuple : urnRoleTuples) {
            if (!urnRoleTuple.isExpired()) {
                res.add(urnRoleTuple.getUrn());
                resStrings.add(urnRoleTuple.getUrn().toString());
            } else
                sawExpiredSlice = true;
        }

        //since it takes only 1 extra call, we'll slo get all other info about the slices (such as expire date)
        if (!resStrings.isEmpty()) {
            Map<String, Object> match2 = new HashMap<>();
            match2.put("SLICE_URN", resStrings);

            if (serverSupportsLookupForMemberMatch || sawExpiredSlice) {
                if (getLoggedInUserAuthorityServer().hasFlag(Server.Flag.workaroundFedSaLookupSliceExpiredMatchBoolean)) {
                    match2.put("SLICE_EXPIRED", "false");
                } else
                    match2.put("SLICE_EXPIRED", false);
            }

            boolean hasSliverActiveInfo = false;
            try {
                AbstractFederationApi.FederationApiReply<AbstractFederationApi.SliceInfoList> sliceInfoLookupRes =
                        sa(logger).lookupSlice(getSaConnection(), addSpeaksForCredentials(userCredentials), match2,
                                null, addCredentialExtraOptions(null));

                if (sliceInfoLookupRes.getGeniResponseCode().isSuccess()) {
                    hasSliverActiveInfo =
                            sliceInfoLookupRes.getValue().values().stream()
                                    .anyMatch(sliceInfo -> Objects.equals(sliceInfo.getEmulabHasSlivers(), Boolean.TRUE));
                } else {
                    LOG.warn("The sliceInfo lookup for expired credentials failed. We cannot assure that we don't return expired credentials");
                }
            } catch (JFedException ex) {
                LOG.error("Error fetching SLICE info. Will be ignored, because it's not crucial.");
            }

            if (!hasSliverActiveInfo) {
                try {
                    Map<String, Object> match3 =
                            Collections.singletonMap("SLIVER_INFO_SLICE_URN", resStrings);
                    AbstractFederationApi.FederationApiReply<AbstractFederationApi.SliverInfoList> sliverInfoLookupRes =
                            sa(logger).lookupSliverInfo(getSaConnection(), addSpeaksForCredentials(userCredentials), null, match3, null, addCredentialExtraOptions(null));
                } catch (JFedException e) {
                    LOG.error("Error fetching SLIVER_INFO. Will be ignored, because it's not crucial.");
                }
            }
        } else {
            LOG.debug("Not requesting extra SLICE info, because there are no non expired slices anyway.");
        }

        return res;
    }

    public List<GeniUrn> getUserProjects(Logger logger, GeniUrn user) throws JFedException {


        AbstractFederationApi2.FederationApiReply<List<FederationSliceAuthorityApi2.UrnRoleTuple>> reply =
                sa(logger).lookupForMember(getSaConnection(),
                        "PROJECT",
                        user,
                        addSpeaksForCredentials(userCredentials),
                        addCredentialExtraOptions(null));
        if (!reply.getGeniResponseCode().isSuccess())
            throw new JFedException("Could not retrieve projects for user from server", reply);

        List<GeniUrn> res = new ArrayList<>();

        List<FederationSliceAuthorityApi2.UrnRoleTuple> urnRoleTuples = reply.getValue();
        boolean hasExpired = false;
        for (FederationSliceAuthorityApi2.UrnRoleTuple urnRoleTuple : urnRoleTuples) {

            //first a special exception: server might include EXPIRED field in tuple. We can use that info
            if (urnRoleTuple.isExpired()) {
                hasExpired = true;
                LOG.debug("skipping expired project: " + urnRoleTuple.getUrn());
                continue;

            }

            res.add(urnRoleTuple.getUrn());
        }

        if (!hasExpired) {
            LOG.debug("doing second call to check for expired projects");

            Map<String, Object> match;
            assert getLoggedInUserAuthorityServer() != null;
            if (getLoggedInUserAuthorityServer().hasFlag(Server.Flag.workaroundFedMaMatchProjectExpired))
                match = Collections.singletonMap("EXPIRED", Boolean.FALSE);
            else
                match = Collections.emptyMap();

            List<String> filter = Collections.singletonList("PROJECT_URN");
            try {
                AbstractFederationApi2.FederationApiReply<AbstractFederationApi.ProjectInfoList> nonExpiredProj =
                        sa(logger).lookupProject(getSaConnection(),
                                addSpeaksForCredentials(userCredentials),
                                match,
                                filter,
                                addCredentialExtraOptions(null));

                if (nonExpiredProj.getGeniResponseCode().isSuccess() && nonExpiredProj.getValue() != null) {
                    LOG.debug("Found " + nonExpiredProj.getValue().size() + " non expired projects on server.");
                    res.removeIf(projUrn -> !nonExpiredProj.getValue().containsKey(projUrn));
                } else {
                    LOG.warn("lookup_projects call failed. Could not check for expired projects. This error will be ignored (possibly leaving expired projects in the list).");
                }
            } catch (AssertionError | Exception e) {
                LOG.warn("lookup_projects call failed. Could not check for expired projects. This error will be ignored (possibly leaving expired projects in the list).", e);
            }
        } else {
            LOG.debug("no second call needed to check for expired projects");
        }
        return res;
    }

    public AbstractFederationApi.MemberInfoList getUserIdentifyingInfo(Logger logger, GeniUrn user) throws JFedException {
        Map<String, String> match = Collections.singletonMap("MEMBER_URN", user.getValue());


        AbstractFederationApi.FederationApiReply<AbstractFederationApi.MemberInfoList> reply =
                ma(logger).lookupMember(getMaConnection(), addSpeaksForCredentials(userCredentials), match, null,
                        addCredentialExtraOptions(null));

        if (!reply.getGeniResponseCode().isSuccess())
            throw new JFedException("Problem looking up user info for " + user, reply);

        return reply.getValue();
    }

    @Override
    public List<GeniUrn> getAggregatesForSlice(@Nonnull Logger logger, @Nonnull List<AnyCredential> sliceCredentials, @Nonnull GeniUrn sliceUrn) throws JFedException {
        AbstractFederationApi.FederationApiReply<AbstractFederationApi.SliverInfoList>
                reply =
                sa(logger).lookupSliverInfo(getSaConnection(), finalizeSliceCredentials(sliceCredentials),
                        sliceUrn, null, null, addCredentialExtraOptions(null));

        if (!reply.getGeniResponseCode().isSuccess())
            throw new JFedException("Problem looking up aggregates for slice '" + sliceUrn + "'", reply);

        return reply.getValue().values().stream()
                .map(AbstractFederationApi.SliverInfo::getAggregateUrn)
                .distinct()
                .collect(Collectors.toList());

    }

    @Nullable
    @Override
    public Date getSliceExpiration(@Nonnull Logger logger, @Nonnull List<AnyCredential> sliceCredentials, @Nonnull GeniUrn sliceUrn) throws JFedException {
        Map<String, String> match = Collections.singletonMap("SLICE_URN", sliceUrn.getValue());
        AbstractFederationApi.FederationApiReply<AbstractFederationApi.SliceInfoList> reply =
                sa(logger).lookupSlice(getSaConnection(), finalizeSliceCredentials(sliceCredentials),
                        match, null, null);

        if (!reply.getGeniResponseCode().isSuccess())
            throw new JFedException("Could not retrieve slice expiration from server", reply);

        return reply.getValue().values().stream()
                .map(AbstractFederationApi.SliceInfo::getExpirationDate).findAny()
                .orElse(null);
    }

    @Override
    public boolean isRegisterAggregatesForSliceSupported() {
        return true;
    }

    @Override
    public void registerAggregatesForSlice(
            @Nonnull Logger logger,
            @Nonnull List<AnyCredential> sliceCredentials,
            @Nonnull GeniUrn sliceUrn,
            @Nonnull GeniUrn aggregateUrn,
            @Nonnull Collection<GeniUrn> sliverUrns,
            @Nonnull Date sliverExpirationDate) throws JFedException {
        LOG.debug("UniformFederationApi2UserAndSliceApiWrapper will register: " + sliverUrns.size());

        GeniUrn creatorUrn = getRelevantUserUrn();
        assert creatorUrn != null;

//        Date expiration = sliverExpirationDate;

//        //fall back to slice expiration
//        if (expiration == null)
//            for (AnyCredential c : sliceCredentials)
//                if (c.getExpiresDate() != null && (expiration == null || expiration.after(c.getExpiresDate())))
//                    expiration = c.getExpiresDate();

        boolean hasFailures = false;
        for (GeniUrn sliverUrn : sliverUrns) {
            LOG.debug("UniformFederationApi2UserAndSliceApiWrapper is registering: " + sliverUrn);

            try {
                sa(logger).createSliverInfo(getSaConnection(), finalizeSliceCredentials(sliceCredentials),
                        sliceUrn, sliverUrn, aggregateUrn, creatorUrn,
                        sliverExpirationDate, null, addCredentialExtraOptions(null));
            } catch (JFedException e) {
                LOG.error("Failed to register {} for slice {}", sliverUrn, sliceUrn, e);
                hasFailures = true;
            }
        }

        if (hasFailures)
            throw new JFedException("One or more slivers failed to register to the SliceRegistery");
    }

    @Override
    public void updateSliverExpirationDateInfoForSlice(
            @Nonnull Logger logger,
            @Nonnull List<AnyCredential> sliceCredentials,
            @Nonnull GeniUrn sliceUrn,
            @Nonnull Collection<GeniUrn> sliverUrns,
            @Nonnull Date newSliverExpirationDate) throws JFedException {
        LOG.debug("UniformFederationApi2UserAndSliceApiWrapper will register: " + sliverUrns.size());
        GeniUrn creatorUrn = getRelevantUserUrn();
        assert creatorUrn != null;

//        Date expiration = newSliverExpirationDate;
//
//        //fall back to slice expiration
//        if (expiration == null)
//            for (AnyCredential c : sliceCredentials)
//                if (c.getExpiresDate() != null && (expiration == null || expiration.after(c.getExpiresDate())))
//                    expiration = c.getExpiresDate();

        Map<String, String> fields = new HashMap<>();
        fields.put("SLIVER_INFO_EXPIRATION", RFC3339Util.dateToRFC3339String(newSliverExpirationDate, true/*force zulu*/));

        for (GeniUrn sliverUrn : sliverUrns) {
            LOG.debug("UniformFederationApi2UserAndSliceApiWrapper is registering: " + sliverUrn);
            sa(logger).updateSliverInfo(getSaConnection(), finalizeSliceCredentials(sliceCredentials),
                    sliverUrn.toString(), fields, addCredentialExtraOptions(null));
        }
    }

    @Override
    public void unregisterAggregatesForSlice(@Nonnull Logger logger, @Nonnull List<AnyCredential> sliceCredentials,
                                             @Nonnull GeniUrn sliceUrn, @Nonnull GeniUrn aggregateUrn,
                                             @Nullable Collection<GeniUrn> sliverUrns) throws JFedException {
        Date expiration = null;
        for (AnyCredential c : sliceCredentials)
            if (c.getExpiresDate() != null && (expiration == null || expiration.after(c.getExpiresDate())))
                expiration = c.getExpiresDate();


        List<GeniUrn> sliverInfosToDelete = new ArrayList<>();

        if (sliverUrns != null) {
            LOG.debug("UniformFederationApi2UserAndSliceApiWrapper will unregister: " + sliverUrns.size());
            //first check which slivers are still registered
            try {
                AbstractFederationApi.FederationApiReply<AbstractFederationApi.SliverInfoList>
                        reply =
                        sa(logger).lookupSliverInfo(getSaConnection(), finalizeSliceCredentials(sliceCredentials),
                                sliceUrn, null, null, addCredentialExtraOptions(null));

                if (reply.getGeniResponseCode().isSuccess())
                    reply.getValue().values().stream()
                            .map(AbstractFederationApi.SliverInfo::getSliverUrn)
                            .filter(sliverUrns::contains)
                            .forEach(sliverInfosToDelete::add);
                else {
                    LOG.warn("lookupSliverInfo call failed for slice {}. Will forcefully try to remove all passed slivers.", sliceUrn);
                    sliverInfosToDelete.addAll(sliverUrns);
                }
            } catch (JFedException t) {
                LOG.warn("Exception checking registered slivers before deleting them. Will forcefully try to remove all passed slivers", t);
            /*ignore*/
                sliverInfosToDelete.addAll(sliverUrns);
            }
        } else {
            LOG.debug("UniformFederationApi2UserAndSliceApiWrapper will unregister ALL sliver for " + aggregateUrn);

            AbstractFederationApi.FederationApiReply<FederationSliceAuthorityApi2.SliverInfoList>
                    reply =
                    sa(logger).lookupSliverInfo(getSaConnection(), finalizeSliceCredentials(sliceCredentials),
                            sliceUrn, null, null, addCredentialExtraOptions(null));

            if (!reply.getGeniResponseCode().isSuccess())
                throw new JFedException("Could not retrieve registered slivers for slice '" + sliceUrn + "' from server", reply);

            for (FederationSliceAuthorityApi2.SliverInfo sliverInfo : reply.getValue().values()) {
                GeniUrn sliverCmUrn = sliverInfo.getAggregateUrn();
                if (Objects.equals(sliverCmUrn, aggregateUrn))
                    sliverInfosToDelete.add(sliverInfo.getSliverUrn());
            }


        }

        boolean hasFailure = false;
        for (GeniUrn sliverUrn : sliverInfosToDelete) {
            LOG.debug("UniformFederationApi2UserAndSliceApiWrapper is unregistering: " + sliverUrn);

            try {
                sa(logger).deleteSliverInfo(getSaConnection(), finalizeSliceCredentials(sliceCredentials),
                        sliverUrn.toString(), addCredentialExtraOptions(null));
            } catch (JFedException e) {
                LOG.warn("Error while deleting sliverInfo for {}", sliverUrn, e);
                hasFailure = true;
            }
        }

        if (hasFailure)
            throw new JFedException("One or more deleteSliverInfo calls failed");
    }

    @Override
    public void shareSlice(@Nonnull Logger logger, @Nonnull GeniUrn sliceUrn, @Nonnull List<AnyCredential> sliceCredentials, @Nonnull GeniUrn userUrn) throws JFedException {
        final List<FederationSliceAuthorityApi2.UrnRoleTuple> membersToAdd =
                Collections.singletonList(new FederationSliceAuthorityApi2.UrnRoleTuple(FederationSliceAuthorityApi2.TYPE_SLICE, userUrn, FederationSliceAuthorityApi2.ROLE_MEMBER));

        AbstractFederationApi.FederationApiReply<String> reply =
                sa(logger).modifyMembership(getSaConnection(), FederationSliceAuthorityApi2.TYPE_SLICE,
                        sliceUrn, membersToAdd, null, null,
                        finalizeSliceCredentials(sliceCredentials), addCredentialExtraOptions(null)
                );

        if (!reply.getGeniResponseCode().isSuccess()) {
            if (reply.getOutput() != null && reply.getOutput().contains("DUPLICATE")) {
                LOG.warn("Reply to shared slice was \"" + reply.getOutput() + "\". Assuming this means it is successfully shared already.");
                return;
            }
            throw new JFedException("Sharing slice with " + userUrn + " did not succeed: ", reply);
        }
    }

    @Override
    public List<GeniUrn> getUsersForSubAuthority(@Nonnull Logger logger, @Nonnull String projectName) throws JFedException {
        assert getLoggedInUserAuthorityServer() != null;
        GeniUrn projectUrn =
                GeniUrn.createGeniUrnFromEncodedParts(getLoggedInUserAuthorityServer().getUrnTld(), "project", projectName);
        AbstractFederationApi.FederationApiReply<List<FederationSliceAuthorityApi2.UrnRoleTuple>> reply =
                sa(logger).lookupMembers(getSaConnection(), FederationSliceAuthorityApi2.TYPE_PROJECT, projectUrn,
                        addSpeaksForCredentials(userCredentials), addCredentialExtraOptions(null));

        if (!reply.getGeniResponseCode().isSuccess())
            throw new JFedException("Error getting users in project " + projectUrn, reply);

        return reply.getValue().stream().map(FederationSliceAuthorityApi2.UrnRoleTuple::getUrn).collect(Collectors.toList());
    }

    @Override
    public List<GeniUrn> getUsersForSlice(@Nonnull Logger logger, @Nonnull GeniUrn sliceUrn, @Nonnull List<AnyCredential> sliceCredentials) throws JFedException {
        AbstractFederationApi.FederationApiReply<List<FederationSliceAuthorityApi2.UrnRoleTuple>> reply =
                sa(logger).lookupMembers(getSaConnection(), FederationSliceAuthorityApi2.TYPE_SLICE, sliceUrn,
                        finalizeSliceCredentials(sliceCredentials), addCredentialExtraOptions(null));

        if (!reply.getGeniResponseCode().isSuccess())
            throw new JFedException("Error getting users in " + sliceUrn, reply);

        return reply.getValue().stream().map(FederationSliceAuthorityApi2.UrnRoleTuple::getUrn).collect(Collectors.toList());
    }

    @Override
    public void addUsersToSlice(@Nonnull Logger logger, @Nonnull GeniUrn sliceUrn, @Nonnull List<AnyCredential> sliceCredentials,
                                @Nonnull List<GeniUrn> userUrns, @Nonnull String role) throws JFedException {
        List<FederationSliceAuthorityApi2.UrnRoleTuple> membersToAdd =
                userUrns.stream()
                        .map(userUrn -> new FederationSliceAuthorityApi2.UrnRoleTuple("SLICE", userUrn, role))
                        .collect(Collectors.toList());

        AbstractFederationApi.FederationApiReply<String> reply =
                sa(logger).modifyMembership(getSaConnection(),
                        FederationSliceAuthorityApi2.TYPE_SLICE, sliceUrn,
                        membersToAdd, null, null,
                        finalizeSliceCredentials(sliceCredentials), addCredentialExtraOptions(null));

        if (!reply.getGeniResponseCode().isSuccess()) {
            throw new JFedException("Error adding user to " + sliceUrn, reply);
        }
    }

    @Override
    public SliceInfo createSlice(@Nonnull Logger logger, @Nonnull String sliceName, Date expirationDate, @Nullable String subAuthName) throws JFedException {
        LOG.debug("createSlice called");
        //2 steps: create slice, and then get its credential(s)

        FederationSliceAuthorityApi2.GetVersionSAResult gv = getGetVersionSAResult(logger);

        Map<String, String> fields = new HashMap<>();
        fields.put("SLICE_DESCRIPTION", "jFed Experimenter GUI slice '" + sliceName + "' for user " + getRelevantUserUrnString());

        if (gv.getServices().contains("PROJECT")) {
            if (subAuthName != null) {
                assert getRelevantUserUrn() != null;
                GeniUrn project =
                        GeniUrn.createGeniUrnFromEncodedParts(getRelevantUserUrn().getEncodedTopLevelAuthority(), "project", subAuthName);
                fields.put("SLICE_PROJECT_URN", project.getValue());
            } else {
                throw new JFedException("A project must be provided to create a slice");
            }
        }

        if (gv.getFieldsForObject("PROJECT").containsKey("_GENI_SLICE_EMAIL")) {
            AbstractFederationApi.MemberInfoList userIdentifyingInfo =
                    getUserIdentifyingInfo(logger, getRelevantUserUrn());

            if (userIdentifyingInfo != null && userIdentifyingInfo.get(getRelevantUserUrn()).getEmail() != null) {
                String userEmail = userIdentifyingInfo.get(getRelevantUserUrn()).getEmail();
                fields.put("_GENI_SLICE_EMAIL", userEmail);
            } else {
                throw new JFedException("Slice creation requires a user email, but the user email is not known.");
            }
        }

//      TODO  addRequiredSupplementaryFieldsToCreate(fields, "SLICE");

        if (expirationDate != null) {
            fields.put("SLICE_EXPIRATION", RFC3339Util.dateToRFC3339String(expirationDate, true, true, true));
        }

        LOG.debug("createSlice is calling server with sliceName=" + sliceName + " and fields: " + fields);

        AbstractFederationApi2.FederationApiReply<AbstractFederationApi2.SliceInfo> reply =
                sa(logger).createSlice(getSaConnection(), addSpeaksForCredentials(userCredentials),
                        sliceName, fields, addCredentialExtraOptions(null));

        LOG.debug("createSlice got answer from server");

        if (!reply.getGeniResponseCode().isSuccess()) {
            throw new JFedException("Error creating slice", reply);
        }

        AbstractFederationApi2.SliceInfo sliceInfo = reply.getValue();

//            if (sliceInfo.containsKey("SLICE_CREDENTIAL"?)) {
        //        //TODO slice credential is normally in returned fields? Otherwise, we can retrieve it with a call.
//            }

        AbstractFederationApi2.FederationApiReply<List<AnyCredential>> credReply =
                sa(logger).getSliceCredentials(getSaConnection(), addSpeaksForCredentials(userCredentials),
                        sliceInfo.getSliceUrn(), addCredentialExtraOptions(null));

        if (!credReply.getGeniResponseCode().isSuccess())
            throw new JFedException("Failed to get slice credentials", credReply);


        List<AnyCredential> sliceCredentials = new ArrayList<>();
        if (speaksForCredentials != null && !speaksForCredentials.isEmpty())
            sliceCredentials.addAll(speaksForCredentials);
        sliceCredentials.addAll(credReply.getValue());

        return new SliceInfo(sliceName, sliceInfo.getSliceUrn(), sliceCredentials);


    }

    @Override
    public List<AnyCredential> renewSlice(@Nonnull Logger logger, @Nonnull List<AnyCredential> sliceCredentials, @Nonnull Date newExpirationDate) throws JFedException {
        GeniUrn sliceUrn;
        try {
            sliceUrn = findSliceUrn(sliceCredentials);
        } catch (IllegalArgumentException e) {
            throw new JFedException("Could not find slice URN in provided slice credential list.");
        }

        Map<String, String> fields = new HashMap<>();
        fields.put("SLICE_EXPIRATION", RFC3339Util.dateToRFC3339String(newExpirationDate, true/*zulu timezone*/, true/*discard subsecond*/, true/*use Z*/));
        AbstractFederationApi.FederationApiReply<String> updateReply =
                sa(logger).updateSlice(getSaConnection(), finalizeSliceCredentials(sliceCredentials), sliceUrn,
                        fields, addCredentialExtraOptions(null));

        if (!updateReply.getGeniResponseCode().isSuccess())
            throw new JFedException("Failed to update SLICE_EXPIRATION", updateReply);

        AbstractFederationApi2.FederationApiReply<List<AnyCredential>> credReply =
                sa(logger).getSliceCredentials(getSaConnection(), addSpeaksForCredentials(userCredentials), sliceUrn,
                        addCredentialExtraOptions(null));

        if (!credReply.getGeniResponseCode().isSuccess())
            throw new JFedException("Failed to get updated slice credentials", credReply);

        List<AnyCredential> newSliceCredentials = new ArrayList<>();
        if (speaksForCredentials != null && !speaksForCredentials.isEmpty())
            newSliceCredentials.addAll(speaksForCredentials);
        newSliceCredentials.addAll(credReply.getValue());
        return newSliceCredentials;
    }

    @Override
    public List<String> getSshKeysForUser(@Nonnull Logger logger, @Nonnull GeniUrn userUrn) throws JFedException {
        AbstractFederationApi.FederationApiReply<FederationMemberAuthorityApi2.MemberKeyInfoList>
                reply =
                ma(logger).lookupKeyForMember(getMaConnection(), addSpeaksForCredentials(userCredentials),
                        userUrn, null, null, addCredentialExtraOptions(null));
        if (!reply.getGeniResponseCode().isSuccess())
            throw new JFedException("Failed to get SSH keys", reply);

        AbstractFederationApi.LookupResult<FederationMemberAuthorityApi2.MemberKeyInfo> lRes = reply.getValue();
        List<String> res = new ArrayList<>();
        for (FederationMemberAuthorityApi2.MemberKeyInfo key : lRes.values()) {
            if (key.getPublicKey() != null)
                res.add(key.getPublicKey());
        }


        try {
            Map<String, String> matchMember = new HashMap<>();
            matchMember.put("MEMBER_URN", userUrn.toString());
            AbstractFederationApi.FederationApiReply<FederationMemberAuthorityApi2.MemberInfoList>
                    reply2 =
                    ma(logger).lookupMember(getMaConnection(), addSpeaksForCredentials(userCredentials), matchMember, null, addCredentialExtraOptions(null));

            if (!reply.getGeniResponseCode().isSuccess())
                throw new JFedException("Failed to get SSH keys from user certificate because lookup MEMBER " + userUrn + " call failed",
                        reply);

            AbstractFederationApi.LookupResult<FederationMemberAuthorityApi2.MemberInfo> lookupResult =
                    reply2.getValue();
            if (lookupResult.size() != 1)
                throw new JFedException("Failed to get SSH keys from user certificate because " +
                        "lookup MEMBER " + userUrn + " call does not contain a member ssl certificate", reply);


            //add certificate ssh key
            LOG.debug("Looked up member in order to find public key in ssl certificate");

            final FederationMemberAuthorityApi2.MemberInfo memberInfo = lookupResult.values().iterator().next();

            if (memberInfo.getX509pemCerts() == null)
                throw new JFedException("Failed to get SSH keys from user certificate because " +
                        "lookup MEMBER " + userUrn + " call does not contain a member ssl certificate.", reply);

            for (X509Certificate cert : memberInfo.getX509pemCerts()) {
                //a bit of a hack, to find the user certificate in the chain
                List<GeniUrn> subjectUrns =
                        KeyUtil.findUrnsInCertAltNames(cert, KeyUtil.AltNamesSource.SUBJECT_ALT_NAMES, false);
                LOG.debug("subjectUrns in cert: " + subjectUrns);

                for (GeniUrn subjectUrn : subjectUrns) {
                    if (Objects.equals(subjectUrn.getResourceType(), "user")) {
                        String openSshKey = KeyUtil.publicKeyToOpenSshAuthorizedKeysFormat(cert.getPublicKey());
                        res.add(openSshKey);
                        LOG.debug("Got user key from certificate: " + openSshKey);
                        break;
                    }
                }
            }
        } catch (Exception e) {
            LOG.warn("Failed to get SSH keys from user certificate", e);
        }

        //remove duplicate keys
        LOG.debug("Keys (" + res.size() + ") seen for " + userUrn + ": " + res);
        List<String> uniqueRes = new ArrayList<>();
        Set<String> seenKeys = new HashSet<>();
        for (String key : res) {
            String[] parts = key.split(" ");
            if (parts.length >= 2 && parts[0].startsWith("ssh-")) {
                boolean added = seenKeys.add(parts[1]);
                if (added)
                    uniqueRes.add(key);
                else
                    LOG.debug("Duplicate key removed: " + key);
            } else {
                LOG.warn("Unexpected key format seen: " + key);
                uniqueRes.add(key);
            }
        }

        return uniqueRes;
    }


    /* sub authority support */
    @Override
    public SubAuthoritySupport getSubAuthoritySupport(@Nonnull Logger logger) throws JFedException {
        FederationSliceAuthorityApi2.GetVersionSAResult getSAVersionRes = getGetVersionSAResult(logger);
        if (!getSAVersionRes.getServices().contains("PROJECT"))
            return SubAuthoritySupport.SUB_AUTHORITY_FORBIDDEN;
        else
            return SubAuthoritySupport.SUB_AUTHORITY_MANDATORY;
    }

    @Override
    public List<String> getSubAuthorityNames(@Nonnull Logger logger, @Nonnull GeniUrn user) throws JFedException {
        List<String> res =
                getUserProjects(logger, user).stream().map(GeniUrn::getEncodedResourceName).collect(Collectors.toList());

        LOG.debug("returning subauthorities: {}", res);
        return res;
    }


    @Override
    public void setSpeaksFor(List<AnyCredential> speaksForCredentials, @Nullable GeniUrn speakingForUrn) {
        super.setSpeaksFor(speaksForCredentials, speakingForUrn);
        if (speakingForUrn != null)
            setExtraOptionsForCallsWithCredential(Collections.singletonMap("speaking_for", speakingForUrn.getValue()));
    }
}
