package be.iminds.ilabt.jfed.rspec.rspec_source;

import be.iminds.ilabt.jfed.rspec.model.*;
import be.iminds.ilabt.jfed.util.GeniUrn;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;

/**
 * ManifestRspecMerger merges manifest from different sources, keeping the appropriate data.
 * It can also use a request, as basic for authorities from which no manifests is received.
 */
public class ManifestRspecMerger {
    private final static Logger LOG = LoggerFactory.getLogger(ManifestRspecMerger.class);

    private final ModelRspecType modelRspecType;
    private final RspecFactory rspecFactory;

    private RequestRspecSource request;
    private final Map<GeniUrn, ManifestRspecSource> manifestBySource = new HashMap<>();
    private final List<GeniUrn> manifestSources = new ArrayList<>(); //a list to help fix order in tests

    private ManifestRspecSource mergedManifest;

    private boolean addNodesOnlyFoundInRequest;

    public ManifestRspecMerger(ModelRspecType modelRspecType) {
        this.modelRspecType = modelRspecType;
        this.rspecFactory = RspecFactoryFactory.getRspecFactoryInstance(modelRspecType);
    }

    public boolean isAddNodesOnlyFoundInRequest() {
        return addNodesOnlyFoundInRequest;
    }

    /**
     * Note that there is a difference between no manifest received, and an empty or null manifest received.
     * <p/>
     * In the later case, it is assumed that there are no resources at the cm.
     * <p/>
     * This option determines whether these nodes are added to the manifest, or not.
     *
     * @param addNodesOnlyFoundInRequest If true, always add all nodes in the request.
     *                                   If false, don't add nodes from the request if
     *                                   they are not in the manifest of a matching authority.
     */
    public void setAddNodesOnlyFoundInRequest(boolean addNodesOnlyFoundInRequest) {
        this.addNodesOnlyFoundInRequest = addNodesOnlyFoundInRequest;
    }

    public void setRequest(RequestRspecSource requestRspecSource) {
        if (this.request == requestRspecSource)
            return;
        this.request = requestRspecSource;
        mergedManifest = null;
    }

    /**
     * @param manifestRspecSource may be null, to signal that no resources at all are in use at this authority.
     */
    public void setManifest(GeniUrn sourceAuthority, ManifestRspecSource manifestRspecSource) {
        if (manifestBySource.get(sourceAuthority) == manifestRspecSource ||
                manifestBySource.get(sourceAuthority) != null && manifestRspecSource == null)
            return;

        if (sourceAuthority.getEncodedTopLevelAuthority_withoutSubAuth().equalsIgnoreCase("exogeni.net")) {
            //exogeni fixes. Because exogeni messes up link URNs in some cases

            ModelRspec newModelRspec = manifestRspecSource.getModelRspec();
            List<RspecLink> linksToRemove = new ArrayList<>();
            for (RspecLink rspecLink : newModelRspec.getLinks()) {
                List<GeniUrn> origComponentManagerUrns = rspecLink.getComponentManagerUrns();
                for (GeniUrn cm : origComponentManagerUrns) {
                    if (cm.hasSubAuthority() && cm.getEncodedTopLevelAuthority_withoutSubAuth().equalsIgnoreCase("exogeni.net") && cm.getEncodedSubAuthName().endsWith("Net")) {
                        String newSubAuth = cm.getEncodedSubAuthName().replaceAll("Net$", "vmsite");
                        GeniUrn newUrn =
                                GeniUrn.createGeniUrnFromEncodedParts("exogeni.net:" + newSubAuth, cm.getEncodedResourceType(), cm.getEncodedResourceName());
                        LOG.debug("Changing link component manager URN from " + cm + " to " + newUrn);
                        rspecLink.getComponentManagerUrns().remove(cm);
                        rspecLink.getComponentManagerUrns().add(newUrn);
                    }
                }
                //exogeni returns stitched links with the other side's CM missing. We can't handle that so we remove them.
                if (rspecLink.getInterfaces().size() < 2)
                    linksToRemove.add(rspecLink);
            }
            for (RspecLink toRemove : linksToRemove)
                newModelRspec.deleteLink(toRemove);

            manifestRspecSource = new ManifestRspecSource(newModelRspec);
        }

        manifestBySource.put(sourceAuthority, manifestRspecSource);
        if (!manifestSources.contains(sourceAuthority))
            manifestSources.add(sourceAuthority);
        mergedManifest = null;
    }

