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

import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Server;
import be.iminds.ilabt.jfed.lowlevel.AbstractApi;
import be.iminds.ilabt.jfed.lowlevel.JFedException;
import be.iminds.ilabt.jfed.lowlevel.TestbedInfoSource;
import be.iminds.ilabt.jfed.lowlevel.authority.AuthorityFinder;
import be.iminds.ilabt.jfed.rspec.model.BasicStringRspec;
import be.iminds.ilabt.jfed.util.GeniUrn;
import be.iminds.ilabt.jfed.util.RFC3339Util;
import be.iminds.ilabt.jfed.util.XmlUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import javax.annotation.Nullable;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;

/**
 * StitchingData stores StitchingHopData and StitchingCallData
 * <p/>
 * It also knows the order in which AM's need to be called.
 * <p/>
 * It derives all data from the info returned from the SCS call:
 * - stitching extension of returned rspec
 * - workflow data
 */
public class StitchingData {
    private final org.slf4j.Logger log;

    public final static String NAMESPACE_STITCHING = "http://hpn.east.isi.edu/rspec/ext/stitch/0.1/";
    public final static String NAMESPACE_RSPEC = "http://www.geni.net/resources/rspec/3";

    private final List<StitchingHopData> stitchingHopDataList = new ArrayList<>();
    private final Map<String, StitchingCallData> stitchingCallDataByAuthUrn = new HashMap<>();

    private final List<StitchingCallData> orderedStitchingCallDataList = new ArrayList<>();
    private final List<Server> orderedSfaAuths = new ArrayList<>();

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

    private final String scsReplyRspec;

