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.AbstractGeniAggregateManager;
import be.iminds.ilabt.jfed.lowlevel.api.AggregateManager3;
import be.iminds.ilabt.jfed.lowlevel.api.user_spec.UserSpec;
import be.iminds.ilabt.jfed.lowlevel.api_wrapper.AggregateManagerWrapper;
import be.iminds.ilabt.jfed.lowlevel.api_wrapper.ErrorDetails;
import be.iminds.ilabt.jfed.lowlevel.api_wrapper.StatusDetails;
import be.iminds.ilabt.jfed.lowlevel.connection.JFedConnectionProvider;
import be.iminds.ilabt.jfed.lowlevel.connection.SfaConnection;
import be.iminds.ilabt.jfed.lowlevel.resourceid.ResourceUrn;
import be.iminds.ilabt.jfed.lowlevel.stitching.StitchingDirector;
import be.iminds.ilabt.jfed.preferences.JFedPreferences;
import be.iminds.ilabt.jfed.util.GeniUrn;
import be.iminds.ilabt.jfed.util.RFC3339Util;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
import java.util.*;
import java.util.stream.Collectors;

/**
 * AMv3Wrapper
 */
public class AMv3Wrapper extends AggregateManagerWrapper {
    private final static org.slf4j.Logger LOG = LoggerFactory.getLogger(AMv3Wrapper.class);

    @Nonnull
    private final AggregateManager3 am3;


    /**
     * returns only the SfaCredentials in the list, discarding the ABAC and other credentials.
     * <p>
     * This has been temporary added to AMv3Wrapper. Make this return origCredentials to get the correct AMv3 behaviour. see FEDIBBTDEV-752
     */
    @Nonnull
    @Override
    public List<AnyCredential> filterCredentials(@Nonnull List<AnyCredential> origCredentials) {
        assert amServer != null;
        boolean removeAbac = amServer.hasFlag(Server.Flag.workaroundRemoveAbacCredential);
        boolean removeSfa = amServer.hasFlag(Server.Flag.workaroundRemoveSfaCredential);

        if (!removeAbac && !removeSfa) {
            //remove nothing
            return origCredentials;
        }

        List<AnyCredential> res = new ArrayList<>();
        for (AnyCredential cred : origCredentials) {
            if (cred instanceof SfaCredential) {
                if (!removeSfa) {
                    res.add(cred);
                } else {
                    LOG.debug("Removing SfaCredential");
                }
            }
            if (cred instanceof AbacCredential) {
                if ((!removeAbac || cred.isSpeaksFor()))
                    res.add(cred);
                else {
                    LOG.debug("Removing AbacCredential");
                }
            }
        }
        if (res.isEmpty() && !origCredentials.isEmpty()) {
            LOG.warn("After filtering credentials, none are left. Will have to fall back to using them all anyway.");
            return origCredentials;
        }
        return res;
    }


    public AMv3Wrapper(@Nonnull Logger logger, GeniUserProvider geniUserProvider, JFedConnectionProvider connectionProvider, @Nonnull JFedPreferences jFedPreferences, Server amServer) {
        super(logger, geniUserProvider, connectionProvider, jFedPreferences, amServer);

        am3 = new AggregateManager3(logger, jFedPreferences);
    }

