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

import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Server;
import be.iminds.ilabt.jfed.lowlevel.*;
import be.iminds.ilabt.jfed.lowlevel.api.AbstractGeniAggregateManager;
import be.iminds.ilabt.jfed.lowlevel.api.AggregateManager2;
import be.iminds.ilabt.jfed.lowlevel.api.AggregateManager3;
import be.iminds.ilabt.jfed.lowlevel.api.StitchingComputationService;
import be.iminds.ilabt.jfed.lowlevel.authority.AuthorityFinder;
import be.iminds.ilabt.jfed.lowlevel.stitching.info.Hop;
import be.iminds.ilabt.jfed.util.GeniUrn;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * Stitcher
 */
public class StitchingDirector {
    public static final Logger ACTUAL_LOG = LoggerFactory.getLogger(StitchingDirector.class);

    @Nonnull
    private final org.slf4j.Logger log;

    public static final int MAX_UNAVAILABLE_VLANS_BEFORE_GIVEUP = 5;

    private static final boolean AUTOMATIC_NEXT_SUGGESTED_VLAN = true;


    @Nonnull
    private final TestbedInfoSource testbedInfoSource;
    @Nonnull
    private final AuthorityFinder authFinder;
    @Nullable
    private final Date expireDate;
    private boolean insecure;

    private StitchingComputationService.ComputePathResult computePathResult;
    private StitchingData computePathResultStitchingData;

    private final Set<Server> allAuthorities = new HashSet<>();
    private final Set<Server> activelyInvolvedAuthorities = new HashSet<>();

    private final Set<GeniUrn> aggUrnsNotSupportingAny = new HashSet<>();

    private boolean gaveUp = false;

    public StitchingDirector(@Nonnull TestbedInfoSource testbedInfoSource, @Nonnull AuthorityFinder authFinder,
                             @Nullable Date expireDate, @Nonnull org.slf4j.Logger log) {
        this.testbedInfoSource = testbedInfoSource;
        this.authFinder = authFinder;
        this.expireDate = expireDate;
        this.log = log;
    }

    public StitchingDirector(TestbedInfoSource testbedInfoSource, AuthorityFinder authFinder, @Nullable Date expireDate) {
        this(testbedInfoSource, authFinder, expireDate, ACTUAL_LOG);
    }


    public synchronized void setComputePathResult(@Nonnull StitchingComputationService.ComputePathResult computePathResult) throws JFedException {
        this.computePathResult = computePathResult;

        assert computePathResult.getWorkflowData() != null;

        computePathResultStitchingData =
                new StitchingData(
                        computePathResult.getServiceRspec(),
                        computePathResult.getWorkflowData(),
                        testbedInfoSource, authFinder, log);

        log.debug("setComputePathResult created new computePathResultStitchingData. size=" + computePathResultStitchingData.getOrderedStitchingCallDataList().size());

        for (StitchingCallData stitchingCallData : computePathResultStitchingData.getOrderedStitchingCallDataList()) {
            if (!stitchingCallData.getServer().isEdgeVlan())
                allAuthorities.add(stitchingCallData.getServer());

            if (!stitchingCallData.getServer().hasFlag(Server.Flag.featureStitchingAny))
                try {
                    aggUrnsNotSupportingAny.add(new GeniUrn(stitchingCallData.getServer().getDefaultComponentManagerUrn()));
                } catch (GeniUrn.GeniUrnParseException e) {
                    throw new JFedException("Failed to parse server defaultComponentManagerUrn: \"" + stitchingCallData.getServer().getDefaultComponentManagerUrn() + "\"", e);
                }
        }

        log.debug("setComputePathResult allAuthorities.size=" + allAuthorities.size());
        if (log.isDebugEnabled()) {
            String tmp = "";
            for (Server auth : allAuthorities) {
                tmp += auth.getDefaultComponentManagerUrn() + " ";
            }
            log.debug("setComputePathResult allAuthorities=" + tmp);
        }
    }

    /**
     * The caller MUST do all these actions. Calling again wil not return the same actions!
     * So the caller must store any action it won't do immediately.
     * <p/>
     * However, the caller may call again before all actions are completed.
     * Additional actions, or an empty list will be returned.
     * So, AN EMPTY LIST is not a sign that stitching is done,
     * unless all actions have been compelted and reported to the director.
     *
     * @return the actions needing to be performed now
     */
    public synchronized List<StitchAction> getStitchActions() {
        assert computePathResult != null : "Call setComputePathResult first";
        assert computePathResultStitchingData != null : "Call setComputePathResult first";

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

        for (StitchingCallData stitchingCallData : computePathResultStitchingData.getOrderedStitchingCallDataList()) {
            if (stitchingCallData.getServer() == null || stitchingCallData.getServer().isEdgeVlan()) {
                if (stitchingCallData.getServer() == null)
                    log.debug("getStitchActions() stitchingCallData.getAuth() == null");
                if (stitchingCallData.getServer().isEdgeVlan())
                    log.debug("getStitchActions() stitchingCallData.getAuth().isEdgeVlan()");
                continue;
            }

            if (stitchingCallData.getState() == StitchingCallData.State.MUST_DELETE) {
                log.debug("getStitchActions() creates DELETE StitchAction for " + stitchingCallData.getServer().getUrnTld());
                StitchAction stitchAction = new StitchAction(StichActionType.DELETE,
                        stitchingCallData.getServer(),
                        null/*rspec*/,
                        null/*stitchingCallData.getSuggestedVlans()*/,
                        stitchingCallData);
                stitchingCallData.setState(StitchingCallData.State.DELETING);

                res.add(stitchAction);
            }

            log.debug("getStitchActions() needsAdvertisementRspec=" + stitchingCallData.needsAdvertisementRspec() +
                    " for auth=" + stitchingCallData.getServer().getDefaultComponentManagerUrn());
            if (stitchingCallData.needsAdvertisementRspec() && stitchingCallData.getState() == StitchingCallData.State.NONE) {
                log.debug("getStitchActions() creates LISTRESOURCES StitchAction for " + stitchingCallData.getServer().getUrnTld());
                StitchAction stitchAction = new StitchAction(StichActionType.LISTRESOURCES,
                        stitchingCallData.getServer(),
                        null,
                        null,
                        stitchingCallData);

                stitchingCallData.setState(StitchingCallData.State.LISTING_RESOURCES);
                res.add(stitchAction);
            }

            //if ALLOCATING, do not return, caller should already be doing this action
            if (stitchingCallData.areAllDepsReady() &&
                    stitchingCallData.areAllDependingOnThisReady() &&
                    stitchingCallData.getState() == StitchingCallData.State.NONE) {
                boolean addExpires =
                        stitchingCallData.getServer().hasFlag(Server.Flag.workaroundAddExpiresAttributeToRequestRspec);
                log.debug("getStitchActions() creates ALLOCATE StitchAction for " +
                        stitchingCallData.getServer().getUrnTld() + " vlans=" + stitchingCallData.getSuggestedVlans() + " addExpires=" + addExpires + " expireDate=" + expireDate);
                StitchAction stitchAction = new StitchAction(StichActionType.ALLOCATE,
                        stitchingCallData.getServer(),
                        computePathResultStitchingData.getCurrentRequestRspec(
                                aggUrnsNotSupportingAny,
                                addExpires,
                                expireDate),
                        stitchingCallData.getSuggestedVlans(),
                        stitchingCallData);

                stitchingCallData.setState(StitchingCallData.State.ALLOCATING);
                res.add(stitchAction);
            } else
                log.debug("getStitchActions() NOT READY to allocate for " + stitchingCallData.getServer().getUrnTld() +
                        " (state=" + stitchingCallData.getState() + " depsOk=" + stitchingCallData.areAllDepsReady() + ")");
        }

        return res;
    }