    public StitchingData(String scsReplyRspec, Map workflow, TestbedInfoSource testbedInfoSource, AuthorityFinder authFinder, Logger log) throws JFedException {
        if (log == null)
            log = LoggerFactory.getLogger(StitchingData.class);
        this.log = log;
        this.scsReplyRspec = scsReplyRspec;

        Element stitchingElement = extractStitchingElementFromRspec(scsReplyRspec, log);
        assert stitchingElement != null : "No stitching element in rspec";

        //then go over each hop in the workflow
        Map<String, Object> workflowData = AbstractApi.apiSpecifiesMapStringToObject(workflow);
        Map<String, String> aggUrnsToAggUrl =
                new HashMap<>(); //keep track of all unique aggregates
        for (Map.Entry<String, Object> linkEntry : workflowData.entrySet()) {
            final String linkName = linkEntry.getKey();
            final Map<String, Object> linkData = AbstractApi.apiSpecifiesMapStringToObject(linkEntry.getValue());
            List<Map> linkDeps = AbstractApi.apiSpecifiesListOfT(Map.class, linkData.get("dependencies"));

            Queue<Map> hops = new LinkedList<>();
            hops.addAll(linkDeps);
            while (!hops.isEmpty()) {
                Map<String,Object> hopHt = AbstractApi.apiSpecifiesMapStringToObject(hops.poll());

                if (hopHt.containsKey("dependencies")) {
                    List<Map> deps = AbstractApi.apiSpecifiesListOfT(Map.class, hopHt.get("dependencies"));
                    hops.addAll(deps);
                }

                //this is incorrect, if we do this here, stitching data gets removed from the rspec!
//                String aggregateUrn = (String) hopHt.get("aggregate_urn");
//                SfaAuthority hopAggAuth = authList.getFromAnyUrn(aggregateUrn);
//                if (aggregateUrn != null && hopAggAuth != null && hopAggAuth.getServerType().equals("fake")) {
//                    //ignore this hop because it is a fake hop used for adding networks
//                    continue;
//                }

                String aggregateUrn = AbstractApi.apiSpecifiesNullableString(hopHt.get("aggregate_urn"));


//                SfaAuthority hopAggAuth = authList.getByUrnExact(aggregateUrn); //same but with finder:
                Server hopServer = authFinder.findByUrn(aggregateUrn, AuthorityFinder.Purpose.STITCHING);
                if (hopServer == null) {
                    String aggregateUrlString = AbstractApi.apiSpecifiesStringInMap(hopHt,"aggregate_url");

                    //dirty exception to fix SCS error
                    if (Objects.equals(aggregateUrlString, "https://uvanl-hn.exogeni.net:11443/orca/xmlrpc")) {
                        aggregateUrlString = "https://uva-nl-hn.exogeni.net:11443/orca/xmlrpc";
                    }

                    //fallback to using URL
                    log.debug("Did not find authority \"" + aggregateUrn + "\" by using URN. Falling back to searching for URL \"" + aggregateUrlString + "\"");
                    if (aggregateUrlString != null)
                        try {
                            URL aggregateUrl = new URL(aggregateUrlString);
                            hopServer = testbedInfoSource.getServerByUrl(aggregateUrl);
                            if (hopServer != null)
                                log.debug("Found auth by URL. Real URN is: \"" + hopServer.getDefaultComponentManagerUrn() + "\"");
                        } catch (MalformedURLException e) {
                            log.warn("Malformed URL in workflow aggregate_url: \"" + aggregateUrlString + "\"", e);
                        }
                }

                if (hopServer == null) {
                    throw new JFedException("Could not find the authority \"" + aggregateUrn + "\" which is described in the stitching workflow. Giving up.");
                }

                if (!Objects.equals(hopServer.getDefaultComponentManagerUrn(), aggregateUrn)) {
                    log.debug("Hop with auth " + aggregateUrn + " will be seen as hop with auth " + hopServer.getDefaultComponentManagerUrn());
                    //force same as auth. This is important in the exogeni case. WE might need to use exoSM or a specific rack. The SCS decides!
                    aggregateUrn = hopServer.getDefaultComponentManagerUrn();
                    //noinspection unchecked
                    hopHt.put("aggregate_urn", aggregateUrn);
                } else
                    log.debug("Hop with auth " + aggregateUrn + "");

                if (hopServer.isEdgeVlan()) {
                    fakeSfaAuths.add(hopServer);
                }

                StitchingHopData stitchingHopData = new StitchingHopData(linkName, hopHt, stitchingElement, log);
                if (stitchingHopDataList.contains(stitchingHopData)) {
                    int i = stitchingHopDataList.indexOf(stitchingHopData);
                    assert i >= 0;
                    assert i < stitchingHopDataList.size();
                    stitchingHopData = stitchingHopDataList.get(i); //replace created stitchingHopData with existing
                    stitchingHopData.processExtraWorkflowData(linkName, hopHt);
                } else {
                    stitchingHopDataList.add(stitchingHopData);
                }

                assert Objects.equals(stitchingHopData.getAggregateUrn(), hopServer.getDefaultComponentManagerUrn());
                assert Objects.equals(stitchingHopData.getAggregateUrn(), aggregateUrn);

                if (!aggUrnsToAggUrl.containsKey(stitchingHopData.getAggregateUrn()))
                    aggUrnsToAggUrl.put(stitchingHopData.getAggregateUrn(), stitchingHopData.getAggregateUrl());
            }
        }
        //link dependency graph of stitchingHopDataList
        for (StitchingHopData stitchingHopData : stitchingHopDataList) {
            stitchingHopData.linkDeps(stitchingHopDataList);
        }

        log.debug("stitchingHopDataList=" + stitchingHopDataList);

        //we now have a list with all StitchingHopData
        //we will create all StitchingCallData
        Set<String> stitchedAuthorityUrns = new HashSet<>(); //also keep track of all authorities
        for (Map.Entry<String, String> e : aggUrnsToAggUrl.entrySet()) {
            String urn = e.getKey();
            String url = e.getValue();

            StitchingCallData stitchingCallData = new StitchingCallData(
                    urn, url,
                    testbedInfoSource, log);
            stitchingCallDataByAuthUrn.put(urn, stitchingCallData);
            stitchedAuthorityUrns.add(urn);
        }
        for (StitchingHopData stitchingHopData : stitchingHopDataList) {
            StitchingCallData stitchingCallData = stitchingCallDataByAuthUrn.get(stitchingHopData.getAggregateUrn());
            assert stitchingCallData != null;
            stitchingCallData.addHopData(stitchingHopData);
        }
        for (StitchingCallData stitchingCallData : stitchingCallDataByAuthUrn.values()) {
            stitchingCallData.linkDeps(stitchingCallDataByAuthUrn.values());
        }

        log.debug("stitchedAuthorityUrns=" + stitchedAuthorityUrns);
        log.debug("stitchingCallDataByAuthUrn=" + stitchingCallDataByAuthUrn);

        //map how exogeni is used in the workflow
        boolean stitchedExoGeniToplevelAuth = false;
        Set<GeniUrn> stitchedExoGeniSubAuths = new HashSet<>();
        for (StitchingHopData stitchingHopData : stitchingHopDataList) {
            GeniUrn hopAuthUrn = GeniUrn.parse(stitchingHopData.getAggregateUrn());
            if (hopAuthUrn != null && Objects.equals(hopAuthUrn.getEncodedTopLevelAuthority_withoutSubAuth(), "exogeni.net")) {
                if (hopAuthUrn.hasSubAuthority())
                    stitchedExoGeniSubAuths.add(hopAuthUrn);
                else
                    stitchedExoGeniToplevelAuth = true;
            }
        }

        if (!stitchedExoGeniSubAuths.isEmpty() && stitchedExoGeniToplevelAuth)
            throw new JFedException("Exogeni stitching with a too complex scenario: both sub authorities and top level authority need to be involved. jFed cannot handle this.");

        //now create and add the StitchingCallData for AM's that are NOT involved in stitching at all, but are in the RSpec.
        List<String> allAuthorityUrns = new BasicStringRspec(scsReplyRspec).getAllComponentManagerUrns();
        Set<String> nonStitchedAuthorityUrns = new HashSet<>(allAuthorityUrns);
        for (String urnString : allAuthorityUrns) {
            try {
                GeniUrn urn = new GeniUrn(urnString);
                if (Objects.equals(urn.getEncodedTopLevelAuthority_withoutSubAuth(), "exogeni.net")) {

                    //ignore componentmanagers like urn:publicid:IDN+exogeni.net:bbnNet+authority+am
                    if (urn.hasSubAuthority() && urn.getEncodedSubAuthName() != null && urn.getEncodedSubAuthName().endsWith("Net")) {
                        nonStitchedAuthorityUrns.remove(urnString);
                        continue;
                    }

                    if (!urn.hasSubAuthority()) {
                        //exogeni.net top level itself
                        if (!stitchedExoGeniToplevelAuth) {
                            if (!stitchedExoGeniSubAuths.isEmpty())
                                throw new JFedException("Exogeni stitching with a too complex scenario: both sub authorities and top level authority need to be involved. jFed cannot handle this.");

                            stitchedExoGeniToplevelAuth = true;
                            nonStitchedAuthorityUrns.add(urn.toString());
                        }// else nothing to do
                    } else {
                        //not that this is a tricky situation: we can't use the user exogeni preferences, as we can't mix sub auth and top level auth.
                        if (!stitchedExoGeniToplevelAuth) {
                            if (!stitchedExoGeniSubAuths.contains(urn)) {
                                //involve sub auth
                                stitchedExoGeniSubAuths.add(urn);
                                nonStitchedAuthorityUrns.add(urn.toString());
                            }
                        } //else nothing to do: we DON't want to involve any sub auth, the top auth will handle it.

                    }
                }
            } catch (GeniUrn.GeniUrnParseException e) {
                log.warn("Could not parse URN {}. Ignoring!", urnString);
            }

        }
        nonStitchedAuthorityUrns.removeAll(stitchedAuthorityUrns);
        log.debug("nonStitchedAuthorityUrns=" + nonStitchedAuthorityUrns);
        for (String nonStitchedAuthorityUrn : nonStitchedAuthorityUrns) {
            StitchingCallData stitchingCallData = new StitchingCallData(
                    nonStitchedAuthorityUrn,
                    null /*url not needed if urn is of known authority (and it should be or we can't use this RSpec even without stitching)*/,
                    testbedInfoSource,
                    log);
            stitchingCallDataByAuthUrn.put(nonStitchedAuthorityUrn, stitchingCallData);
        }


        //now find the ordering: copy depgraph and keep removing nodes without deps until nothing is left. The order of removing is the order searched.
        Map<StitchingCallData, StitchingCallDataDepGrahpHelper> depGraphCopy =
                new HashMap<>();
        for (StitchingCallData stitchingCallData : stitchingCallDataByAuthUrn.values())
            depGraphCopy.put(stitchingCallData, new StitchingCallDataDepGrahpHelper(stitchingCallData));
        if (!stitchingCallDataByAuthUrn.isEmpty())
            while (orderedStitchingCallDataList.size() != stitchingCallDataByAuthUrn.size()) {
                int prevOrderedStitchingCallDataListSize = orderedStitchingCallDataList.size();

                for (StitchingCallDataDepGrahpHelper h : depGraphCopy.values()) {
                    if (h.dependencies.isEmpty() && !orderedStitchingCallDataList.contains(h.s)) {
                        for (StitchingCallData dep : h.dependingOnThis) {
                            StitchingCallDataDepGrahpHelper hDep = depGraphCopy.get(dep);
                            hDep.dependencies.remove(h.s);
                        }
                        h.dependingOnThis.clear();
                        orderedStitchingCallDataList.add(h.s);
                    }
                }

                if (prevOrderedStitchingCallDataListSize == orderedStitchingCallDataList.size())
                    throw new JFedException("Loop in depgraph (size stays " + prevOrderedStitchingCallDataListSize + " targetsize=" + stitchingCallDataByAuthUrn.size() + "): " +
                            "unlooped: " + orderedStitchingCallDataList + " all: " + depGraphCopy.values());
            }

        //create orderedSfaAuths
        boolean sawExoGeni = false;
        List<GeniUrn> orderedSfaAuthUrns = new ArrayList<>();
        for (StitchingCallData dep : orderedStitchingCallDataList) {
            assert !orderedSfaAuths.contains(dep.getServer()) :
                    "duplicate auth: " + orderedSfaAuths + " + " + dep.getServer();
            GeniUrn serverUrn;
            try {
                serverUrn = new GeniUrn(dep.getServer().getDefaultComponentManagerUrn());
            } catch (GeniUrn.GeniUrnParseException e) {
                throw new JFedException("Failed to parse server URN: " +
                        "\"" + dep.getServer().getDefaultComponentManagerUrn() + "\" for server " + dep.getServer().getId());
            }
            boolean isExoGeni = Objects.equals(serverUrn.getEncodedTopLevelAuthority_withoutSubAuth(), "exogeni.net");
            if (isExoGeni) {
                assert !sawExoGeni : "more then one exogeni auth: " + orderedSfaAuths + " + " + dep.getServer();
                sawExoGeni = true;
            }

            orderedSfaAuths.add(dep.getServer());
            orderedSfaAuthUrns.add(serverUrn);
        }
        log.debug("orderedSfaAuthUrns=" + orderedSfaAuthUrns);
    }