    /**
     * @param resourceUrn  urn of a resource, or a resource component manager
     * @param authorityUrn authority
     * @return true if the resource is managed by the given authority
     */
    public static boolean isAuthoritative(GeniUrn resourceUrn, GeniUrn authorityUrn) {
        /* TODO: this might not be an ideal solution. maybe this needs an SfaAuthority extended with
                 info about authority is authoritative above which? (and even in that case, isAuthoritative can't handle 2
                 authorities providing a manifest, and both being "authoritative", but one more than te other)  */

        if (authorityUrn.hasSubAuthority()) {
            //if authority is sub authority, resource must match exactly that sub authority
            return Objects.equals(resourceUrn.getEncodedTopLevelAuthority(), authorityUrn.getEncodedTopLevelAuthority());
        } else {
            //allow a resource that is from a sub authority
            return Objects.equals(resourceUrn.getEncodedTopLevelAuthority_withoutSubAuth(), authorityUrn.getEncodedTopLevelAuthority_withoutSubAuth());
        }
    }

    protected boolean haveManifestForResource(List<GeniUrn> urns) {
        for (GeniUrn urn : urns)
            for (GeniUrn manifestAuthority : manifestBySource.keySet())
                if (isAuthoritative(urn, manifestAuthority))
                    return true;
        return false;
    }

    protected boolean haveManifestForResource(GeniUrn urn) {
        List<GeniUrn> urns = new ArrayList<>();
        urns.add(urn);
        return haveManifestForResource(urns);
    }