    /**
     * returns true if any actions will possibly be available in the future.
     * returns false only if a call to getStitchActions() will always return zero = no more actions needed for stitching
     * <p/>
     * Note that if any calls such as allocting or deleting are in progress, this will return true until their result has been processed
     */
    public boolean areAnyActionsLeft() {
        assert computePathResult != null : "Call setComputePathResult first";
        assert computePathResultStitchingData != null : "Call setComputePathResult first";

        if (gaveUp)
            return false;

        for (StitchingCallData stitchingCallData : computePathResultStitchingData.getOrderedStitchingCallDataList()) {
            if (stitchingCallData.getServer() == null || stitchingCallData.getServer().isEdgeVlan()) {
                continue;
            }

            if (stitchingCallData.getState() != StitchingCallData.State.ALLOCATED) //MUST_DELETE, ALLOCATING, LISTING_RESOURCES, ALLOCATING_BUT_MUST_DELETE, DELETING or NONE
                return true;

            if (!stitchingCallData.areAllDepsReady())
                return true;

            if (!stitchingCallData.isAllocated())
                return true;
        }

        return false;
    }

    public boolean isGaveUp() {
        return gaveUp;
    }

    /*
     * Informative hop data, indexed by link
     *
     * LINK -> AUTH/HOP (multi with deps) -> STATE+AVAIL+SUGGEST/SET
     * */
    public synchronized Map<String, List<Hop>> getHopOverview() {
        Map<String, Hop> hopsByUrn = new HashMap<>();

        //make all hops, without dependencies filled in
        for (StitchingCallData stitchingCallData : computePathResultStitchingData.getOrderedStitchingCallDataList()) {
            for (StitchingHopData hopData : stitchingCallData.getAllHopData()) {
                Hop hop = new Hop(hopData.getLinkName(),
                        stitchingCallData.getAuthUrnString(),
                        stitchingCallData.getState(),
                        hopData.getHopUrn(),
                        hopData.getAvailableVlans(),
                        new VlanRange(hopData.getUnavailableVlans()),
                        hopData.getSuggestedVlan(),
                        stitchingCallData.getState() == StitchingCallData.State.ALLOCATED
                                ? hopData.getLastManifestVlan() : null,
                        hopData.getImportVlans(),
                        Collections.emptyList(),
                        Collections.emptyList());

                assert !hopsByUrn.containsKey(hop.getHopUrn()) : "hopsByUrn already contains " + hop.getHopUrn();
                hopsByUrn.put(hop.getHopUrn(), hop);
            }
        }

        Map<String, List<Hop>> res = new HashMap<>();
        for (StitchingCallData stitchingCallData : computePathResultStitchingData.getOrderedStitchingCallDataList()) {
            for (StitchingHopData hopData : stitchingCallData.getAllHopData()) {
                List<StitchingHopData> depOn = hopData.getDependencies();
                List<StitchingHopData> depOnThis = hopData.getDependingOnThis();
                Hop hop = hopsByUrn.get(hopData.getHopUrn());
                assert hop != null;

                List<Hop> hopDepOn = new ArrayList<>();
                List<Hop> hopDepOnThis = new ArrayList<>();
                for (StitchingHopData shd : depOn) {
                    Hop h = hopsByUrn.get(shd.getHopUrn());
                    assert h != null;
                    hopDepOn.add(h);
                }
                for (StitchingHopData shd : depOnThis) {
                    Hop h = hopsByUrn.get(shd.getHopUrn());
                    assert h != null;
                    hopDepOnThis.add(h);
                }

                Hop newHop = new Hop(hop, hopDepOn, hopDepOnThis);

                if (!res.containsKey(hop.getLinkName())) {
                    res.put(hop.getLinkName(), new ArrayList<>());
                }

                res.get(newHop.getLinkName()).add(newHop);
            }
        }

        return res;
    }

    /**
     * ready hops and hops waiting for deps. not hops that have returned manifest.
     */
    public synchronized List<Server> getHopsLeft() {
        assert computePathResult != null : "Call setComputePathResult first";
        assert computePathResultStitchingData != null : "Call setComputePathResult first";

        return computePathResultStitchingData.getOrderedStitchingCallDataList().stream()
                .filter(stitchingCallData -> !stitchingCallData.isAllocated()
                        && !stitchingCallData.getServer().isEdgeVlan())
                .map(StitchingCallData::getServer)
                .collect(Collectors.toList());
    }

