package be.iminds.ilabt.jfed.rspec.model.impl;

import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Server;
import be.iminds.ilabt.jfed.lowlevel.TestbedInfoSource;
import be.iminds.ilabt.jfed.lowlevel.resourceid.ResourceUrn;
import be.iminds.ilabt.jfed.rspec.model.*;
import be.iminds.ilabt.jfed.rspec.parser.ExtraXml;
import be.iminds.ilabt.jfed.util.GeniUrn;
import be.iminds.ilabt.jfed.util.TextUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.stream.Collectors;

/**
 * BasicRspecLink
 */
public class BasicRspecLink implements RspecLink {
    private final static Logger LOG = LoggerFactory.getLogger(BasicRspecLink.class);

    protected final List<ExtraXml> extraXml = new ArrayList<>();
    private final List<GeniUrn> componentManagerUrns = new ArrayList<>();
    private final List<String> linkTypes = new ArrayList<>();
    private final List<BasicRspecInterface> interfaces = new ArrayList<>();
    private final List<BasicLinkSetting> linkSettings = new ArrayList<>();
    private String clientId;
    private GeniUrn sliverId;
    private String sharedLanName;
    private final BasicModelRspec modelRspec;

    private final List<String> autoSharedLanNames = new ArrayList<>();

    private Boolean vlanTagging;
    private Integer vlanTag;
    private Boolean noMacLearning;
    private Boolean linkMultiplexing;
    private Boolean bestEffort;
    private Boolean interswitchDisallow;
    private String noMacLearningValueString;

    BasicRspecLink(BasicModelRspec modelRspec, String clientId, String addDefaultLinkType) {
        this.modelRspec = modelRspec;
        this.clientId = clientId;
        if (addDefaultLinkType != null)
            linkTypes.add(addDefaultLinkType);
    }

    public BasicRspecLink(BasicModelRspec newModelRspec, RspecLink other, RspecNode.InterfaceCopyMethod interfaceCopyMethod) {
        this(newModelRspec, other.getClientId(), null);

        this.extraXml.addAll(other.getExtraXml());
        setSliverId(other.getSliverId());
        this.componentManagerUrns.addAll(other.getComponentManagerUrns());
        this.linkTypes.addAll(other.getLinkTypes());

        this.sharedLanName = other.getSharedLan();
        this.autoSharedLanNames.clear();
        this.autoSharedLanNames.addAll(other.getAutoShareLanNames());
        this.vlanTagging = other.hasVlanTaggingTag() ? other.isVlanTaggingEnabled() : null;
        this.vlanTag = other.getVlanTag();
        this.noMacLearning = other.hasNoMacLearningTag() ? other.isNoMacLearning() : null;
        this.noMacLearningValueString = other.hasNoMacLearningTag() ? other.getNoMacLeaningValueString() : null;
        this.bestEffort = other.hasBestEffortTag() ? other.isBestEffortEnabled() : null;
        this.linkMultiplexing = other.hasLinkMultiplexingTag() ? other.isLinkMultiplexingEnabled() : null;
        this.interswitchDisallow = other.hasInterswitchDisallowTag() ? other.isInterswitchDisallowed() : null;

        //first copy interfaces
        if (Objects.equals(interfaceCopyMethod, RspecNode.InterfaceCopyMethod.CREATE_NEW_INTERFACE)) {
            copyInterfaces(other);
        }
        if (Objects.equals(interfaceCopyMethod, RspecNode.InterfaceCopyMethod.STEAL_INTERFACE)) {
            stealInterfaces(other);
        }

        if (interfaceCopyMethod.equals(RspecNode.InterfaceCopyMethod.CREATE_NEW_INTERFACE) || interfaceCopyMethod.equals(RspecNode.InterfaceCopyMethod.STEAL_INTERFACE)) {
            updateLinkSettings();
            //then copy linksettings
            for (LinkSetting otherLinkSetting : other.getLinkSettings()) {
                BasicLinkSetting linkSetting =
                        getLinkSettingByClientIds(otherLinkSetting.getFromIface().getClientId(), otherLinkSetting.getToIface().getClientId());
                assert linkSetting != null :
                        "no linksetting for from=" + otherLinkSetting.getFromIface().getClientId() + " to=" + otherLinkSetting.getToIface().getClientId();
                linkSetting.setSameAs(otherLinkSetting);
            }
        }
    }

    public void copyInterfaces(RspecLink rspecLink) {
        for (RspecInterface rspecInterface : rspecLink.getInterfaces()) {
            //clone interface
            BasicRspecInterface res;
            if (rspecLink.getRspec() == getRspec())
                res =
                        new BasicRspecInterface((BasicRspecNode) rspecInterface.getNode(), this, BasicRspecNode.generateNewInterfaceId(rspecInterface));
            else
                res =
                        new BasicRspecInterface(this, rspecInterface.getClientId()); //since we copy to new model, just copy interface name
            res.overwritePropertiesWith(rspecInterface, true);
        }
    }