    public ManifestRspecSource getMergedManifest() {
        if (mergedManifest != null)
            return mergedManifest;

        assert manifestSources.size() == manifestBySource.size() :
                "manifestSources=" + manifestSources + "   manifestBySource.keySet=" + manifestBySource.keySet();
        LOG.debug("Starting manifest merge " +
                (request == null ? "without request" : "with request")
                + " and with " + manifestSources.size() + " manifests from: " + manifestSources);

        ModelRspec mergedModel = rspecFactory.createModelRspec("manifest");

        Set<String> authoritativeNodesUniqueIds = new HashSet<>();

        Map<String, String> nodesWithOtherUniqueIds = new HashMap<>();

        //STEP 1: NODES: first do everything for nodes, then for links.
        // When creating the nodes, all interfaces are created but left unbound.
        // They are bound to a link in the phase where links are added

        List<GeniUrn> sortedSourceUrns = new ArrayList<>(manifestSources);
        //don't sort so we can test different orders
//        sortedSourceUrns.sort(new Comparator<GeniUrn>() {
//            @Override
//            public int compare(GeniUrn o1, GeniUrn o2) {
//                return o1.getValue().compareTo(o2.getValue());
//            }
//        });

        //STEP 1a: first add nodes for which the manifest is an authority
        for (GeniUrn authorityUrn : sortedSourceUrns) { //order must be fixed for tests to work reliably!
            ManifestRspecSource manifestRspecSource = manifestBySource.get(authorityUrn);
            if (manifestRspecSource == null) {
                //occurs on parse error
                LOG.debug("Skip missing  manifest for " + sortedSourceUrns);
                continue;
            }
            ModelRspec modelRspec = manifestRspecSource.getModelRspec(modelRspecType);
            if (modelRspec == null) {
                //occurs on parse error
                LOG.debug("Skip invalid  manifest for " + sortedSourceUrns);
                continue;
            }
            assert modelRspec.getNodes() != null;
            for (RspecNode rspecNode : modelRspec.getNodes())
                if (rspecNode.getComponentManagerId() != null) {
                    LOG.debug("Checking authoritative merge for " + authorityUrn + " node id=" + rspecNode.getUniqueId() + " (cm=" + rspecNode.getComponentManagerId() + ")");
                    if (isAuthoritative(rspecNode.getComponentManagerId(), authorityUrn)) {
                        LOG.debug("    authoritative merge");
                        assert !authoritativeNodesUniqueIds.contains(rspecNode.getUniqueId()) :
                                "already got authority for node client_id=\"" + rspecNode.getClientId() + "\" component_manager_id=\"" + rspecNode.getComponentManagerId() + "\" authorityUrn=" + authorityUrn;
                        RspecNode mergedNode =
                                rspecFactory.copyNode(mergedModel, rspecNode, RspecNode.InterfaceCopyMethod.CREATE_NEW_INTERFACE);
                        authoritativeNodesUniqueIds.add(mergedNode.getUniqueId());
                        /* prevent the same node to be added again if it appears less specific in other rspecs (important
                         in case of ExoSM, which adds a subauthority with the specific site to the componentManagerId
                          of a node) */
                        String otherUniqueId = getUniqueIdWithoutSubAuth(mergedNode);
                        if (!Objects.equals(otherUniqueId, mergedNode.getUniqueId())) {
                            authoritativeNodesUniqueIds.add(otherUniqueId);
                            nodesWithOtherUniqueIds.put(otherUniqueId, mergedNode.getUniqueId());
                        }
                        mergedModel.addNode(mergedNode);
                    }
                }
        }

        //STEP 1b: then add any missing nodes that are in manifests
        for (GeniUrn authorityUrn : sortedSourceUrns) {
            ManifestRspecSource manifestRspecSource = manifestBySource.get(authorityUrn);
            if (manifestRspecSource == null) {
                //occurs on parse error
                LOG.debug("Skip missing  manifest for " + sortedSourceUrns);
                continue;
            }
            ModelRspec modelRspec = manifestRspecSource.getModelRspec(modelRspecType);
            if (modelRspec == null) {
                //occurs on parse error
                LOG.debug("Skip invalid  manifest for " + sortedSourceUrns);
                continue;
            }
            for (RspecNode rspecNode : modelRspec.getNodes())
                if (rspecNode.getComponentManagerId() != null) {
                    if (!authoritativeNodesUniqueIds.contains(rspecNode.getUniqueId())) {
                        if (mergedModel.getNodeByUniqueId(rspecNode.getUniqueId()) == null) {
                            LOG.debug("Adding missing node from non-authoritative manifest: client_id=" + rspecNode.getClientId() + " (cm=" + rspecNode.getComponentManagerId() + ")");
                            RspecNode mergedNode =
                                    rspecFactory.copyNode(mergedModel, rspecNode, RspecNode.InterfaceCopyMethod.CREATE_NEW_INTERFACE);
                            mergedModel.addNode(mergedNode);
                            assert mergedModel.getNodeByUniqueId(rspecNode.getUniqueId()) != null;
                        }
                    } else {
                        assert mergedModel.getNodeByUniqueId(rspecNode.getUniqueId()) != null
                                || nodesWithOtherUniqueIds.containsKey(rspecNode.getUniqueId())
                                && mergedModel.getNodeByUniqueId(nodesWithOtherUniqueIds.get(rspecNode.getUniqueId())) != null;
                    }
                }
        }

        //STEP 1c: add any nodes from the request that hasn't been seen yet
        if (request != null) {
            ModelRspec modelRspec = request.getModelRspec(modelRspecType);
            if (modelRspec != null) //== null occurs on parse error
                for (RspecNode rspecNode : modelRspec.getNodes())
                    if (rspecNode.getComponentManagerId() != null) {
                        if (!authoritativeNodesUniqueIds.contains(rspecNode.getUniqueId())) {
                            if (mergedModel.getNodeByUniqueId(rspecNode.getUniqueId()) == null) {
                                LOG.debug("Adding missing node from (non-authoritative) request: client_id=" + rspecNode.getClientId() + " (cm=" + rspecNode.getComponentManagerId() + ")");
                                if (!addNodesOnlyFoundInRequest || !haveManifestForResource(rspecNode.getComponentManagerId())) {
                                    RspecNode mergedNode =
                                            rspecFactory.copyNode(mergedModel, rspecNode, RspecNode.InterfaceCopyMethod.CREATE_NEW_INTERFACE);
                                    mergedModel.addNode(mergedNode);
                                    assert mergedModel.getNodeByUniqueId(rspecNode.getUniqueId()) != null;
                                }
                            }
                        } else {
                            assert mergedModel.getNodeByUniqueId(rspecNode.getUniqueId()) != null
                                    || nodesWithOtherUniqueIds.containsKey(rspecNode.getUniqueId())
                                    && mergedModel.getNodeByUniqueId(nodesWithOtherUniqueIds.get(rspecNode.getUniqueId())) != null;

                        }
                    }
        }

        //STEP 2: LINKS: now add links, but skip links that are dangling

        //STEP 2a: first add links for which the manifest is an authority
//        Map<String, RspecLink> authoritativeLinksByUniqueId = new HashMap<String, RspecLink>();
        List<RspecLink> mergedLinks = new ArrayList<>();

        for (GeniUrn authorityUrn : sortedSourceUrns) { //order must be fixed for tests to work reliably!
            ManifestRspecSource manifestRspecSource = manifestBySource.get(authorityUrn);
            if (manifestRspecSource == null) {
                //occurs on parse error
                LOG.debug("Skip missing  manifest for " + sortedSourceUrns);
                continue;
            }
            ModelRspec modelRspec = manifestRspecSource.getModelRspec(modelRspecType);
            if (modelRspec == null) {
                //occurs on parse error
                LOG.debug("Skip invalid  manifest for " + sortedSourceUrns);
                continue;
            }
            for (RspecLink rspecLink : modelRspec.getLinks()) {
                boolean authorityMatches = false;
                for (GeniUrn linkUrn : rspecLink.getComponentManagerUrns())
                    if (isAuthoritative(linkUrn, authorityUrn))
                        authorityMatches = true;
                if (authorityMatches) {
                    /* first reduce the rspecLink.uniqueId to it's proper form. In case of ExoSM, this uniqueId can have
                    changed because of the addition of one or more sub-authorities to a ComponentManagerId
                     */
//                    String reducedUniqueLinkId = getUniqueIdWithoutSubAuth(rspecLink);
                    RspecLink mergedLink = findLink(mergedLinks, rspecLink);

//                    if (authoritativeLinksByUniqueId.containsKey(reducedUniqueLinkId)) {
                    if (mergedLink != null) {
                        LOG.debug("Updating missing link with additional authoritative manifest (" + authorityUrn + "): client_id=" + rspecLink.getClientId() + " (cm=" + rspecLink.getComponentManagerUrns() + ")");
                        //link already added for other component_manager  (example in case of stitching)
//                        RspecLink mergedLink = authoritativeLinksByUniqueId.get(reducedUniqueLinkId);
                        mergedLink.overwritePropertiesWith(rspecLink, false/*do not overwrite with null*/);
                        for (RspecInterface mergedRspecInterface : mergedLink.getInterfaces()) {
                            RspecInterface manifestRspecInterface =
                                    rspecLink.getInterfaceByClientId(mergedRspecInterface.getClientId());
                            if (manifestRspecInterface != null) {
                                mergedRspecInterface.overwritePropertiesWith(manifestRspecInterface, false/*do not overwrite with null*/);
                            }
                        }
                    } else {
                        LOG.debug("Adding missing link from authoritative manifest (" + authorityUrn + "): client_id=" + rspecLink.getClientId() + " (cm=" + rspecLink.getComponentManagerUrns() + ")");
                        mergedLink = addLink(mergedModel, rspecLink);
//                        authoritativeLinksByUniqueId.put(reducedUniqueLinkId, mergedLink);
                        mergedLinks.add(mergedLink);
                    }
                } else {
                    LOG.debug("Skipping link {} because it doesn't have authority {}", rspecLink.getUniqueId(), authorityUrn);
                }
            }
        }

        //STEP 2b: then add any missing links that are in manifests
        for (GeniUrn authorityUrn : sortedSourceUrns) { //order must be fixed for tests to work reliably!
            ManifestRspecSource manifestRspecSource = manifestBySource.get(authorityUrn);
            if (manifestRspecSource == null) {
                //occurs on parse error
                LOG.debug("Skip missing  manifest for " + sortedSourceUrns);
                continue;
            }
            ModelRspec modelRspec = manifestRspecSource.getModelRspec(modelRspecType);
            if (modelRspec == null) {
                //occurs on parse error
                LOG.debug("Skip invalid  manifest for " + sortedSourceUrns);
                continue;
            }
            for (RspecLink rspecLink : modelRspec.getLinks()) {
                /* first reduce the rspecLink.uniqueId to it's proper form. In case of ExoSM, this uniqueId can have
                    changed because of the addition of one or more sub-authorities to a ComponentManagerId
                */
//                String reducedUniqueLinkId = getUniqueIdWithoutSubAuth(rspecLink);
                RspecLink mergedLink = findLink(mergedLinks, rspecLink);

//                if (!authoritativeLinksByUniqueId.containsKey(reducedUniqueLinkId)) {
                if (mergedLink == null) {
//                    if (mergedModel.getLinkByUniqueId(reducedUniqueLinkId) == null) {
                    LOG.debug("Adding missing link from non-authoritative manifest: client_id=" + rspecLink.getClientId() + " (cm=" + rspecLink.getComponentManagerUrns() + ")");
                    mergedLink = addLink(mergedModel, rspecLink);
//                        assert mergedModel.getLinkByUniqueId(reducedUniqueLinkId) != null;

                    mergedLinks.add(mergedLink);
//                    }
                } else {
                    //nothing to do

//                    //assert: a link must exist which from which the reduced uniqueId matches the current one
//                    boolean linkExistsAlready = false;
//                    for (RspecLink existingLink : mergedModel.getLinks()) {
//                        if (getUniqueIdWithoutSubAuth(existingLink).equals(reducedUniqueLinkId)) {
//                            linkExistsAlready = true;
//                            break;
//                        }
//                    }
//
//                    assert linkExistsAlready :
//                            "Expected to find a link with reduced uniqueLinkId '" + reducedUniqueLinkId + "' in the mergedModel";

                }
            }
        }

        //STEP 2c: finally, add any links from the request that hasn't been seen yet
        if (request != null) {
            ModelRspec modelRspec = request.getModelRspec(modelRspecType);
            if (modelRspec != null) //== null occurs on parse error
                for (RspecLink rspecLink : modelRspec.getLinks()) {
                    /* first reduce the rspecLink.uniqueId to it's proper form. In case of ExoSM, this uniqueId can have
                    changed because of the addition of one or more sub-authorities to a ComponentManagerId
                    */
//                    String reducedUniqueLinkId = getUniqueIdWithoutSubAuth(rspecLink);
                    RspecLink mergedLink = findLink(mergedLinks, rspecLink);

//                    if (!authoritativeLinksByUniqueId.containsKey(reducedUniqueLinkId)) {
                    if (mergedLink == null) {
//                        if (mergedModel.getLinkByUniqueId(reducedUniqueLinkId) == null) {
                        LOG.debug("Adding missing link from (non-authoritative) request: client_id=" + rspecLink.getClientId() + " (cm=" + rspecLink.getComponentManagerUrns() + ")");
                        if (!addNodesOnlyFoundInRequest || !haveManifestForResource(rspecLink.getComponentManagerUrns())) {
                            addLink(mergedModel, rspecLink);
                            //cannot assert this: link unique ID may not be same as reducedUniqueLinkId
//                                assert mergedModel.getLinkByUniqueId(reducedUniqueLinkId) != null :
//                                        "reducedUniqueLinkId="+reducedUniqueLinkId+" not found in "+linkUniqueIdHelper(mergedModel.getLinks());
                        }
//                        }
                    } else {
                        //nothing to do

//                        //assert: a link must exist which from which the reduced uniqueId matches the current one
//                        boolean linkExistsAlready = false;
//                        for (RspecLink existingLink : mergedModel.getLinks()) {
//                            if (getUniqueIdWithoutSubAuth(existingLink).equals(reducedUniqueLinkId)) {
//                                linkExistsAlready = true;
//                                break;
//                            }
//                        }
//
//                        assert linkExistsAlready :
//                                "Expected to find a link with reduced uniqueLinkId '" + reducedUniqueLinkId + "' in the mergedModel";
                    }
                }
        }


        //STEP 2d: remove double componentManagerIds from links (double in the sense that it appears once without and once with subauth)


        for (RspecLink link : mergedModel.getLinks()) {
            //create a copy of the list to prevent the iterator from breaking
            Set<GeniUrn> copyOfComponentManagerUrns = new HashSet<>(link.getComponentManagerUrns());

            for (GeniUrn componentManagerUrn : copyOfComponentManagerUrns) {
                if (componentManagerUrn.hasSubAuthority() && link.getComponentManagerUrns().contains(GeniUrn.removeSubAuth(componentManagerUrn))) {
                    link.getComponentManagerUrns().remove(GeniUrn.removeSubAuth(componentManagerUrn));
                }
            }
        }

        //STEP 3: merge address pools
        //STEP 3a: Merge addresspools from authoritative sources
        for (GeniUrn authorityUrn : sortedSourceUrns) { //order must be fixed for tests to work reliably!
            ManifestRspecSource manifestRspecSource = manifestBySource.get(authorityUrn);
            if (manifestRspecSource == null) {
                //occurs on parse error
                LOG.debug("Skip missing  manifest for " + sortedSourceUrns);
                continue;
            }
            ModelRspec modelRspec = manifestRspecSource.getModelRspec(modelRspecType);
            if (modelRspec == null) {
                //occurs on parse error
                LOG.debug("Skip invalid  manifest for " + sortedSourceUrns);
                continue;
            }
            assert modelRspec.getAddressPools() != null;
            for (AddressPool addressPool : modelRspec.getAddressPools()) {
                if (addressPool.getComponentManagerId() != null) {
                    LOG.debug("Checking authoritative merge for " + authorityUrn + " addresspool id=" + addressPool.getClientId() + " (cm=" + addressPool.getComponentManagerId() + ")");
                    if (isAuthoritative(addressPool.getComponentManagerId(), authorityUrn)) {
                        LOG.debug("    authoritative merge");
                        AddressPool mergedAddressPool = rspecFactory.copyAddressPool(mergedModel, addressPool);

                        mergedModel.addAddressPool(mergedAddressPool);
                    }
                }
            }
        }

        //STEP 3b: merge addresspools from non-authorative sources
        for (GeniUrn authorityUrn : sortedSourceUrns) { //order must be fixed for tests to work reliably!
            ManifestRspecSource manifestRspecSource = manifestBySource.get(authorityUrn);
            if (manifestRspecSource == null) {
                //occurs on parse error
                LOG.debug("Skip missing  manifest for " + sortedSourceUrns);
                continue;
            }
            ModelRspec modelRspec = manifestRspecSource.getModelRspec(modelRspecType);
            if (modelRspec == null) {
                //occurs on parse error
                LOG.debug("Skip invalid  manifest for " + sortedSourceUrns);
                continue;
            }
            assert modelRspec.getAddressPools() != null;
            for (AddressPool addressPool : modelRspec.getAddressPools()) {
                if (mergedModel.getAddressPoolByClientId(addressPool.getClientId()) != null)
                    continue; //already present!

                LOG.debug("Adding missing addresspool from non-authoritative manifest: client_id=" + addressPool.getClientId() + " (cm=" + addressPool.getComponentManagerId() + ")");

                AddressPool mergedAddressPool = rspecFactory.copyAddressPool(mergedModel, addressPool);

                mergedModel.addAddressPool(mergedAddressPool);

            }
        }
        //STEP 3c: merge remaining addresspools from source
        if (request != null) {
            ModelRspec modelRspec = request.getModelRspec(modelRspecType);
            if (modelRspec != null) { //== null occurs on parse error
                for (AddressPool addressPool : modelRspec.getAddressPools()) {
                    if (mergedModel.getAddressPoolByClientId(addressPool.getClientId()) != null)
                        continue; //already present!

                    LOG.debug("Adding missing addresspool from (non-authoritative) request: client_id=" + addressPool.getClientId() + " (cm=" + addressPool.getComponentManagerId() + ")");

                    AddressPool mergedAddressPool = rspecFactory.copyAddressPool(mergedModel, addressPool);

                    mergedModel.addAddressPool(mergedAddressPool);
                }
            }
        }

        //STEP 4: merge extra XML in main RSpec, such as barriers
        if (request != null)

        {
            ModelRspec modelRspec = request.getModelRspec(modelRspecType);
            rspecFactory.mergeExtra(mergedModel, modelRspec);
        }

        for (
                Map.Entry<GeniUrn, ManifestRspecSource> e
                : manifestBySource.entrySet())

        {
            ManifestRspecSource manifestRspecSource = e.getValue();
            if (manifestRspecSource == null) continue;
            ModelRspec modelRspec = manifestRspecSource.getModelRspec(modelRspecType);
            if (modelRspec == null) continue; //occurs on parse error
            rspecFactory.mergeExtra(mergedModel, modelRspec);
        }

        mergedManifest = new

                ManifestRspecSource(mergedModel);

        return mergedManifest;
    }