    public static boolean isUnavailableVlan(AbstractGeniAggregateManager.AggregateManagerReply reply) {
        if (reply == null)
            return false;
        if (reply.getGeniResponseCode() == null)
            return false;
        if (reply.getOutput() == null)
            return false;

        if (Objects.equals(reply.getGeniResponseCode(), GeniAMResponseCode.GENIRESPONSE_VLAN_UNAVAILABLE))
            return true;

        //AMv3 Sliver Info
        if (reply.getValue() != null && (reply.getValue() instanceof AggregateManager3.AllocateAndProvisionInfo || reply.getValue() instanceof AggregateManager3.StatusInfo)) {
            List<AggregateManager3.SliverInfo> sliverInfos;
            if (reply.getValue() instanceof AggregateManager3.AllocateAndProvisionInfo) {
                sliverInfos = ((AggregateManager3.AllocateAndProvisionInfo) reply.getValue()).getSliverInfo();
            } else {
                assert reply.getValue() instanceof AggregateManager3.StatusInfo;
                sliverInfos = ((AggregateManager3.StatusInfo) reply.getValue()).getSliverInfo();
            }
            if (sliverInfos != null)
                for (AggregateManager3.SliverInfo si : sliverInfos)
                    if (si.getOperationalStatus() == null || si.getOperationalStatus().equals("geni_failed")) {
                        if (si.getError() != null && si.getError().toLowerCase().matches("vlan [0-9]* is unavailable"))
                            return true;
                        if (si.getError() != null && si.getError().toLowerCase().matches("VLAN PCE(PCE_CREATE_FAILED): 'There are no VLANs available on link .* on reservation .* in VLAN PCE'".toLowerCase()))
                            return true;
                    }
        }
        //AMv2 sliverstatus
        if (reply.getGeniResponseCode().isSuccess() && reply.getValue() != null && reply.getValue() instanceof AggregateManager2.SliverStatus) {
            AggregateManager2.SliverStatus status = (AggregateManager2.SliverStatus) reply.getValue();
            if (status.getStatus().equalsIgnoreCase("failed") || status.getStatus().equalsIgnoreCase("fail"))
                for (AggregateManager2.SliverStatus.ResourceStatus rs : status.getResources()) {
                    String error = rs.getError() == null ? null : rs.getError().toLowerCase();
                    if (error != null && error.matches("vlan [0-9]* is unavailable")) return true;
                    if (error != null && error.matches(".*There are no VLANs available on link .* on reservation .* in VLAN PCE.*".toLowerCase()))
                        return true;
                }
            return false;
        }

        if (reply.getGeniResponseCode().equals(GeniAMResponseCode.GENIRESPONSE_BADARGS)
                || reply.getGeniResponseCode().equals(GeniAMResponseCode.GENIRESPONSE_ERROR)) {
            String output = reply.getOutput().toLowerCase();

            //Some AM's, like emulab, add a string value when an error occurs. We include that in this search as well.
            if (reply.getValue() != null && reply.getValue() instanceof String)
                output += ((String) reply.getValue()).toLowerCase();
            else {
                if (reply.getRawValue() != null && reply.getRawValue() instanceof String)
                    output += ((String) reply.getRawValue()).toLowerCase();
            }

            if (output.startsWith("vlan tag") && output.endsWith("not available"))
                return true;
            if (output.startsWith("Exception: requested VLAN unavailable".toLowerCase()))
                return true;
            if (output.contains("requested VLAN unavailable".toLowerCase()))
                return true;
            if (output.contains("Error in building the dependency tree, probably not available vlan path OR trying to reuse a stitching tag".toLowerCase())) //exogeni
                return true;
        }

        return false;
    }

    private static final Pattern UNAVAILABLE_VLAN_PATTERN_1 = Pattern.compile("vlan tag ([0-9]*) for .* not available");
    //example: Exception: requested VLAN unavailable: sdn-sw.newy32aoa.net.internet2.edu,eth7/2 VLAN=1717
    private static final Pattern UNAVAILABLE_VLAN_PATTERN_2 =
            Pattern.compile(".*requested VLAN unavailable.* VLAN=([0-9]*)".toLowerCase());

    /**
     * @return null if not detected which vlan is unsupported. The unsupported vlan otherwise.
     */
    public static Integer getUnavailableVlan(AbstractGeniAggregateManager.AggregateManagerReply reply) {
        return getUnavailableVlan(reply, null);
    }

    public static Integer getUnavailableVlan(AbstractGeniAggregateManager.AggregateManagerReply reply, org.slf4j.Logger log) {
        if (log == null)
            log = LoggerFactory.getLogger(StitchingDirector.class);
        if (reply == null)
            return null;
        if (reply.getOutput() == null)
            return null;

        Matcher matchingMatcher = null;

        Matcher m1 = UNAVAILABLE_VLAN_PATTERN_1.matcher(reply.getOutput().toLowerCase());
        if (m1.matches())
            matchingMatcher = m1;

        if (matchingMatcher == null) {
            Matcher m2 = UNAVAILABLE_VLAN_PATTERN_2.matcher(reply.getOutput().toLowerCase());
            if (m2.matches())
                matchingMatcher = m2;
        }

        if (matchingMatcher != null) {
            String vlanPart = matchingMatcher.group(1);
            log.trace("detected that vlan \"" + vlanPart + "\" is unavailable.");
            try {
                return Integer.parseInt(vlanPart);
            } catch (NumberFormatException e) {
                log.warn("Could not find vlan in returned error output \"" + reply.getOutput() + "\" (because the found \"" + vlanPart + " is not an integer\")");
                return null;
            }
        } else {
            log.warn("Could not find vlan in returned error output \"" + reply.getOutput() + "\"");
            return null;
        }

//        String vlanPart = reply.getOutput().replaceAll("vlan tag ([0-9]*) for .* not available", "$1");
//        if (vlanPart != null && !vlanPart.isEmpty()) {
//            LOG.trace("detected that vlan \"" + vlanPart + "\" is unavailable.");
//            try {
//                return Integer.parseInt(vlanPart);
//            } catch (NumberFormatException e) {
//                return null;
//            }
//        } else {
//            LOG.warn("Could not find vlan in returned error output \"" + reply.getOutput() + "\" (found \"" + vlanPart + "\"). ");
//            return null;
//        }
    }

    public static boolean isAnyNotSupported(AbstractGeniAggregateManager.AggregateManagerReply reply) {
        if (reply == null)
            return false;
        if (reply.getGeniResponseCode() == null)
            return false;
        if (reply.getOutput() == null)
            return false;

        if (reply.getOutput().equalsIgnoreCase("vlan range any is invalid"))
            return true;

        if (reply.getOutput().contains("vlan") && reply.getOutput().contains("any"))
            return true;

        //short text with any in it most of the time means vlan any is not supported
        return !reply.getGeniResponseCode().isSuccess() && reply.getOutput() != null && reply.getOutput().contains("any") && reply.getOutput().length() < 100;
    }


    /**
     * Report that all slivers on an authority where deleted.
     * This may have occured due to:
     * - a StitchAction request, so the director asked for it itself
     * - some failure in Provision or Status, or other that caused the caller to decide to Delete
     * <p/>
     * The director will take the delete into account in both cases, and will modify the stitchActions if needed.
     */
    public synchronized void processDeleteResult(@Nonnull Server auth) throws JFedException {
        log.debug("processDeleteResult(" + auth.getDefaultComponentManagerUrn() + ")");

        //re-allocate all that is needed to be restarted as a result of this Delete.
        //  obviously, this auth must be re-allocated, but dependencies might need to be deleted and reallocated as well.
        //  if the auth was in a state waiting for deletion, the delete will add an allocate to the stitchActions
        //  if the auth was allocated and had dependencies, the delete will first add deletes for the dependencies to the stitchActions

        //find StitchingCallData related to auth
        StitchingCallData authStitchingCallData = null;
        for (StitchingCallData curStitchingCallData : computePathResultStitchingData.getOrderedStitchingCallDataList()) {
            if (Objects.equals(curStitchingCallData.getServer(), auth)) {
                authStitchingCallData = curStitchingCallData;
                break;
            }
        }
        assert authStitchingCallData != null;

        assert gaveUp || authStitchingCallData.getState() == StitchingCallData.State.DELETING;

        //set MUST_DELETE on all that depend on it
        deleteDeps(authStitchingCallData);

        //since it is deleted, mark it as not allocated but "allocatable if deps allocated".
        authStitchingCallData.setState(StitchingCallData.State.NONE);
    }