    @Nonnull
    public static StatusDetails.SliverStatus getGlobalSliverStatus(@Nonnull AggregateManager3.SliverInfo sliverInfo) {
        final String operationalStatus = sliverInfo.getOperationalStatus();

        //allocation states come first, only for the "geni_provisioned" state, we need to look at the operational state
        if (sliverInfo.getAllocationStatus().equalsIgnoreCase(AggregateManager3.GENI_UNALLOCATED_STATE))
            return StatusDetails.SliverStatus.UNALLOCATED;
        if (sliverInfo.getAllocationStatus().equalsIgnoreCase(AggregateManager3.GENI_ALLOCATED_STATE))
            return StatusDetails.SliverStatus.CHANGING; //actually, this means "unallocated" in SliverStatus terminology :-)

        //AM API v3 defined operatinoal states  (AMv3Wrapper does not currently support extra operational states advertised in RSpec advertisement)
        if (operationalStatus.equalsIgnoreCase("geni_pending_allocation")) return StatusDetails.SliverStatus.CHANGING;
        if (operationalStatus.equalsIgnoreCase("geni_notready")) return StatusDetails.SliverStatus.CHANGING;
        if (operationalStatus.equalsIgnoreCase("geni_configuring")) return StatusDetails.SliverStatus.CHANGING;
        if (operationalStatus.equalsIgnoreCase("geni_stopping")) return StatusDetails.SliverStatus.CHANGING;
        if (operationalStatus.equalsIgnoreCase("geni_ready")) return StatusDetails.SliverStatus.READY;
        if (operationalStatus.equalsIgnoreCase("geni_ready_busy")) return StatusDetails.SliverStatus.READY;
        if (operationalStatus.equalsIgnoreCase("geni_failed")) return StatusDetails.SliverStatus.FAIL;

        LOG.warn("Server returned unknown sliver operationalStatus: '{}'  (allocationStatus={})", operationalStatus, sliverInfo.getAllocationStatus());
        return StatusDetails.SliverStatus.UNKNOWN;
    }

    @Nonnull
    protected SfaConnection getConnection() throws JFedException {
        return (SfaConnection) getConnection(new ApiInfo.Api(ApiInfo.ApiName.GENI_AM, 3));
    }

    @Override
    public Map getVersion() throws JFedException {
        AbstractGeniAggregateManager.AggregateManagerReply<AggregateManager3.VersionInfo> reply =
                am3.getVersion(getConnection());

        if (!reply.getGeniResponseCode().isSuccess()) {
            throw new JFedException("GetVersion Call not successful", reply);
        }

        return reply.getRawResult();
    }

    @Override
    public String listResources(@Nonnull List<AnyCredential> userCredentials, boolean available, @Nonnull String type, @Nonnull String version) throws JFedException {
        AbstractGeniAggregateManager.AggregateManagerReply<String> reply = am3.listResources(
                getConnection(), addSpeaksForCredentials(filterCredentials(userCredentials)),
                type, version, available, true, addCredentialExtraOptions(null));
        lastAmReply = reply;

        if (!reply.getGeniResponseCode().isSuccess()) {
            throw new JFedException("ListResources Call not successful", reply);
        }

        return reply.getValue();
    }

    public boolean hasSeperateAllocateAndProvision() {
        return true;
    }

    public static class AllocateReplyAmv3 implements AllocateReply {
        private final String manifest;
        @Nullable
        private final List<GeniUrn> sliverUrns;

        //dirty trick to prevent erasure clash
        private AllocateReplyAmv3(@Nullable List<AggregateManager3.SliverInfo> sliverInfos, String manifest) {
            this.manifest = manifest;
            if (sliverInfos != null) {
                this.sliverUrns = new ArrayList<>();
                for (AggregateManager3.SliverInfo si : sliverInfos) {
                    try {
                        GeniUrn sliverUrn = new GeniUrn(si.getSliverUrn());
                        sliverUrns.add(sliverUrn);
                    } catch (GeniUrn.GeniUrnParseException e) {
                        LOG.error("Could not parse GeniUrn in AMv3 Allocate reply", e);
                    }
                }
            } else {
                sliverUrns = null;
            }
        }

        @Override
        public String getManifest() {
            return manifest;
        }

        @Override
        @Nullable
        public List<GeniUrn> getSliverUrns() {
            return sliverUrns;
        }
    }