    public void stealInterfaces(RspecLink rspecLink) {
        List<RspecInterface> origIfaces = new ArrayList<>(rspecLink.getInterfaces());
        for (RspecInterface rspecInterface : origIfaces) {
            rspecInterface.moveToLink(this);
        }
    }

    @Override
    public void overwritePropertiesWith(RspecLink other, boolean overwriteWithNull) {
        assert Objects.equals(this.clientId, other.getClientId());

        if (overwriteWithNull) {
            this.sliverId = other.getSliverId();
            this.componentManagerUrns.clear();
            this.componentManagerUrns.addAll(other.getComponentManagerUrns());
            this.linkTypes.clear();
            this.linkTypes.addAll(other.getLinkTypes());
            for (LinkSetting ls : other.getLinkSettings())
                this.getLinkSettingByClientIds(ls.getFromIface().getClientId(), ls.getToIface().getClientId()).setSameAs(ls);

            this.extraXml.clear();
            this.extraXml.addAll(other.getExtraXml());


            //not needed, overwritePropertiesWith is only used for manifest merge:
//            this.sharedLanName = other.getSharedLan();
//            this.autoSharedLanNames.clear();
//            this.autoSharedLanNames.addAll(other.getAutoShareLanNames());
//            this.vlanTagging = other.hasVlanTaggingTag() ? other.isVlanTaggingEnabled() : null;
//            this.vlanTag = other.getVlanTag();
//            this.noMacLearning = other.hasNoMacLearningTag() ? other.isNoMacLearning() : null;
//            this.noMacLearningValueString = other.hasNoMacLearningTag() ? other.getNoMacLeaningValueString() : null;
//            this.bestEffort = other.hasBestEffortTag() ? other.isBestEffortEnabled() : null;
//            this.linkMultiplexing = other.hasLinkMultiplexingTag() ? other.isLinkMultiplexingEnabled() : null;
        } else {
            if (other.getSliverId() != null)
                this.sliverId = other.getSliverId();
            other.getLinkSettings().stream()
                    .filter((Predicate<LinkSetting>) LinkSetting::isActive)
                    .forEach(ls -> {
                        BasicLinkSetting ols =
                                this.getLinkSettingByClientIds(ls.getFromIface().getClientId(), ls.getToIface().getClientId());
                        if (ols != null) {
                            ols.setSameAs(ls);
                        } else {
                            BasicRspecInterface fromIface =
                                    this.getInterfaceByClientId(ls.getFromIface().getUniqueId());
                            BasicRspecInterface toIface = this.getInterfaceByUniqueId(ls.getToIface().getUniqueId());
                            if (fromIface != null && toIface != null) {
                                BasicLinkSetting newLs = getLinkSetting(fromIface, toIface);
                                assert newLs != null :
                                        "link settings not found for " + ls.getFromIface().getClientId() + " " + ls.getToIface().getClientId();
                                newLs.setSameAs(ls);
                            }
                        }
                    });
            addMissing(this.componentManagerUrns, other.getComponentManagerUrns());
            addMissing(this.linkTypes, other.getLinkTypes());
            addMissing(this.extraXml, other.getExtraXml()); //works due to custom comparison in EventListExtraXml


            //not needed, overwritePropertiesWith is only used for manifest merge:
//            if (other.getSharedLan() != null)
//                this.sharedLanName = other.getSharedLan();
//            addMissing(this.autoSharedLanNames, other.getAutoShareLanNames());
//            if (other.hasVlanTaggingTag() )
//                this.vlanTagging = other.isVlanTaggingEnabled();
//            if (other.getVlanTag() != null)
//                this.vlanTag = other.getVlanTag();
//            if (other.hasNoMacLearningTag()) {
//                this.noMacLearning = other.isNoMacLearning();
//                this.noMacLearningValueString = other.getNoMacLeaningValueString();
//            }
//            if (other.hasBestEffortTag())
//                this.bestEffort =other.isBestEffortEnabled();
//            if (other.hasLinkMultiplexingTag())
//                this.linkMultiplexing = other.isLinkMultiplexingEnabled();
        }
    }

    private static <T> void addMissing(List<T> target, List<T> other) {
        other.stream().filter(o -> !target.contains(o)).forEach(target::add);
    }

    @Override
    public BasicModelRspec getRspec() {
        return modelRspec;
    }

    @Override
    public String getUniqueId() {
        return BasicStringRspec.makeUniqueLinkId(clientId, null, getComponentManagerUrnStrings());
    }