    /**
     * Report the result of a ListResources call
     * <p/>
     * The director will take this result into account, and you should call getStitchActions() after reporting this to
     * see if there are new actions to be done.
     */
    public synchronized void processListResourcesResult(@Nonnull Server auth, @Nullable String advertisementRspec) {
        log.debug("processListResourcesResult(" + auth.getDefaultComponentManagerUrn() + ", rspec.length=" + (
                advertisementRspec == null ? null
                        : advertisementRspec.length()) + ")");

        //re-allocate all that is needed to be restarted as a result of this Delete.
        //  obviously, this auth must be re-allocated, but dependencies might need to be deleted and reallocated as well.
        //  if the auth was in a state waiting for deletion, the delete will add an allocate to the stitchActions
        //  if the auth was allocated and had dependencies, the delete will first add deletes for the dependencies to the stitchActions

        //find StitchingCallData related to auth
        StitchingCallData authStitchingCallData = null;
        for (StitchingCallData curStitchingCallData : computePathResultStitchingData.getOrderedStitchingCallDataList()) {
            if (Objects.equals(curStitchingCallData.getServer(), auth)) {
                authStitchingCallData = curStitchingCallData;
                break;
            }
        }
        assert authStitchingCallData != null;

        if (authStitchingCallData.getState() == StitchingCallData.State.LISTING_RESOURCES) {
            authStitchingCallData.setState(StitchingCallData.State.NONE);
        }

        authStitchingCallData.seeAdvertismentRspec(advertisementRspec);
    }

    private void deleteDeps(StitchingCallData target) throws JFedException {
        int safety = 0;
        Deque<StitchingCallData> dependingOnThis = new LinkedList<>(target.getDependingOnThis());
        while (!dependingOnThis.isEmpty()) {
            StitchingCallData depStitchingCallData = dependingOnThis.removeFirst();
            if (depStitchingCallData.getState() == StitchingCallData.State.ALLOCATED) {
                depStitchingCallData.setState(StitchingCallData.State.MUST_DELETE);
            }
            if (depStitchingCallData.getState() == StitchingCallData.State.ALLOCATING) {
                depStitchingCallData.setState(StitchingCallData.State.ALLOCATING_BUT_MUST_DELETE);
            }

            //recurse
            dependingOnThis.addAll(depStitchingCallData.getDependingOnThis());

            if (safety++ > 50)
                throw new JFedException("Detected possible infinite loop! (looped " + safety + " times)" + dependingOnThis);
        }
    }

    public synchronized void reportGiveup() {
        gaveUp = true;
    }

    /**
     * Report to the director that you have waited after receiving an AllocateVerdict.CALLER_DELAYED_RETRY,
     * and want to receive new StitchActions for this authority.
     */
    public synchronized AllocateVerdict reportReadyForRetry(Server auth) {
        return reportMustDelete(auth, false, true);
    }

    /**
     * Report to the director that a delete is needed on a certain AM.
     * This might be caused by provision or Status failure.
     * <p/>
     * It might be known whether or not the VLANs are unavailable.
     * <p/>
     * The director will change the getStitchActions() result after this call,
     * to request the actual delete. IT will also request the delete of other hops if needed.
     * And it will eventually request allocate retries.
     * <p/>
     * This returns an AllocateVerdict. The meaning is slightly different for some values:
     * - GIVEUP: same as always, occurs when no VLANs are left
     * - CALLER_DELAYED_RETRY: never returned
     * - DIRECTOR_ACTION: returned when director want to continue. You need to call getStitchActions() after this
     * - OK: never returned.
     *
     * @param vlansUnavailable        -
     * @param forceAllocateFinished set to true if this is called instead of processAllocateResult, so if the allocate finished but failed and has to be deleted to be sure.
     */
    public synchronized AllocateVerdict reportMustDelete(Server auth, boolean vlansUnavailable, boolean forceAllocateFinished) {
        log.debug("reportMustDelete(" + auth.getDefaultComponentManagerUrn() + ", " + vlansUnavailable + ", " + forceAllocateFinished + ")");

        //no special case! The choice of which AM to use has already been made an should not be redone here.
//        if (auth.getDefaultComponentManagerAsGeniUrn().getTopLevelAuthority_withoutSubAuth().equals("exogeni.net")) {
//            //special case for exogeni: always use exoSM when stitching
//            auth = authFinder.findByUrn(auth.getDefaultComponentManagerAsGeniUrn(), AuthorityFinder.Purpose.STITCHING);
//        }

        assert auth.getDefaultComponentManagerUrn() != null;
        StitchingCallData stitchingCallData =
                computePathResultStitchingData.getStitchingCallData(auth.getDefaultComponentManagerUrn());
        //assert stitchingCallData != null;
        if (stitchingCallData == null) {
            log.warn("Unexpected: no stitchingCallData found for " + auth.getDefaultComponentManagerUrn() + " (is exogeni cause?). Doing fallback: ignoring the mustDelete.");
            return AllocateVerdict.DIRECTOR_ACTION;
        }

        log.debug("reportMustDelete auth state=" + stitchingCallData.getState());

        if (stitchingCallData.getState() == StitchingCallData.State.DELETING) {
            log.debug("reportMustDelete return DIRECTOR_ACTION -> already deleting: nothing to do");
            return AllocateVerdict.DIRECTOR_ACTION; //could be OK as "NONE", but that would complicate things. DIRECTOR_ACTION is fine too
        }

        assert stitchingCallData.getState() == StitchingCallData.State.ALLOCATED
                || stitchingCallData.getState() == StitchingCallData.State.ALLOCATING :
                "reportMustDelete Unexpected state: " + stitchingCallData.getState();

        if (forceAllocateFinished) {
            assert stitchingCallData.getState() == StitchingCallData.State.ALLOCATING :
                    "reportMustDelete forceAllocateFinished=true Unexpected state: " + stitchingCallData.getState();
            if (stitchingCallData.getState() == StitchingCallData.State.ALLOCATING)
                stitchingCallData.setState(StitchingCallData.State.MUST_DELETE);
        } else {
            if (stitchingCallData.getState() == StitchingCallData.State.ALLOCATING)
                stitchingCallData.setState(StitchingCallData.State.ALLOCATING_BUT_MUST_DELETE);
            else
                stitchingCallData.setState(StitchingCallData.State.MUST_DELETE);
        }
        if (vlansUnavailable) {
            Set<Integer> vlansUsed = new HashSet<>();
            for (StitchingHopData hopData : stitchingCallData.getAllHopData()) {
                log.debug("reportMustDelete  searches vlans in use at hop " + hopData.getHopUrn() + ": " + hopData.getLastManifestVlan());
                Integer vlan = hopData.getLastManifestVlan();
                if (vlan != null) vlansUsed.add(vlan);
            }
            log.debug("reportMustDelete will mark the following vlans as unavailable: " + vlansUsed);
            for (int vlan : vlansUsed) {
                boolean vlansLeft = stitchingCallData.setVlanUnavailable(vlan);
                if (!vlansLeft) {
                    gaveUp = true;
                    log.debug("reportMustDelete return GIVEUP (no more vlans)");
                    return AllocateVerdict.GIVEUP;
                }
            }
        }

        //recursively mark deps for delete and retry
        if (!stitchingCallData.getDependingOnThis().isEmpty()) {
            log.debug("reportMustDelete DEPDEL: CreateSliver changed VLAN for " + stitchingCallData.getAuthUrnString() + ", " +
                    "but other hops depends on this one. Forcing redo for these.");
            //get all dependencies recursively
            Set<StitchingCallData> allDeps = new HashSet<>();
            Deque<StitchingCallData> newDeps =
                    new LinkedList<>(stitchingCallData.getDependingOnThis());
            while (!newDeps.isEmpty()) {
                StitchingCallData scd = newDeps.removeFirst();
                allDeps.add(scd);
                for (StitchingCallData dep : scd.getDependingOnThis()) {
                    if (!allDeps.contains(dep))
                        newDeps.addLast(dep);
                }
            }

            log.debug("reportMustDelete DEPDEL:     allDeps.size()=" + allDeps.size());

            for (StitchingCallData dep : allDeps) {
                if (dep.isAllocated()) {
                    log.debug("reportMustDelete DEPDEL:      forcing delete on " + dep.getAuthUrnString());
                    dep.setState(StitchingCallData.State.MUST_DELETE);
                }
                if (dep.getState() == StitchingCallData.State.ALLOCATING) {
                    log.debug("reportMustDelete DEPDEL:      forcing delete after allocate on " + dep.getAuthUrnString());
                    dep.setState(StitchingCallData.State.ALLOCATING_BUT_MUST_DELETE);
                }
            }
        }

        log.debug("reportMustDelete return DIRECTOR_ACTION - new auth state=" + stitchingCallData.getState());
        return AllocateVerdict.DIRECTOR_ACTION;
    }