    @Nonnull
    public AllocateReplyAmv3 allocate(@Nonnull GeniUrn sliceUrn, @Nonnull List<AnyCredential> sliceCredentials, @Nonnull String rspec, @Nullable Date requestedExpirationDate)
            throws JFedException {
        String endTime = requestedExpirationDate == null ? null :
                RFC3339Util.dateToRFC3339String(requestedExpirationDate, true, true, true); //most compatible date format

        AbstractGeniAggregateManager.AggregateManagerReply<AggregateManager3.AllocateAndProvisionInfo> replyAllocate =
                am3.allocate(getConnection(), sliceUrn.getValue(), finalizeSliceCredentials(sliceCredentials),
                        rspec, endTime, addCredentialExtraOptions(null));
        lastAmReply = replyAllocate;

        if (!replyAllocate.getGeniResponseCode().isSuccess()) {
            if (AbstractGeniAggregateManager.isNotEnoughFreeResources(replyAllocate))
                throw new NotEnoughFreeResourcesException(replyAllocate);

            if (StitchingDirector.isAnyNotSupported(replyAllocate))
                throw new AnyVlanUnsupportedException(replyAllocate);

            if (StitchingDirector.isUnavailableVlan(replyAllocate))
                throw new VlanUnavailableException(StitchingDirector.getUnavailableVlan(replyAllocate), replyAllocate);

            throw new JFedException("Allocate Call not successful", replyAllocate);
        }

        if (replyAllocate.getValue() == null)
            throw new JFedException("Allocate call was succesful, but returned nothing", replyAllocate);

        return new AllocateReplyAmv3(replyAllocate.getValue().getSliverInfo(), replyAllocate.getValue().getRspec());
    }

    @Nonnull
    public AllocateReply allocate(@Nonnull GeniUrn sliceUrn, @Nonnull List<AnyCredential> sliceCredentials, @Nonnull String rspec, @Nonnull Date startDate, @Nonnull Date endDate)
            throws JFedException {
        String startTime = RFC3339Util.dateToRFC3339String(startDate, true, true, true); //most compatible date format
        String endTime = RFC3339Util.dateToRFC3339String(endDate, true, true, true); //most compatible date format

        Map<String, Object> extraOptionsStartTime = Collections.singletonMap("geni_start_time", startTime);

        AbstractGeniAggregateManager.AggregateManagerReply<AggregateManager3.AllocateAndProvisionInfo> replyAllocate =
                am3.allocate(getConnection(), sliceUrn.getValue(), finalizeSliceCredentials(sliceCredentials),
                        rspec, endTime, addCredentialExtraOptionsToMap(extraOptionsStartTime));
        lastAmReply = replyAllocate;

        if (!replyAllocate.getGeniResponseCode().isSuccess()) {
            if (AbstractGeniAggregateManager.isNotEnoughFreeResources(replyAllocate))
                throw new NotEnoughFreeResourcesException(replyAllocate);

            if (StitchingDirector.isAnyNotSupported(replyAllocate))
                throw new AnyVlanUnsupportedException(replyAllocate);

            if (StitchingDirector.isUnavailableVlan(replyAllocate))
                throw new VlanUnavailableException(StitchingDirector.getUnavailableVlan(replyAllocate), replyAllocate);

            throw new JFedException("Allocate Call not successful", replyAllocate);
        }

        if (replyAllocate.getValue() == null)
            throw new JFedException("Allocate call was succesful, but returned nothing", replyAllocate);

        return new AllocateReplyAmv3(replyAllocate.getValue().getSliverInfo(), replyAllocate.getValue().getRspec());
    }