    public List<String> getComponentManagerUrnStrings() {
        return componentManagerUrns.stream().map(ResourceUrn::getValue).collect(Collectors.toList());
    }

    @Override
    public String getClientId() {
        return clientId;
    }

    @Override
    public void setClientId(String value) {
        this.clientId = value;
    }

    @Override
    public List<GeniUrn> getComponentManagerUrns() {
        return componentManagerUrns;
    }

    @Override
    public List<String> getLinkTypes() {
        return linkTypes;
    }

    @Override
    public List<? extends LinkSetting> getLinkSettings() {
        updateLinkSettings();
        return linkSettings;
    }

    @Override
    public BasicLinkSetting getLinkSetting(RspecInterface fromIface, RspecInterface toIface) {
        assert fromIface != null;
        assert fromIface instanceof BasicRspecInterface;
        assert toIface != null;
        assert toIface instanceof BasicRspecInterface;
        //this one only makes sense within the same modelRspec
        assert fromIface.getRspec() == getRspec();
        assert toIface.getRspec() == getRspec();

        updateLinkSettings();
        assert interfaces.contains(fromIface);
        assert interfaces.contains(toIface);
        for (BasicLinkSetting ls : linkSettings) {
            if (ls.getFromIface() == fromIface && ls.getToIface() == toIface)
                return ls;
        }
        return null;
    }

    @Override
    public BasicLinkSetting getLinkSettingByClientIds(String fromIfaceName, String toIfaceName) {
        updateLinkSettings();
        for (BasicLinkSetting ls : linkSettings) {
            if (Objects.equals(ls.getFromIface().getClientId(), fromIfaceName) &&
                    Objects.equals(ls.getToIface().getClientId(), toIfaceName))
                return ls;
        }
        return null;
    }

    @Override
    public BasicLinkSetting getLinkSettingByUniqueIds(String fromIfaceName, String toIfaceName) {
        updateLinkSettings();
        for (BasicLinkSetting ls : linkSettings) {
            if (Objects.equals(ls.getFromIface().getUniqueId(), fromIfaceName) &&
                    Objects.equals(ls.getToIface().getUniqueId(), toIfaceName))
                return ls;
        }
        return null;
    }

    /**
     * remove unneeded link settings, and add all needed link settings
     */
    private void updateLinkSettings() {
        //remove unneeded link settings
        for (Iterator<BasicLinkSetting> it = linkSettings.iterator(); it.hasNext(); ) {
            BasicLinkSetting ls = it.next();
            if (!interfaces.contains(ls.getFromIface()) || !interfaces.contains(ls.getToIface())) {
                //remove linksetting
                it.remove();
            }
            if (ls.getFromIface() == ls.getToIface()) {
                //remove linksetting
                it.remove();
            }
        }

        //add all missing needed link settings
        for (BasicRspecInterface fromIface : interfaces) {
            for (BasicRspecInterface toIface : interfaces) {
                if (fromIface == toIface)
                    continue;
                boolean missing = true;
                for (BasicLinkSetting ls : linkSettings)
                    if (ls.getFromIface() == fromIface && ls.getToIface() == toIface) {
                        missing = false;
                        break;
                    }
                if (missing) {
                    BasicLinkSetting newLinkSetting = new BasicLinkSetting(fromIface, toIface);
                    linkSettings.add(newLinkSetting);
                }
            }
        }
    }


    @Override
    public boolean isImpaired() {
        for (BasicLinkSetting ls : linkSettings) {
            if (ls.isActive())
                return true;
        }
        return false;
    }

    protected boolean addInterface(RspecInterface iface) {
        assert iface instanceof BasicRspecInterface;
        return interfaces.add((BasicRspecInterface) iface);
    }

    protected boolean removeInterface(RspecInterface iface) {
        assert iface instanceof BasicRspecInterface;
        return interfaces.remove(iface);
    }

    @Override
    public List<BasicRspecInterface> getInterfaces() {
        return interfaces;
    }

    @Override
    public BasicRspecInterface getInterfaceForNode(RspecNode node) {
        assert node instanceof BasicRspecNode;
        for (BasicRspecInterface iface : interfaces) {
            if (iface.getNode() == node)
                return iface;
        }
        return null;
    }

    @Override
    public BasicRspecInterface getInterfaceByClientId(String clientId) {
        for (BasicRspecInterface iface : interfaces) {
            if (Objects.equals(iface.getClientId(), clientId))
                return iface;
        }
        return null;
    }

    @Override
    public BasicRspecInterface getInterfaceByUniqueId(String uniqueId) {
        for (BasicRspecInterface iface : interfaces) {
            if (Objects.equals(iface.getUniqueId(), uniqueId))
                return iface;
        }
        return null;
    }