    public enum AllocateVerdict {
        /**
         * The StitchingDirector has determined that the entire stitching will never succeed.
         * When receiving GIVEUP the called should abort all calls and delete all slivers on all authorities to clean up.
         */
        GIVEUP,

        /**
         * The call was a failure for unknown reasons. It might work by trying again.
         * It is advisable to wait some time between these retries and to only do a limited number (it can be an infinite loop otherwise).
         * If the caller does not want to retry, or has already done to many retries, the caller should do the same as if GIVEUP was returned.
         */
        CALLER_DELAYED_RETRY,

        /**
         * The call was not successful, and the StitchingDirector will handle this itself, by changing the StitchActions.
         * The caller should continue by requesting the StitchActions.
         * <p/>
         * (Note that any retry needed because of this, never requires waiting between calls (unlike CALLER_DELAYED_RETRY).)
         */
        DIRECTOR_ACTION,

        /**
         * The call was successful and the StitchingDirector is done with this authority.
         * The caller may Provision the sliver, and requests its Status. (or may wait for all to be ready to do so)
         */
        OK
    }

    /**
     * returns true if failure.
     * returns false if success, or if director wants to try again.
     */
    public synchronized AllocateVerdict processAllocateResult(@Nonnull StitchAction hop,
                                                              AbstractGeniAggregateManager.AggregateManagerReply/*<String> or <AggregateManager3.AllocateAndProvisionInfo>*/ reply) throws JFedException {

        if (reply == null) {
            log.warn("Didn't get a reply. Giving up.");
            gaveUp = true;
        }

        if (gaveUp) {
            log.warn("processAllocateResult called when already gave up.");
            hop.stitchingCallData.setState(StitchingCallData.State.MUST_DELETE);
            return AllocateVerdict.GIVEUP;
        }

        log.debug("processAllocateResult with hop=" + hop + " hop.vlans=" + hop.vlans + " reply.code=" + reply.getGeniResponseCode() + " reply.output=" + reply.getOutput());


        //don't care if it's failure or not: it is involved now!
        if (!activelyInvolvedAuthorities.contains(hop.getServer()))
            activelyInvolvedAuthorities.add(hop.getServer());

        //determine answer type
        boolean vlanNotAvailable = isUnavailableVlan(reply);

        boolean success = reply.getGeniResponseCode().isSuccess();

        boolean anyNotSupported = isAnyNotSupported(reply);

        boolean permanentFailure = isUnrecoverableError(reply);

        boolean otherFailure = !success && !anyNotSupported && !vlanNotAvailable && !permanentFailure;

        //exactly 1 should be true, the rest should be false
        assert (vlanNotAvailable ? 1 : 0) + (anyNotSupported ? 1 : 0) + (success ? 1 : 0) + (otherFailure ? 1 : 0) + (
                permanentFailure ? 1 : 0) == 1;

        assert !hop.stitchingCallData.isAllocated();
        assert hop.stitchingCallData.getState() != StitchingCallData.State.NONE;
        assert hop.stitchingCallData.getState() != StitchingCallData.State.LISTING_RESOURCES;
        assert hop.stitchingCallData.getState() != StitchingCallData.State.MUST_DELETE;

        if (hop.stitchingCallData.getState() == StitchingCallData.State.ALLOCATING_BUT_MUST_DELETE) {
            log.warn("ALLOCATING_BUT_MUST_DELETE hop=" + hop);

            hop.stitchingCallData.setState(StitchingCallData.State.MUST_DELETE);

            if (vlanNotAvailable) {
                Integer unsupportedVlan = getUnavailableVlan(reply, log);
                if (unsupportedVlan != null)
                    hop.stitchingCallData.setVlanUnavailable(unsupportedVlan);
            }

            if (anyNotSupported)
                try {
                    aggUrnsNotSupportingAny.add(new GeniUrn(hop.getServer().getDefaultComponentManagerUrn()));
                } catch (GeniUrn.GeniUrnParseException e) {
                    throw new JFedException("Failed to parse server defaultComponentManagerUrn: \"" + hop.getServer().getDefaultComponentManagerUrn() + "\"", e);
                }

            //should not be needed, but better be safe
            deleteDeps(hop.stitchingCallData);

            return AllocateVerdict.DIRECTOR_ACTION;
        }

        assert hop.stitchingCallData.getState() == StitchingCallData.State.ALLOCATING :
                "Illegal state: " + hop.stitchingCallData.getState();

//        if (!hop.stitchingCallData.isReady()) { //old before ALLOCATING was added
        if (!hop.stitchingCallData.areAllDepsReady()) {
            //Note: don't replace this by doing this exceptional case only for success!
            //  that is probably not a good idea, and dangerous,
            //  because a permanent failure might be detected in some cases, that is not really one.
            //    (example: the vlan(s) available here may have limited (to one) by a dependency which needs to be re-done)
            log.warn("This allocate cannot be used. The dependencies are no longer met (probably due to some other allocate failing while this one was in progress). " +
                    "It will have to be deleted (and probably tried again later) hop=" + hop);

            if (success)
                hop.stitchingCallData.setState(StitchingCallData.State.MUST_DELETE);
            else
                hop.stitchingCallData.setState(StitchingCallData.State.NONE);

            if (vlanNotAvailable) {
                Integer unsupportedVlan = getUnavailableVlan(reply, log);
                if (unsupportedVlan != null)
                    hop.stitchingCallData.setVlanUnavailable(unsupportedVlan);
            }

            if (anyNotSupported)
                try {
                    aggUrnsNotSupportingAny.add(new GeniUrn(hop.getServer().getDefaultComponentManagerUrn()));
                } catch (GeniUrn.GeniUrnParseException e) {
                    throw new JFedException("Failed to parse server defaultComponentManagerUrn: \"" + hop.getServer().getDefaultComponentManagerUrn() + "\"", e);
                }

            //should not be needed, but better be safe
            deleteDeps(hop.stitchingCallData);

            return AllocateVerdict.DIRECTOR_ACTION;
        }

        assert hop.stitchingCallData.getState() == StitchingCallData.State.ALLOCATING :
                "hop is in unsupported state: " + hop.stitchingCallData.getState();

        if (permanentFailure) {
            log.debug("Detected error that cannot be handled in CreateSliver and cannot be fixed with retry. Give up.");
            gaveUp = true;
            hop.stitchingCallData.setState(StitchingCallData.State.NONE);
            return AllocateVerdict.GIVEUP;
        }

        if (anyNotSupported) {
            //aggregate does not support "any" as suggestedVlan
            try {
                aggUrnsNotSupportingAny.add(new GeniUrn(hop.getServer().getDefaultComponentManagerUrn()));
            } catch (GeniUrn.GeniUrnParseException e) {
                throw new JFedException("Failed to parse server defaultComponentManagerUrn: \"" + hop.getServer().getDefaultComponentManagerUrn() + "\"", e);
            }
            log.info("VLAN \"any\" was not supported on aggregate \"" + hop.getServer().getDefaultComponentManagerUrn() + "\"");
            hop.stitchingCallData.setState(StitchingCallData.State.NONE);
            return AllocateVerdict.DIRECTOR_ACTION;
        }

        if (vlanNotAvailable) {

            /*
             * Special case:   geni error code 1 '1 Bad Arguments: malformed arguments'  with output like 'vlan tag 3748 for 'link0' not available'
             *
             * Solution try again with different VLAN!
             */

            if (AUTOMATIC_NEXT_SUGGESTED_VLAN) {
                log.debug("detected error \"vlan tag ... not available\" will try to pick other VLAN automatically. hop.vlans=" + hop.vlans);

                //only try 5 different vlans
                if (hop.stitchingCallData.getUnavailableVlans().size() > MAX_UNAVAILABLE_VLANS_BEFORE_GIVEUP) {
                    log.debug("error \"vlan tag ... not available\" received too many times. Giving up.   hop.vlans=" + hop.vlans + "  unavailable=" + hop.stitchingCallData.getUnavailableVlans());
                    gaveUp = true;
                    hop.stitchingCallData.setState(StitchingCallData.State.NONE);
                    return AllocateVerdict.GIVEUP;
                }

                boolean vlansLeft;
                Integer unsupportedVlan = getUnavailableVlan(reply, log);
                if (unsupportedVlan != null) {
                    vlansLeft = hop.stitchingCallData.setVlanUnavailable(unsupportedVlan);
                } else {
                    log.warn("Could not find vlan in returned error output \"" + reply.getOutput() + "\" " +
                            "Will mark all used vlans as unavailable as fallback: " + hop.vlans);

                    assert hop.vlans != null;
                    assert !hop.vlans.isEmpty();

                    vlansLeft = true;

                    for (int vlan : hop.vlans)
                        vlansLeft = hop.stitchingCallData.setVlanUnavailable(vlan) && vlansLeft;
                }
                if (vlansLeft && hop.stitchingCallData.getDependencies().isEmpty()) {
                    hop.stitchingCallData.setState(StitchingCallData.State.NONE);
                    return AllocateVerdict.DIRECTOR_ACTION;
                } else {
                    //no available vlan's left. But perhaps the available are forced due to dependencies, in which case we can go up the dep tree instead.
                    if (!hop.stitchingCallData.getDependencies().isEmpty()) {
                        log.debug("DEPDEL: No alternative vlans are left for " + hop.stitchingCallData.getAuthUrnString() + ", " +
                                "but hop depends on others. Checking to redo these.");
                        //get all dependencies recursively
                        Set<StitchingCallData> allDeps = new HashSet<>();
                        Deque<StitchingCallData> newDeps =
                                new LinkedList<>(hop.stitchingCallData.getDependencies());
                        while (!newDeps.isEmpty()) {
                            StitchingCallData scd = newDeps.removeFirst();
                            allDeps.add(scd);
                            for (StitchingCallData dep : scd.getDependencies()) {
                                if (!allDeps.contains(dep))
                                    newDeps.addLast(dep);
                            }
                        }

                        log.debug("DEPDEL:     allDeps.size()=" + allDeps.size());

                        assert unsupportedVlan != null; //TODO support other case

                        boolean depHasVlansLeft = true;
                        for (StitchingCallData dep : allDeps) {
                            log.debug("DEPDEL:     marking " + unsupportedVlan + " as unavailable on " + dep.getAuthUrnString());
                            boolean hasVlanLeft = dep.setVlanUnavailable(unsupportedVlan);
                            if (!hasVlanLeft) {
                                depHasVlansLeft = false;
                                log.debug("DEPDEL:       no vlans left on " + dep.getAuthUrnString());
                            } else {
                                if (dep.isAllocated()) {
                                    log.debug("DEPDEL:       need to delete on " + dep.getAuthUrnString());
                                    if (dep.getState() == StitchingCallData.State.ALLOCATING)
                                        dep.setState(StitchingCallData.State.ALLOCATING_BUT_MUST_DELETE);
                                    else
                                        dep.setState(StitchingCallData.State.MUST_DELETE);
                                }
                            }
                        }
                        log.warn("DEPDEL:     depHasVlansLeft=" + depHasVlansLeft);

                        if (depHasVlansLeft) {
                            hop.stitchingCallData.setState(StitchingCallData.State.NONE);
                            return AllocateVerdict.DIRECTOR_ACTION;
                        } else {
                            gaveUp = true;
                            hop.stitchingCallData.setState(StitchingCallData.State.NONE);
                            return AllocateVerdict.GIVEUP;
                        }
                    }

                    log.warn("The vlan was not available according to CreateSliver. " +
                            "No alternative vlans are left for " + hop.stitchingCallData.getAuthUrnString() + ", " +
                            "so giving up on stitching.");
                    gaveUp = true;
                    hop.stitchingCallData.setState(StitchingCallData.State.NONE);
                    return AllocateVerdict.GIVEUP;
                }
            } else {
                log.debug("error \"vlan tag ... not available\" received while setting AUTOMATIC_NEXT_SUGGESTED_VLAN=false. hop.vlans=" + hop.vlans);
                gaveUp = true;
                hop.stitchingCallData.setState(StitchingCallData.State.NONE);
                return AllocateVerdict.GIVEUP;
            }
        }

        if (success) {
            log.debug("Allocate call successful - processing manifest");
            Object replyValue = reply.getValue();
            String manifestRspec = null;
            if (replyValue instanceof AggregateManager3.AllocateAndProvisionInfo)
                manifestRspec = ((AggregateManager3.AllocateAndProvisionInfo) replyValue).getRspec();
            if (replyValue instanceof String)
                manifestRspec = (String) replyValue;
            if (manifestRspec == null) {
                log.debug("Detected error that cannot be handled in CreateSliver (unknown reply value, type=" + replyValue.getClass().getName() + "). Maybe retry? Otherwise, give up.");
                assert hop.stitchingCallData.getState() == StitchingCallData.State.ALLOCATING;
                return AllocateVerdict.CALLER_DELAYED_RETRY;
            }
            GeniUrn serverUrn;
            try {
                serverUrn = new GeniUrn(hop.getServer().getDefaultComponentManagerUrn());
            } catch (GeniUrn.GeniUrnParseException e) {
                throw new JFedException("Failed to parse server defaultComponentManagerUrn: \"" + hop.getServer().getDefaultComponentManagerUrn() + "\"", e);
            }
            if (serverUrn.getEncodedTopLevelAuthority_withoutSubAuth().equalsIgnoreCase("exogeni.net")) {
                //very dirty workaround for bugs on exogeni.net
                List<Integer> vlans = hop.stitchingCallData.getSuggestedVlans();
                assert vlans.size() == 1;
                hop.stitchingCallData.exogeniBugWorkAround_registerVlan(vlans.get(0));
            } else
                hop.stitchingCallData.processManifestRspec(manifestRspec);
            hop.stitchingCallData.setState(StitchingCallData.State.ALLOCATED);
            log.debug("Allocate call successful - state is ALLOCATED, returning OK");
            return AllocateVerdict.OK;
        }

        log.debug("Detected error that cannot be handled in CreateSliver. Maybe retry? Otherwise, give up.");
        //keep allocating   hop.stitchingCallData.setState(StitchingCallData.State.ALLOCATING);
        assert hop.stitchingCallData.getState() == StitchingCallData.State.ALLOCATING;
        return AllocateVerdict.CALLER_DELAYED_RETRY;
    }


    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    ///////////////////////////////////////////// Helper to detect error types /////////////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    /**
     * @param reply Reply to be checked for recoverable errors
     * @return null if unknown, true if known to be unrecoverable, false if known to be possibly recoverable, or success, or unknown but likely recoverable
     */
    public static Boolean isUnrecoverableError(SfaApiCallReply reply) {
        if (reply == null) return null;
        if (reply.getGeniResponseCode() == null) return null;

        if (Objects.equals(reply.getGeniResponseCode(), GeniAMResponseCode.GENIRESPONSE_ERROR) &&
                reply.getOutput() != null &&
                reply.getOutput().startsWith("Could not reserve vlan tags for "))
            return true; //see FEDIBBTDEV-919

        if (!reply.getGeniResponseCode().isSuccess() &&
                reply.getOutput() != null &&
                reply.getOutput().startsWith("TEST BUG DETECTED"))
            return true; //this fatal error only occurs during testing

        //AMv3 Sliver Info
        if (reply.getValue() != null && (reply.getValue() instanceof AggregateManager3.AllocateAndProvisionInfo || reply.getValue() instanceof AggregateManager3.StatusInfo)) {
            List<AggregateManager3.SliverInfo> sliverInfos;
            if (reply.getValue() instanceof AggregateManager3.AllocateAndProvisionInfo) {
                sliverInfos = ((AggregateManager3.AllocateAndProvisionInfo) reply.getValue()).getSliverInfo();
            } else {
                assert reply.getValue() instanceof AggregateManager3.StatusInfo;
                sliverInfos = ((AggregateManager3.StatusInfo) reply.getValue()).getSliverInfo();
            }
            if (sliverInfos != null)
                for (AggregateManager3.SliverInfo si : sliverInfos)
                    if (si.getOperationalStatus() == null || si.getOperationalStatus().equals("geni_failed")) {
                        if (si.getError() != null && si.getError().toLowerCase().contains("bandwidth")) return true;
                        if (si.getError() != null && si.getError().toLowerCase().contains("bandwith")) return true;
                        if (si.getError() != null && si.getError().toLowerCase().contains("There are no VLANs available on link".toLowerCase()))
                            return true;
                    }
        }
        //AMv2 sliverstatus
        if (reply.getGeniResponseCode().isSuccess() && reply.getValue() != null && reply.getValue() instanceof AggregateManager2.SliverStatus) {
            AggregateManager2.SliverStatus status = (AggregateManager2.SliverStatus) reply.getValue();

            for (AggregateManager2.SliverStatus.ResourceStatus rs : status.getResources()) {
                if (rs.getError() != null && rs.getError().toLowerCase().contains("bandwidth")) return true;
                if (rs.getError() != null && rs.getError().toLowerCase().contains("bandwith"))
                    return true; //not sure if I ever saw this misspelling in an error reply
            }
            return false;
        }

        if (reply.getGeniResponseCode().isSuccess()) return false;

        String output = "";
        if (reply.getOutput() != null) output += reply.getOutput().toLowerCase();

        //Some AM's, like emulab, add a string value when an error occurs. We include that in this search as well.
        if (reply.getValue() != null && reply.getValue() instanceof String)
            output += " " + ((String) reply.getValue()).toLowerCase();
        else {
            if (reply instanceof AbstractGeniAggregateManager.AggregateManagerReply) {
                AbstractGeniAggregateManager.AggregateManagerReply amReply =
                        (AbstractGeniAggregateManager.AggregateManagerReply) reply;
                if (amReply.getRawValue() != null && amReply.getRawValue() instanceof String)
                    output += " " + ((String) amReply.getRawValue()).toLowerCase();
            }
        }

        if (output.isEmpty()) return null;

        if (output.contains("Could not map to resources: Not enough bandwidth to connect some nodes".toLowerCase()))
            return true; //FEDIBBTDEV-1412
        if (output.contains("could not map to resources")) return true;
        if (output.contains("not enough bandwidth")) return true;
        if (output.contains("mapping failed")) return true;
        if (output.contains("bandwidth")) return true;
        if (output.contains("bandwith")) return true; //not sure if I ever saw this misspelling in an error reply
        if (reply.getOutput() != null && Objects.equals(reply.getOutput(), "Could not verify topo"))
            return true; //FEDIBBTDEV-1453 (seen at wall2)

        return false; /*probably recoverable*/
    }