    public static List<String> linkUniqueIdHelper(List<? extends RspecLink> in) {
        //unsupported java8: mergedModel.getLinks().stream().map(RspecLink::getUniqueId).collect(Collectors.toList())
        List<String> res = new ArrayList<>();
        for (RspecLink l : in) {
            res.add(l.getUniqueId());
        }
        return res;
    }

    private static String getUniqueIdWithoutSubAuth(RspecNode rspecNode) {
        return BasicStringRspec.makeUniqueNodeId(rspecNode.getClientId(),
                rspecNode.getComponentId() != null ?
                        rspecNode.getComponentId().getValue() : null,
                GeniUrn.removeSubAuth(rspecNode.getComponentManagerId()).getValue());
    }

    private static String getUniqueIdWithoutSubAuth(RspecLink rspecLink) {
        Set<String> componentManagerWithoutSubAuthUrnStrings = new TreeSet<>();
        for (GeniUrn componentManagerUrn : rspecLink.getComponentManagerUrns()) {
            componentManagerWithoutSubAuthUrnStrings.add(GeniUrn.removeSubAuth(componentManagerUrn).getValue());
        }

        return BasicStringRspec.makeUniqueLinkId(rspecLink.getClientId(),
                null,
                componentManagerWithoutSubAuthUrnStrings);

    }