    public static Element extractStitchingElementFromRspec(String rspec) {
        return extractStitchingElementFromRspec(rspec, null);
    }

    public static Element extractStitchingElementFromRspec(String rspec, @Nullable Logger log) {
        if (log == null)
            log = LoggerFactory.getLogger(StitchingData.class);
        try {
            //first parse rspec
            Document doc = XmlUtil.parseXmlFromString(rspec, true/*namespaceaware*/);

            doc.getDocumentElement().normalize();

            NodeList rspecList;
            NodeList rspecListNS = doc.getElementsByTagNameNS(NAMESPACE_RSPEC, "rspec");
            if (rspecListNS.getLength() == 0) {
                rspecList = doc.getElementsByTagName("rspec");
            } else
                rspecList = rspecListNS;

            assert rspecList.getLength() == 1 :
                    "not a single rspec element, but " + rspecList.getLength() + " in " + rspec;
            Node rspecNode = rspecList.item(0);
            assert rspecNode != null;
            assert rspecNode.getNodeType() == Node.ELEMENT_NODE;
            Element rspecElement = (Element) rspecNode;

            NodeList stitchingList;
            NodeList stitchingListNS = rspecElement.getElementsByTagNameNS(NAMESPACE_STITCHING, "stitching");
            if (stitchingListNS.getLength() == 0) {
                //fallback to looking for stitching element without namespace.
                NodeList stitchingListNoNS = rspecElement.getElementsByTagName("stitching");
                if (stitchingListNoNS.getLength() == 0)
                    return null;
                stitchingList = stitchingListNoNS;
            } else
                stitchingList = stitchingListNS;

            assert stitchingList.getLength() == 1 :
                    "not a single stitching element, but " + stitchingList.getLength() + " in " + rspec;
            Node stitchingNode = stitchingList.item(0);
            assert stitchingNode != null;
            assert stitchingNode.getNodeType() == Node.ELEMENT_NODE;

            return (Element) stitchingNode;
        } catch (SAXException | IOException e) {
            log.error("Error parsing rspec", e);
        }
        return null;
    }