    public static Boolean isUnrecoverableError(JFedException e) {
        if (e == null)
            return null;

        //in case of timeout, the underlying AbstractApi will already have retried, so this error is fatal.
        //we use AbstractApi's own detection to see if this is the case
        if (AbstractApi.isTimeout(e)) return true;

        SfaApiCallReply reply = e.getSfaReply();
        if (reply == null)
            return null;

        return isUnrecoverableError(reply);
    }

    /*
    * Example of unrecoverable call AMv2 Status reply:
    {
    "output": "",
    "geni_api": 2,
    "code": {
                 "geni_code": 0,
                 "am_type": "sfa",
                 "am_code": ""
                 },
    "value": {
                  "geni_urn": "urn:publicid:IDN+wall2.ilabt.iminds.be:bvermeul+slice+st110",
                  "geni_resources": [
                         {
                            "geni_urn": "urn:publicid:IDN+wall2.ilabt.iminds.be:bvermeul+slice+st110_vlan_ion.internet2.edu-91961",
                            "geni_error": "Bandwidth PCE(PCE_CREATE_FAILED): 'Unable to find path because the maximum bandwidth of ion.internet2.edu:rtr.hous:ge-1/2/4 has been exceeded. 150.0 Mbps is available and 800 Mbps was requested  on reservation ion.internet2.edu-91961 in Bandwidth PCE'",
                            "geni_status": "failed"
                            }
                         ],
                  "geni_expires": "2014-05-25T12:28:25.000Z",
                  "geni_status": "failed"
                  }
    } */

    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    /**
     * List of all authorities that are or have been already involved somehow. Mostly useful for calling Delete
     */
    public synchronized List<Server> getActivelyInvolvedAuthorities() {
        return new ArrayList<>(activelyInvolvedAuthorities);
    }