    public String provisionAndStart(@Nonnull GeniUrn sliceUrn, @Nonnull List<AnyCredential> sliceCredentials, @Nullable List<UserSpec> users, @Nullable Date requestedExpirationDate)
            throws JFedException {
        String endTime = requestedExpirationDate == null ? null :
                RFC3339Util.dateToRFC3339String(requestedExpirationDate, true, true, true); //most compatible date format

        List<String> urns = Collections.singletonList(sliceUrn.getValue());
        AbstractGeniAggregateManager.AggregateManagerReply<AggregateManager3.AllocateAndProvisionInfo> replyProvision =
                am3.provision(getConnection(), urns, finalizeSliceCredentials(sliceCredentials),
                        "geni", "3", null, endTime, users, addCredentialExtraOptions(null));
        lastAmReply = replyProvision;

        if (!replyProvision.getGeniResponseCode().isSuccess()) {
            if (AbstractGeniAggregateManager.isNotEnoughFreeResources(replyProvision)) //doesn't normally occur at Provision!
                throw new NotEnoughFreeResourcesException(replyProvision);

            if (StitchingDirector.isAnyNotSupported(replyProvision))
                throw new AnyVlanUnsupportedException(replyProvision);

            if (StitchingDirector.isUnavailableVlan(replyProvision))
                throw new VlanUnavailableException(StitchingDirector.getUnavailableVlan(replyProvision), replyProvision);

            throw new JFedException("Provision Call not successful", replyProvision);
        }

        AbstractGeniAggregateManager.AggregateManagerReply<List<AggregateManager3.SliverInfo>> replyPerfOpAction =
                am3.performOperationalAction(getConnection(), urns, finalizeSliceCredentials(sliceCredentials),
                        "geni_start", null, addCredentialExtraOptions(null));
        lastAmReply = replyProvision; //SET LAST REPLY TO PROVISION REPLY!

        if (!replyPerfOpAction.getGeniResponseCode().isSuccess()) {
            if (AbstractGeniAggregateManager.isNotEnoughFreeResources(replyPerfOpAction)) //doesn't normally occur at PerformOperationAction!
                throw new NotEnoughFreeResourcesException(replyPerfOpAction);

            throw new JFedException("PerformOperationAction Call not successful", replyPerfOpAction);
        }

        //check first sliver
        if (requestedExpirationDate != null
                && replyProvision.getGeniResponseCode().isSuccess()
                && replyProvision.getValue() != null
                && replyProvision.getValue().getSliverInfo() != null
                && !replyProvision.getValue().getSliverInfo().isEmpty()) {
            try {
                Date actualExpiresDate = replyProvision.getValue().getSliverInfo().get(0).getExpiresDate();
                if (actualExpiresDate.after(requestedExpirationDate)) {
                    LOG.info("We requested a geni_end_time in the Provision call, but the AMv3 AM returned a later date! To try fix this, we execute a Renew.");

                    //but we wait first, since it will always be busy if you try immediately.
                    try {
                        Thread.sleep(30 * 1000L);
                    } catch (InterruptedException e) { /*ignore, doesn't matter if sleep worked or not*/ }

                    try {
                        renewSlivers(sliceUrn, null/*sliverUrns*/, finalizeSliceCredentials(sliceCredentials), requestedExpirationDate);
                    } catch (JFedException ex) {
                        LOG.error("Error when trying to define expiration date for newly created sliver", ex);
                    }
                }
            } catch (ParseException e) {
                LOG.warn("Error parsing Provision reply geni_expires date '{}'", replyProvision.getValue().getSliverInfo().get(0).getExpires());
            }
            lastAmReply = replyProvision; //SET LAST REPLY TO PROVISION REPLY!
        }

        return replyProvision.getValue().getRspec();
    }

    @Override
    public String createSliver(@Nonnull GeniUrn sliceUrn, @Nonnull List<AnyCredential> sliceCredentials, @Nonnull String rspec, @Nullable List<UserSpec> users, @Nullable Date requestedExpirationDate)
            throws JFedException {
        allocate(sliceUrn, sliceCredentials, rspec, requestedExpirationDate);
        return provisionAndStart(sliceUrn, sliceCredentials, users, requestedExpirationDate);
    }