    /**
     * Get the current Rspec, including updated suggestVlan and availableVLAN data.
     * <p/>
     * This is based on the original request RSpec returned by the SCS, combined with updates for
     * availableVLAN and suggestedVLAN from "vlan unavailable" errors and CreateSliver manifests
     * <p>
     * This also adds or removes the "expires" attribute of the RSpec tag, as needed.
     */
    public String getCurrentRequestRspec(Collection<GeniUrn> aggUrnsNotSupportingAny, boolean useExpires, @Nullable Date expireDate) {
        log.trace("getCurrentRequestRspec");
        //rewrite rspec, so that vlanInfo is used instead of what is in the rspec (requires DOM + transformation)

        try {
            DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
            dbFactory.setNamespaceAware(true);
            DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
            Document doc = dBuilder.parse(new InputSource(new StringReader(scsReplyRspec)));
            assert doc != null;

            doc.getDocumentElement().normalize();

            Element root = doc.getDocumentElement();

            //rspec - stitching - path - hop (MULTIPLE) - link - switchingCapabilityDescriptor - switchingCapabilitySpecificInfo - switchingCapabilitySpecificInfo_L2sc -
            //          vlanRangeAvailability
            //          suggestedVLANRange

            NodeList rspecList = doc.getElementsByTagNameNS(NAMESPACE_RSPEC, "rspec");
            assert rspecList.getLength() == 1 :
                    "not a single rspec element, but " + rspecList.getLength() + " in " + scsReplyRspec;
            Node rspecNode = rspecList.item(0);
            assert rspecNode != null;
            assert rspecNode.getNodeType() == Node.ELEMENT_NODE;
            Element rspecElement = (Element) rspecNode;

            if (useExpires) {
                if (expireDate != null)
                    rspecElement.setAttribute("expires", RFC3339Util.dateToRFC3339String(expireDate, true/*force Z*/));
            } else {
                rspecElement.removeAttribute("expires");
            }

            NodeList stitchingListNoNS = rspecElement.getElementsByTagNameNS(NAMESPACE_STITCHING, "stitching");
            assert stitchingListNoNS.getLength() == 1 :
                    "not a single stitching element, but " + stitchingListNoNS.getLength() + " in " + scsReplyRspec;
            Node stitchingNode = stitchingListNoNS.item(0);
            assert stitchingNode != null;
            assert stitchingNode.getNodeType() == Node.ELEMENT_NODE;
            Element stitchingElement = (Element) stitchingNode;

            List<Element> pathEls =
                    XmlUtil.getDomElementChildElementsNS(stitchingElement, StitchingData.NAMESPACE_STITCHING, "path");
            for (Element pathEl : pathEls) {
                String linkName = pathEl.getAttribute("id");
                assert linkName != null : "<stitching> has <path> element without id attribute: " + pathEl;

                List<Element> origHopElements =
                        XmlUtil.getDomElementChildElementsNS(pathEl, StitchingData.NAMESPACE_STITCHING, "hop");

                log.trace("getCurrentRequestRspec() rewriting path with link id=" + linkName + " orig el count: " + origHopElements.size());

                //remove original hop elements
                for (Element hopElement : origHopElements) {
                    hopElement.getParentNode().removeChild(hopElement);
                }

                //keep only hops for this path
                List<StitchingHopData> filteredHopData = new ArrayList<>(stitchingHopDataList);
                for (ListIterator<StitchingHopData> it = filteredHopData.listIterator(); it.hasNext(); ) {
                    StitchingHopData hop = it.next();

                    if (!Objects.equals(hop.getLinkName(), linkName))
                        it.remove();
                }

                //sort according to hop id (seems to be needed!)
                List<StitchingHopData> sortedHopData = new ArrayList<>(filteredHopData);
                sortedHopData.sort(Comparator.comparing(StitchingHopData::getRspecStitchingHopId));

                Map<StitchingHopData, String> hopDataToFixedVlans = new HashMap<>();
                for (StitchingHopData hop : sortedHopData) {
                    assert hop != null;
                    for (StitchingHopData depHop : hop.getDependencies()) {
                        if (depHop.getStitchingExtensionSource() == StitchingHopData.StitchingExtensionSource.MANIFEST) {
                            //if dependency is fixed, set suggested and available to only that!
                            Integer fixVlan = depHop.getSuggestedVlan();
                            if (fixVlan != null)
                                hopDataToFixedVlans.put(hop, fixVlan + "");
                        }
                    }
                }
                log.debug("hopDataToFixedVlans=" + hopDataToFixedVlans);

                //add updated hop elements
                for (StitchingHopData hop : sortedHopData) {
                    assert hop != null;

                    boolean useAny = true;

                    //not always so for all links:
//                    //if hop has deps, the dep has already chosen a vlan, and we don't get to choose
//                    if (! hop.getDependencies().isEmpty())
//                        useAny = false;

//                    for (StitchingHopData depHop : hop.getDependencies()) {
                    //why?
//                        if (depHop.getStitchingExtensionSource() == StitchingHopData.StitchingExtensionSource.MANIFEST)
//                            useAny = false;
                    //why?
//                        if (aggUrnsNotSupportingAny.contains(depHop.getAggregateUrn()+""))
//                            useAny = false;
//                    }

                    //why?
//                    if (hop.getStitchingExtensionSource() == StitchingHopData.StitchingExtensionSource.MANIFEST)
//                        useAny = false;

                    GeniUrn hopAggregateU = GeniUrn.parse(hop.getAggregateUrn());
                    if (hopAggregateU == null)
                        useAny = false; //safety fallback
                    else {
                        String hopAggregateTLA = hopAggregateU.getEncodedTopLevelAuthority_withoutSubAuth();
                        for (GeniUrn aggUrn : aggUrnsNotSupportingAny) {
                            if (Objects.equals(aggUrn.getEncodedTopLevelAuthority_withoutSubAuth(), hopAggregateTLA))
                                useAny = false;
                        }
                    }
                    log.debug("useAny=" + useAny + " for " + hop.getAggregateUrn());

                    String fixedVlan = hopDataToFixedVlans.get(hop);
                    Element rspecStitchingHopElement;
                    if (fixedVlan != null) {
                        log.debug("using fixed vlan (" + fixedVlan + ") (due to dep) for " + hop.getAggregateUrn());
                        rspecStitchingHopElement = hop.getRspecStitchingHopElement(fixedVlan, fixedVlan);
                    } else
                        rspecStitchingHopElement = hop.getRspecStitchingHopElement(useAny);
                    assert rspecStitchingHopElement != null;
                    Node importedNode = doc.importNode(rspecStitchingHopElement, true/*deep*/);
                    assert importedNode != null;

//                    logger.trace("getCurrentRequestRspec() adding hop   " + hop.getRspecStitchingHopId()+"  suggestedVlan="+hop.getSuggestedVlan()+"  nodeVlan={}",
//                            XmlUtil.elementToXmlString(hop.getRspecStitchingHopElement()));

//                    Element hopElement = (Element) importedNode;
//                    Element linkEl = XmlUtil.getDomElementFirstChildElement((hopElement, "link");
//                    Element scDescriptorEl = XmlUtil.getDomElementFirstChildElement((linkEl, "switchingCapabilityDescriptor");
//                    Element scSpecificInfoEl = XmlUtil.getDomElementFirstChildElement((scDescriptorEl, "switchingCapabilitySpecificInfo");
//                    Element scSpecificInfo_L2scEl = XmlUtil.getDomElementFirstChildElement((scSpecificInfoEl, "switchingCapabilitySpecificInfo_L2sc");
//                    Element vlanRangeAvailabilityEl = XmlUtil.getDomElementFirstChildElement((scSpecificInfo_L2scEl, "vlanRangeAvailability");
//                    Element suggestedVLANRangeEl = XmlUtil.getDomElementFirstChildElement((scSpecificInfo_L2scEl, "suggestedVLANRange");
//                    logger.trace("suggestedVLANRangeEl text="+suggestedVLANRangeEl.getTextContent());

//                    logger.trace("getCurrentRequestRspec() adding hop   " + hop.getRspecStitchingHopId()+"  suggestedVlan="+hop.getSuggestedVlan());
                    pathEl.appendChild(importedNode);
                }
            }

            try {
                TransformerFactory transFactory = TransformerFactory.newInstance();
                Transformer transformer = transFactory.newTransformer();
                StringWriter buffer = new StringWriter();
                transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
                transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
                transformer.setOutputProperty(OutputKeys.INDENT, "yes");
                transformer.transform(new DOMSource(rspecNode), new StreamResult(buffer));
//                logger.debug("generated RSPEC: {}", buffer.toString());
                return buffer.toString();
            } catch (TransformerConfigurationException e) {
                log.error("TransformerConfigurationException in getCurrentRequestRspec()", e);
            } catch (TransformerException e) {
                log.error("TransformerException in getCurrentRequestRspec()", e);
            }
        } catch (Exception e) {
            log.error("Exception in getCurrentRequestRspec()", e);
        }

        return null;
    }