    /**
     * returns a link in the merged model that matches this link, or null if there is none.
     * <p/>
     * To match, a link must have the same clientId, and at least one component_manager must be the same.
     * Reason:
     * - handle component_managers added by SCS
     * - handle AM's that return only their "part" of the link  (exogeni for example removes all non-exogeni nodes and removes non-exogeni component_managers from links)
     */
    private static RspecLink findLink(List<RspecLink> mergedLinks, RspecLink searchedLink) {
        Set<String> seachedCms = new TreeSet<>();
        for (GeniUrn componentManagerUrn : searchedLink.getComponentManagerUrns()) {
            seachedCms.add(GeniUrn.removeSubAuth(componentManagerUrn).getValue());
        }

        for (RspecLink mergedLink : mergedLinks) {
            if (!Objects.equals(mergedLink.getClientId(), searchedLink.getClientId()))
                continue; //client_id MUST always match

            Set<String> mergedLinkCms = new HashSet<>();
            for (GeniUrn componentManagerUrn : mergedLink.getComponentManagerUrns()) {
                mergedLinkCms.add(GeniUrn.removeSubAuth(componentManagerUrn).getValue());
            }

            if (!Collections.disjoint(mergedLinkCms, seachedCms))
                return mergedLink;
        }
        return null;
    }