    @Override
    public void deleteSliver(@Nonnull GeniUrn sliceUrn, @Nonnull List<AnyCredential> sliceCredentials) throws JFedException {
        List<String> urns = Collections.singletonList(sliceUrn.getValue());

        AbstractGeniAggregateManager.AggregateManagerReply<List<AggregateManager3.SliverInfo>> reply =
                am3.delete(getConnection(), urns, finalizeSliceCredentials(sliceCredentials),
                        null, addCredentialExtraOptions(null));
        lastAmReply = reply;

        if (!reply.getGeniResponseCode().isSuccess()) {
            if (Objects.equals(reply.getGeniResponseCode(), GeniAMResponseCode.GENIRESPONSE_SEARCHFAILED))
                return; //not an error, the sliver was already deleted, so all is ok.

            throw new JFedException("Delete Call not successful", reply);
        }
    }

    @Nullable
    @Override
    public StatusDetails status(@Nonnull GeniUrn sliceUrn, @Nonnull List<AnyCredential> sliceCredentials) throws JFedException {
        LOG.debug("Calling AMv3Wrapper status with {} credentials", sliceCredentials.size());

        List<String> urns = new ArrayList<>();
        urns.add(sliceUrn.getValue());

        AbstractGeniAggregateManager.AggregateManagerReply<AggregateManager3.StatusInfo> reply =
                am3.status(getConnection(), urns, finalizeSliceCredentials(sliceCredentials),
                        null, addCredentialExtraOptions(null));
        lastAmReply = reply;

        if (reply.getGeniResponseCode() == GeniAMResponseCode.GENIRESPONSE_SEARCHFAILED) {
            //the AM could not find any slivers associated to the given slice
            return new StatusDetails(StatusDetails.SliverStatus.UNALLOCATED);
        }

        AggregateManager3.StatusInfo status = reply.getValue();
        if (!reply.getGeniResponseCode().isSuccess() || status == null) {
            throw new JFedException("Status Call not successful", reply);
        }
        if (status.getSliverInfo().isEmpty())
            return new StatusDetails(StatusDetails.SliverStatus.UNALLOCATED); //no slivers


        StatusDetails res = null;
        for (AggregateManager3.SliverInfo sliverInfo : status.getSliverInfo()) {
            StatusDetails.SliverStatus s = getGlobalSliverStatus(sliverInfo);

            StatusDetails sliverStatusDetails = new StatusDetails(s, GeniUrn.parse(sliverInfo.getSliverUrn()));

            if (res != null) {
                res.add(sliverStatusDetails);
            } else {
                res = sliverStatusDetails;
            }

            if (sliverInfo.getExpires() != null)
                try {
                    res.addKnownExpires(sliverInfo.getExpiresDate());
                } catch (ParseException e) {
                    //ignore parse error
                    LOG.debug("Error parsing date in sliver_info {}", sliverInfo.getExpires(), e);
                }

            if (sliverInfo.getError() != null && !sliverInfo.getError().isEmpty())
                res.addError(new ErrorDetails(sliverInfo.getError(), sliverInfo.getSliverUrn()));
        }

        return res;
    }

    @Override
    public String describe(@Nonnull GeniUrn sliceUrn, @Nonnull List<AnyCredential> sliceCredentials) throws JFedException {
        List<String> urns = new ArrayList<>();
        urns.add(sliceUrn.getValue());

        String type = "geni";
        final String version = "3";
        if (amServer != null && amServer.isServerType("FOAM"))
            type = "GENI"; //see FEDIBBTDEV-1392: workaround for bug at al2s (it should ignore case but doesn't)

        AbstractGeniAggregateManager.AggregateManagerReply<AggregateManager3.ManifestInfo> reply =
                am3.describe(getConnection(), urns, finalizeSliceCredentials(sliceCredentials),
                        type, version, true, addCredentialExtraOptions(null));
        lastAmReply = reply;

        if (!reply.getGeniResponseCode().isSuccess()) {
            if (reply.getGeniResponseCode() == GeniAMResponseCode.GENIRESPONSE_SEARCHFAILED) {
                //the AM could not find any slivers associated to the given slice
                throw new JFedException("No such slice found", reply);
            }

            throw new JFedException("Describe Call not successful", reply);
        }

        return reply.getValue().getManifestRspec();
    }