    @Override
    public GeniUrn getSliverId() {
        return sliverId;
    }

    @Override
    public void setSliverId(GeniUrn sliverId) {
        this.sliverId = sliverId;
    }

    @Override
    public void setSliverId(String sliverId) {
        if (sliverId == null) {
            this.sliverId = null;
            return;
        }

        GeniUrn geniUrn = GeniUrn.parse(sliverId);

        if (geniUrn != null) {
            this.sliverId = geniUrn;
        } else {
            LOG.warn("Could not set sliverId to {} as it is not a valid URN!", sliverId);
            this.sliverId = null;
        }
    }

    @Override
    public boolean isStitched(TestbedInfoSource testbedInfoSource) {
        if (getComponentManagerUrns().size() < 2) return false;

        //only links with type "lan" or links without type can be stitching
        if (getLinkTypes().size() > 1 ||
                (getLinkTypes().size() == 1 && !(getLinkTypes().get(0).equalsIgnoreCase("lan") || getLinkTypes().get(0).equalsIgnoreCase("vlan")))
                )
            return false;

        if (testbedInfoSource == null) return true;

        Server firstServer = null;
        for (GeniUrn urn : getComponentManagerUrns()) {
            //TODO this is actually more complex, depends on exogeni setting setc
            Server server =
                    testbedInfoSource.getByUrn(urn, TestbedInfoSource.SubAuthMatchAllowed.ALLOW_TOPLEVEL, TestbedInfoSource.SubAuthMatchPreference.PREFER_TOPLEVEL);
            if (server == null) {
                LOG.warn("Authority not found for " + urn + " during stitch check -> falling back to assuming link is stitched");
                return true; //fall back to assuming it is stitched, if any authority is not found
            }
            if (firstServer == null)
                firstServer = server;
            else {
                if (!Objects.equals(firstServer.getDefaultComponentManagerUrn(), server.getDefaultComponentManagerUrn()))
                    return true; //authorities have different URN, so stitched link
            }
        }
        return false;
    }

    @Override
    public List<ExtraXml> getExtraXml() {
        return extraXml;
    }

    @Override
    public String getSharedLan() {
        return sharedLanName;
    }

    @Override
    public void setSharedLan(String sharedLanName) {
        this.sharedLanName = sharedLanName;
    }


    @Override
    public boolean isVlanTaggingEnabled() {
        return vlanTagging != null && vlanTagging;
    }

    @Override
    public boolean hasVlanTaggingTag() {
        return vlanTagging != null;
    }

    @Override
    public void setVlanTagging(Boolean value) {
        vlanTagging = value;
    }

    @Override
    public Integer getVlanTag() {
        return vlanTag;
    }

    @Override
    public void setVlanTag(Integer vlanTag) {
        this.vlanTag = vlanTag;
    }

    @Override
    public boolean isNoMacLearning() {
        return noMacLearning != null && noMacLearning;
    }

    @Override
    public boolean hasNoMacLearningTag() {
        return noMacLearning != null;
    }

    @Override
    public void setNoMacLearning(Boolean value) {
        noMacLearning = value;
        noMacLearningValueString = value == null ? null : value + "";
    }

    @Override
    public String getNoMacLeaningValueString() {
        if (noMacLearning == null) return null;
        return noMacLearningValueString;
    }

    @Override
    public void setNoMacLearning(String value) {
        noMacLearningValueString = value;
        noMacLearning = TextUtil.stringToBoolean(value);
    }

    @Override
    public boolean isBestEffortEnabled() {
        return bestEffort != null && bestEffort;
    }

    @Override
    public boolean hasBestEffortTag() {
        return bestEffort != null;
    }

    @Override
    public void setBestEffort(@Nullable Boolean value) {
        bestEffort = value;
    }

    @Override
    public boolean isLinkMultiplexingEnabled() {
        return linkMultiplexing != null && linkMultiplexing;
    }

    @Override
    public boolean hasLinkMultiplexingTag() {
        return linkMultiplexing != null;
    }

    @Override
    public void setLinkMultiplexing(@Nullable Boolean value) {
        linkMultiplexing = value;
    }


    @Override
    public boolean hasAutoShareLan() {
        return !autoSharedLanNames.isEmpty();
    }

    @Override
    public List<String> getAutoShareLanNames() {
        return autoSharedLanNames;
    }

    @Override
    public boolean isInterswitchDisallowed() {
        return interswitchDisallow != null && interswitchDisallow;
    }

    @Override
    public boolean hasInterswitchDisallowTag() {
        return interswitchDisallow != null;
    }

    @Override
    public void setInterswitchDisallow(@Nullable Boolean value) {
        this.interswitchDisallow = value;
    }
}