    private RspecLink addLink(ModelRspec mergedModel, RspecLink origLink) {
        //interfaces are NOT copied. They should have already been created when copying the nodes, so we should look them up and link them
        RspecLink mergedLink = rspecFactory.copyLink(mergedModel, origLink, RspecNode.InterfaceCopyMethod.NO_COPY);
        assert mergedLink.getInterfaces().isEmpty();
        assert Objects.equals(origLink.getUniqueId(), mergedLink.getUniqueId());
        mergedModel.addLink(mergedLink);

        //check if node of the links interfaces can be found, and bind it if so
        for (RspecInterface origInterface : origLink.getInterfaces()) {
            assert mergedLink.getInterfaceByUniqueId(origInterface.getUniqueId()) == null;

            RspecInterface mergedInterface = mergedModel.getInterfaceByUniqueId(origInterface.getUniqueId());

            if (mergedInterface != null) {
                assert !mergedInterface.isNodeUnbound();
                assert mergedInterface.isLinkUnbound();

                mergedInterface.bindLink(mergedLink);
            }

//            if (mergedInterface != null && !origInterface.isNodeUnbound() && mergedInterface.isNodeUnbound()) {
//                String origIfaceNodeId = origInterface.getNode().getUniqueId();
//                RspecNode mergedIfaceNode = mergedModel.getNodeByUniqueId(origIfaceNodeId);
//                if (mergedIfaceNode != null)
//                    mergedInterface.bindNode(mergedIfaceNode);
//            }
        }
        return mergedLink;
    }
}