    /**
     * List of all authorities that are, have been or will be involved.
     */
    public synchronized List<Server> getAllAuthorities() {
        return new ArrayList<>(allAuthorities);
    }

    public List<SuggestedAvailableOverview> getSuggestedAvailableOverviews() {
        List<SuggestedAvailableOverview> res = new ArrayList<>();

        if (computePathResultStitchingData != null)
            for (StitchingHopData stitchingHopData : computePathResultStitchingData.getStitchingHopDataList()) {
                SuggestedAvailableOverview o =
                        new SuggestedAvailableOverview(
                                stitchingHopData.getHopUrn(),
                                stitchingHopData.getSuggestedVlan() + "",
                                stitchingHopData.getAvailableVlansString()
                        );
                res.add(o);
            }

        return res;
    }

    public synchronized String getSuggestedAvailableOverviewsString() {
        String res = "";

        List<SuggestedAvailableOverview> overviews = getSuggestedAvailableOverviews();
        for (SuggestedAvailableOverview o : overviews) {
            res += o + "\n";
        }

        return res;
    }

    public static class SuggestedAvailableOverview {
        private final String hopUrn;
        private final String suggestedVlan;
        private final String availableVlans;

        public SuggestedAvailableOverview(String hopUrn, String suggestedVlan, String availableVlans) {
            this.hopUrn = hopUrn;
            this.suggestedVlan = suggestedVlan;
            this.availableVlans = availableVlans;
        }