    @Override
    public void renewSlivers(@Nullable GeniUrn sliceUrn, @Nullable List<GeniUrn> sliverUrns, @Nonnull List<AnyCredential> sliceCredentials, @Nonnull Date newExpire) throws JFedException {
        if (sliceUrn == null && (sliverUrns == null || sliverUrns.isEmpty()))
            throw new IllegalArgumentException("renewSlivers was called with both sliceUrn == null and sliverUrn empty or null (sliverUrns=" + sliverUrns + ")");

        List<String> urns;
        if (sliceUrn != null) {
            urns = Collections.singletonList(sliceUrn.getValue());
            if (sliverUrns != null && !sliverUrns.isEmpty())
                throw new IllegalArgumentException("Either the sliceUrn or the sliverUrns may be defined, not both");
        } else
            urns = sliverUrns.stream().map(ResourceUrn::getValue).collect(Collectors.toList());

        AbstractGeniAggregateManager.AggregateManagerReply<List<AggregateManager3.SliverInfo>> reply =
                am3.renew(getConnection(), urns, finalizeSliceCredentials(sliceCredentials),
                        newExpire, null, addCredentialExtraOptions(null));
        lastAmReply = reply;

        if (!reply.getGeniResponseCode().isSuccess()) {
            throw new JFedException("Renew Call not successful", reply);
        }
    }

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

    @Override
    public boolean isShareLanSupported() {
        assert amServer != null;
        return amServer.hasFlag(Server.Flag.featurePoaShareLan);
    }

    @Override
    public void shareLan(@Nonnull GeniUrn sliceUrn, @Nonnull List<AnyCredential> sliceCredentials, @Nonnull String lanClientId, @Nonnull String newSharedLanName) throws JFedException {
        Map<String, Object> extraOptions = new HashMap<>();
        extraOptions.put("geni_sharelan_lanname", lanClientId);
        extraOptions.put("geni_sharelan_token", newSharedLanName);
        AbstractGeniAggregateManager.AggregateManagerReply<List<AggregateManager3.SliverInfo>> reply =
                am3.performOperationalAction(getConnection(), Collections.singletonList(sliceUrn.getValue()),
                        finalizeSliceCredentials(sliceCredentials),
                        "geni_sharelan", false,
                        addCredentialExtraOptionsToMap(extraOptions));
        lastAmReply = reply;

        if (!reply.getGeniResponseCode().isSuccess())
            throw new JFedException("Share lan command failed: " + reply.getOutput(), reply);
    }

    @Override
    public void unshareLan(@Nonnull GeniUrn sliceUrn, @Nonnull List<AnyCredential> sliceCredentials, @Nonnull String lanClientId, @Nonnull String sharedLanName) throws JFedException {
        Map<String, Object> extraOptions = new HashMap<>();
        extraOptions.put("geni_sharelan_lanname", lanClientId);
        extraOptions.put("geni_sharelan_token", sharedLanName);

        AbstractGeniAggregateManager.AggregateManagerReply<List<AggregateManager3.SliverInfo>> reply =
                am3.performOperationalAction(getConnection(), Collections.singletonList(sliceUrn.getValue()),
                        finalizeSliceCredentials(sliceCredentials),
                        "geni_unsharelan", false,
                        addCredentialExtraOptionsToMap(extraOptions));
        lastAmReply = reply;

        if (!reply.getGeniResponseCode().isSuccess())
            throw new JFedException("Unshare lan command failed: " + reply.getOutput(), reply);
    }


    @Override
    public boolean isRestartSupported() {
        assert amServer != null;
        //exception for planetlab
        return !amServer.isServerType("planetlab");
    }

    @Override
    public boolean isReloadOSSupported() {
        assert amServer != null;
        //exception for planetlab
        return !amServer.isServerType("planetlab");
    }

    @Override
    public boolean isOpenConsoleSupported() {
        assert amServer != null;
        //TODO actually depends on emulab version
        //exception for planetlab
        return !amServer.isServerType("planetlab");
    }