    public List<StitchingCallData> getOrderedStitchingCallDataList() {
        return orderedStitchingCallDataList;
    }

    public StitchingCallData getStitchingCallData(String authUrn) {
        return stitchingCallDataByAuthUrn.get(authUrn);
    }

    public List<StitchingHopData> getStitchingHopDataList() {
        return stitchingHopDataList;
    }

    public List<Server> getOrderedSfaAuths() {
        return orderedSfaAuths;
    }

    public Collection<Server> getFakeSfaAuths() {
        return fakeSfaAuths;
    }


    @Override
    public String toString() {
        String orderedStitchingCallDataListString = "{";
        for (StitchingCallData stitchingCallData : orderedStitchingCallDataList)
            orderedStitchingCallDataListString += "\n    " + stitchingCallData;
        orderedStitchingCallDataListString += "}";

        return "StitchingData{" +
                "orderedStitchingCallDataList=" + orderedStitchingCallDataListString +
                '}';
    }

    private static class StitchingCallDataDepGrahpHelper {
        final List<StitchingCallData> dependencies = new ArrayList<>();
        final List<StitchingCallData> dependingOnThis = new ArrayList<>();
        final StitchingCallData s;

        public StitchingCallDataDepGrahpHelper(StitchingCallData s) {
            this.s = s;
            this.dependencies.addAll(s.getDependencies());
            this.dependingOnThis.addAll(s.getDependingOnThis());
        }

        @Override
        public String toString() {
            return "{" + s.getServer() + " deps=" + dependencies.size() + " depOnThis=" + dependingOnThis.size() + "}";
        }
    }
}