        public String getHopUrn() {
            return hopUrn;
        }

        public String getSuggestedVlan() {
            return suggestedVlan;
        }

        public String getAvailableVlans() {
            return availableVlans;
        }

        @Override
        public String toString() {
            return hopUrn + " -> suggestedVlan='" + suggestedVlan + '\'' + ", availableVlans='" + availableVlans + '\'';
        }
    }

    public enum StichActionType {
        /**
         * An allocation is needed. The stitchAction object will contain the request rspec needed.
         */
        ALLOCATE,
        /**
         * A delete of all slivers on the authority  is needed.
         */
        DELETE,
        /**
         * ListResources on an AM, to get up to date available VLAN info from the advertisement RSpec.
         */
        LISTRESOURCES
    }

    /**
     * A "next step" or command that needs to be done for the stitching setup.
     * Multiple types are possible. Typically, a step is an Allocate. But it can be a Delete too.
     * <p/>
     * Each step is limited to one authority.
     */
    public static class StitchAction {
        private final StichActionType type;

        private final Server server;
        private final String requestRspec;
        private final StitchingCallData stitchingCallData; //not accessible outside of this class

        //List of all suggestedVlans for hops of this authority
        private List<Integer> vlans;

        public StitchAction(StichActionType type, Server server, String requestRspec, List<Integer> vlans, StitchingCallData stitchingCallData) {
            assert type != null;
            assert server != null;
            //assert stitchingCallData != null;

            this.type = type;
            this.server = server;
            this.requestRspec = requestRspec;
            this.stitchingCallData = stitchingCallData;
            this.vlans = vlans;
        }

        public Server getServer() {
            return server;
        }

        public String getRequestRspec() {
            return requestRspec;
        }

        @Override
        public String toString() {
            return "StitchAction{" +
                    "" + type +
                    ", vlans=" + vlans +
                    ", server=" + server.getId() + "(" + server.getName() + ")" +
                    '}';
        }

        public StichActionType getType() {
            return type;
        }
    }
}