    @Override
    public void restart(@Nonnull GeniUrn sliceUrn, @Nonnull List<AnyCredential> sliceCredentials) throws JFedException {
        AbstractGeniAggregateManager.AggregateManagerReply<List<AggregateManager3.SliverInfo>> reply =
                am3.performOperationalAction(getConnection(), Collections.singletonList(sliceUrn.getValue()),
                        finalizeSliceCredentials(sliceCredentials),
                        "geni_restart", false, addCredentialExtraOptions(null));
        lastAmReply = reply;

        if (!reply.getGeniResponseCode().isSuccess())
            throw new JFedException("Restart node command failed: " + reply.getOutput(), reply);
    }

    @Override
    public void reloadOS(@Nonnull GeniUrn sliceUrn, @Nonnull List<AnyCredential> sliceCredentials) throws JFedException {
        AbstractGeniAggregateManager.AggregateManagerReply<List<AggregateManager3.SliverInfo>> reply =
                am3.performOperationalAction(getConnection(), Collections.singletonList(sliceUrn.getValue()),
                        finalizeSliceCredentials(sliceCredentials),
                        "geni_reload", false, addCredentialExtraOptions(null));
        lastAmReply = reply;

        if (!reply.getGeniResponseCode().isSuccess())
            throw new JFedException("Reload OS on node command failed: " + reply.getOutput(), reply);
    }

    @Override
    public URL openConsole(@Nonnull GeniUrn sliverUrn, @Nonnull List<AnyCredential> sliceCredentials) throws JFedException {
        AbstractGeniAggregateManager.AggregateManagerReply reply =
                am3.performOperationalAction(getConnection(), Collections.singletonList(sliverUrn.getValue()),
                        finalizeSliceCredentials(sliceCredentials),
                        "geni_console_url", false, addCredentialExtraOptions(null));
        lastAmReply = reply;

        if (!reply.getGeniResponseCode().isSuccess())
            throw new JFedException("Get Console URL on node command failed: " + reply.getOutput(), reply);

        Object val = reply.getRawValue();
        if (val instanceof String) {
            try {
                return new URL((String) val);
            } catch (MalformedURLException e) {
                throw new JFedException("POA geni_console_url returned invalid URL: '" + val + "'", e);
            }
        } else
            throw new JFedException("POA geni_console_url returned invalid URL: '" + val + "'", reply);
    }


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

    @Override
    public void editSshKeys(@Nonnull GeniUrn sliceUrn, @Nonnull List<AnyCredential> sliceCredentials, @Nonnull Collection<UserSpec> userSpecs) throws JFedException {
        Map<String, Object> extraOptions = new HashMap<>();
        extraOptions.put("geni_users", userSpecs.stream()
                .map(UserSpec::getAsMap)
                .collect(Collectors.toList()));

        AbstractGeniAggregateManager.AggregateManagerReply<List<AggregateManager3.SliverInfo>> reply =
                am3.performOperationalAction(getConnection(), Collections.singletonList(sliceUrn.getValue()),
                        finalizeSliceCredentials(sliceCredentials),
                        "geni_update_users", false, addCredentialExtraOptionsToMap(extraOptions));
        lastAmReply = reply;

        if (!reply.getGeniResponseCode().isSuccess())
            throw new JFedException("Edit SSH keys command failed: " + reply.getOutput(), reply);
    }


    @Override
    public boolean areUserDiskImageFunctionsSupported() {
        return false;
    }

    @Override
    public void setSpeaksFor(@Nullable List<AnyCredential> speaksForCredentials, @Nullable GeniUrn speakingForUrn) {
        super.setSpeaksFor(speaksForCredentials, speakingForUrn);

        //NOTE: speaksFor credential itself should always be supplied as part of argument list of credentials each option takes
        if (speakingForUrn != null)
            setExtraOptionsForCallsWithCredential(Collections.singletonMap("geni_speaking_for", speakingForUrn.getValue()));
        else
            setExtraOptionsForCallsWithCredential(null);
    }
}
