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

import be.iminds.ilabt.jfed.lowlevel.AbstractApi;
import be.iminds.ilabt.jfed.lowlevel.BadReplyGeniException;
import be.iminds.ilabt.jfed.util.TextUtil;
import be.iminds.ilabt.jfed.util.XmlUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import java.util.*;
import java.util.stream.Collectors;

/**
 * StitchingHopData contains all available data about a stitching hop
 * This data comes from:
 * - Rspec stitching extension
 * - SCS call workflow
 * <p/>
 * The hop URN+linkName is the unique identifier that binds the 2 data sources.
 * <p/>
 * It also stores dependencies to other StitchingHopData. To create:
 * - first create it, by giving workflowdata and full Rspec
 * - then optionally process additional workflow data of this same hop,
 * - and then call linkDeps
 */
public class StitchingHopData {
    private final Logger log;

    //workflow data
    private final String linkName;
    private final String hopUrn;
    private final String aggregateUrn;
    private final String aggregateUrl;
    private final Boolean importVlans;

    //rspec data
    private Element hopElement;
    private Element scSpecificInfo_L2scEl;
    private String hopElId;

    public enum StitchingExtensionSource {SCS_REQUEST, MANIFEST}

    private StitchingExtensionSource stitchingExtensionSource;


    private String scsVlanRangeAvailability;
    private String scsSuggestedVLANRange;

    private String lastManifestVlanRangeAvailability;
    private String lastManifestSuggestedVLANRange;

    private String advertisementVlanRangeAvailability;

    private final Set<Integer> unavailableVlans = new HashSet<>();
    //    private VlanRangeHelper availableVlans;
    private Integer suggestedVlan;

    //dependencies in workflow(s)
    private final List<Map> rawDependencies = new ArrayList<>();

    //links (initialised after linkDeps call)
    private final List<StitchingHopData> dependencies = new ArrayList<>();
    private final List<StitchingHopData> dependingOnThis = new ArrayList<>();


    public StitchingHopData(String linkName, Map<String,Object> workflowData, Element stitchingElement, Logger log) throws BadReplyGeniException {
        if (log == null)
            this.log = LoggerFactory.getLogger(StitchingHopData.class);
        else
            this.log = log;
        this.linkName = linkName;
        hopUrn = AbstractApi.apiSpecifiesStringInMap(workflowData, "hop_urn");
        assert hopUrn != null;

        aggregateUrn = AbstractApi.apiSpecifiesStringInMap(workflowData, "aggregate_urn");
        aggregateUrl = AbstractApi.apiSpecifiesStringInMap(workflowData, "aggregate_url");
        importVlans = AbstractApi.apiSpecifiesBoolean(workflowData.get("import_vlans"));

        if (workflowData.get("dependencies") != null) {
            List<Map> dependenciesList = AbstractApi.apiSpecifiesListOfT(Map.class, workflowData.get("dependencies"));
            for (Map o : dependenciesList)
                rawDependencies.add(o);
        }

        //search for matching rspec hop data
        stitchingExtensionSource = StitchingExtensionSource.SCS_REQUEST;
        processRspecElement(stitchingElement, stitchingExtensionSource);
        assert this.hopElement != null;
    }

    public void processExtraWorkflowData(String linkName, Map<String,Object> workflowData) throws BadReplyGeniException {
        assert Objects.equals(this.linkName, linkName);
        String otherHopUrn = AbstractApi.apiSpecifiesStringInMap(workflowData,"hop_urn");
        assert otherHopUrn != null;
        assert Objects.equals(otherHopUrn, hopUrn);

        String otherAggregateUrn = AbstractApi.apiSpecifiesStringInMap(workflowData,"aggregate_urn");
        String otherAggregateUrl = AbstractApi.apiSpecifiesStringInMap(workflowData,"aggregate_url");
        Boolean otherImportVlans = AbstractApi.apiSpecifiesBoolean(workflowData.get("import_vlans"));
        assert Objects.equals(otherAggregateUrn, aggregateUrn);
        assert Objects.equals(otherAggregateUrl, aggregateUrl);
        assert Objects.equals(otherImportVlans, importVlans);

        assert (workflowData.get("dependencies") == null);
    }

    public void linkDeps(Collection<StitchingHopData> allData) throws BadReplyGeniException {
        for (Map workflowData : rawDependencies) {
            String depHopUrn = AbstractApi.apiSpecifiesStringInMap(workflowData,"hop_urn");

            for (StitchingHopData other : allData)
                if (Objects.equals(other.hopUrn, depHopUrn) && Objects.equals(other.linkName, linkName)) {
                    if (!dependencies.contains(other)) {
                        dependencies.add(other);
                        other.dependingOnThis.add(this);
                    }
                }
        }
    }

    void exogeniBugWorkAround_registerManifestVlan(int vlan) {
        assert this.hopElement != null : "There was no scs hop element";
        stitchingExtensionSource = StitchingExtensionSource.MANIFEST;
        this.lastManifestVlanRangeAvailability = vlan + "";
        this.lastManifestSuggestedVLANRange = vlan + "";

    }

    void overwriteWithManifestRspec(String manifestRspec, Element stitchingElement) {
        assert stitchingElement != null;

        this.hopElement = null;

        stitchingExtensionSource = StitchingExtensionSource.MANIFEST;
        processRspecElement(stitchingElement, StitchingExtensionSource.MANIFEST);
        assert this.hopElement != null;
    }

    @Deprecated
    boolean isSameAs(String otherManifestRspec) {
        assert this.hopElement != null;

        Element otherStitchingElement = StitchingData.extractStitchingElementFromRspec(otherManifestRspec, log);

        //search for matching rspec hop data
        List<Element> otherPathEls =
                XmlUtil.getDomElementChildElementsNS(otherStitchingElement, StitchingData.NAMESPACE_STITCHING, "path");
        for (Element otherPathEl : otherPathEls) {
            String otherLinkId = otherPathEl.getAttribute("id");
            assert otherLinkId != null : "<stitching> has <path> element without id attribute: " + otherPathEl;

            if (!Objects.equals(otherLinkId, linkName))
                continue;

            NodeList hopList = otherPathEl.getElementsByTagNameNS(StitchingData.NAMESPACE_STITCHING, "hop");
            for (int i = 0; i < hopList.getLength(); i++) {
                Node otherHopNode = hopList.item(i);
                assert otherHopNode != null;
                assert otherHopNode.getNodeType() == Node.ELEMENT_NODE;
                Element otherHopElement = (Element) otherHopNode;

                Element otherLinkEl =
                        XmlUtil.getDomElementFirstChildElementNS(otherHopElement, StitchingData.NAMESPACE_STITCHING, "link");

                String otherLinkIdUrn = otherLinkEl.getAttribute("id");
                if (!Objects.equals(otherLinkIdUrn, hopUrn))
                    continue;

                Element otherScDescriptorEl =
                        XmlUtil.getDomElementFirstChildElementNS(otherLinkEl, StitchingData.NAMESPACE_STITCHING, "switchingCapabilityDescriptor");
                Element otherScSpecificInfoEl =
                        XmlUtil.getDomElementFirstChildElementNS(otherScDescriptorEl, StitchingData.NAMESPACE_STITCHING, "switchingCapabilitySpecificInfo");
                Element otherScSpecificInfo_L2scEl =
                        XmlUtil.getDomElementFirstChildElementNS(otherScSpecificInfoEl, StitchingData.NAMESPACE_STITCHING, "switchingCapabilitySpecificInfo_L2sc");
                Element otherVlanRangeAvailabilityEl =
                        XmlUtil.getDomElementFirstChildElementNS(otherScSpecificInfo_L2scEl, StitchingData.NAMESPACE_STITCHING, "vlanRangeAvailability");
                Element otherSuggestedVLANRangeEl =
                        XmlUtil.getDomElementFirstChildElementNS(otherScSpecificInfo_L2scEl, StitchingData.NAMESPACE_STITCHING, "suggestedVLANRange");

                String otherHopElId = otherHopElement.getAttribute("id");
                String otherManifestVlanRangeAvailability = otherVlanRangeAvailabilityEl.getTextContent();
                String otherManifestSuggestedVLANRange = otherSuggestedVLANRangeEl.getTextContent();

                if (!otherHopElId.trim().equalsIgnoreCase(this.hopElId.trim())) {
                    log.debug("isSameAs()=false because otherHopElId=" + otherHopElId + " hopElId=" + this.hopElId);
                    return false;
                }
                if (!otherManifestVlanRangeAvailability.trim().equalsIgnoreCase(this.lastManifestVlanRangeAvailability.trim())) {
                    log.debug("isSameAs()=false because otherManifestVlanRangeAvailability=" + otherManifestVlanRangeAvailability +
                            " lastManifestVlanRangeAvailability=" + this.lastManifestVlanRangeAvailability);
                    return false;
                }
                if (!otherManifestSuggestedVLANRange.trim().equalsIgnoreCase(this.lastManifestSuggestedVLANRange.trim())) {
                    log.debug("isSameAs()=false because otherManifestSuggestedVLANRange=" + otherManifestSuggestedVLANRange +
                            " lastManifestSuggestedVLANRange=" + this.lastManifestSuggestedVLANRange);
                    return false;
                }
                log.debug("isSameAs()=true because suggested and available match");
                return true;
            }
        }
        log.debug("isSameAs()=false because no matching hopEl found in new manifest");
        return false;
    }

    void processAdvertismentInfo(String advertisementVlanRangeAvailability) {
        this.advertisementVlanRangeAvailability = advertisementVlanRangeAvailability;
    }

    void processRspecElement(Element stitchingElement, StitchingExtensionSource stitchingExtensionSource) {
        assert stitchingElement != null;
        assert this.hopElement == null :
                "Bug: clear this.hopElement before calling to acknowledge overwrite";

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

            if (!Objects.equals(linkId, linkName))
                continue;

            NodeList hopList = pathEl.getElementsByTagNameNS(StitchingData.NAMESPACE_STITCHING, "hop");
            for (int i = 0; i < hopList.getLength(); i++) {
                Node hopNode = hopList.item(i);
                assert hopNode != null;
                assert hopNode.getNodeType() == Node.ELEMENT_NODE;
                Element hopElement = (Element) hopNode;

                Element linkEl =
                        XmlUtil.getDomElementFirstChildElementNS(hopElement, StitchingData.NAMESPACE_STITCHING, "link");

                String linkIdUrn = linkEl.getAttribute("id");
                if (!Objects.equals(linkIdUrn, hopUrn))
                    continue;

                assert this.hopElement == null :
                        "Found multiple <link> in rspec matching path id=\"" + linkName + "\"  hop urn=\"" + hopUrn + "\"";

                Element scDescriptorEl =
                        XmlUtil.getDomElementFirstChildElementNS(linkEl, StitchingData.NAMESPACE_STITCHING, "switchingCapabilityDescriptor");
                Element scSpecificInfoEl =
                        XmlUtil.getDomElementFirstChildElementNS(scDescriptorEl, StitchingData.NAMESPACE_STITCHING, "switchingCapabilitySpecificInfo");
                this.scSpecificInfo_L2scEl =
                        XmlUtil.getDomElementFirstChildElementNS(scSpecificInfoEl, StitchingData.NAMESPACE_STITCHING, "switchingCapabilitySpecificInfo_L2sc");
                Element vlanRangeAvailabilityEl =
                        XmlUtil.getDomElementFirstChildElementNS(scSpecificInfo_L2scEl, StitchingData.NAMESPACE_STITCHING, "vlanRangeAvailability");
                Element suggestedVLANRangeEl =
                        XmlUtil.getDomElementFirstChildElementNS(scSpecificInfo_L2scEl, StitchingData.NAMESPACE_STITCHING, "suggestedVLANRange");

                this.hopElId = hopElement.getAttribute("id");

                if (stitchingExtensionSource == StitchingExtensionSource.SCS_REQUEST) {
                    this.scsVlanRangeAvailability = vlanRangeAvailabilityEl.getTextContent();
                    this.scsSuggestedVLANRange = suggestedVLANRangeEl.getTextContent();
                }
                if (stitchingExtensionSource == StitchingExtensionSource.MANIFEST) {
                    this.lastManifestVlanRangeAvailability = vlanRangeAvailabilityEl.getTextContent();
                    this.lastManifestSuggestedVLANRange = suggestedVLANRangeEl.getTextContent();
                }

                this.hopElement = hopElement;
            }
        }

        assert this.hopElement != null :
                "Found NO <link> in rspec matching path id=\"" + linkName + "\"  hop urn=\"" + hopUrn + "\"";
    }

    /**
     * Updates the suggested VLAN of this hop, by:
     * - removing all unavailableVlans from availableVLans
     * - finding an available VLAN and setting it as suggestedVlan
     * - if suggestedVlan is not null, uses that
     * <p/>
     * returns found new VLAN or null if none found
     * <p/>
     * if none is found, nothing is updated!
     */
    public Integer updateSuggestedVlan(Set<Integer> unavailableVlans, Integer newSuggestedVlan) {
        log.trace("updateSuggestedVlan({}, {})", this.getHopUrn(), unavailableVlans);

        this.unavailableVlans.addAll(unavailableVlans);
        VlanRange availableVlans = getAvailableVlans();
        assert Collections.disjoint(unavailableVlans, availableVlans);
        if (availableVlans.isEmpty()) {
            suggestedVlan = null;
            return null;
        }

        if (newSuggestedVlan != null) {
            if (unavailableVlans.contains(newSuggestedVlan)) {
                log.warn("updateSuggestedVlan(unavailableVlans=" + unavailableVlans + ", newSuggestedVlan=" + newSuggestedVlan + ") " +
                        "called with newSuggestedVlan element of unavailableVlans");
                return null; //newSuggestedVlan is not available!
            }
            if (this.unavailableVlans.contains(newSuggestedVlan)) {
                log.warn("updateSuggestedVlan(unavailableVlans=" + unavailableVlans + ", newSuggestedVlan=" + newSuggestedVlan + ") " +
                        "this.unavailableVlans=" + this.unavailableVlans + " called with newSuggestedVlan element of this.unavailableVlans");
                return null; //newSuggestedVlan is not available!
            }
            suggestedVlan = newSuggestedVlan;
        }

        Integer res = getSuggestedVlan();

        assert Collections.disjoint(unavailableVlans, availableVlans);
        assert !unavailableVlans.contains(res);
        assert !this.unavailableVlans.contains(res);
        log.debug("Chose vlan " + res);
        return res;
    }


    //Getters
    public List<StitchingHopData> getDependencies() {
        return dependencies;
    }

    public List<StitchingHopData> getDependingOnThis() {
        return dependingOnThis;
    }

    public String getLinkName() {
        return linkName;
    }

    public String getHopUrn() {
        return hopUrn;
    }

    public String getAggregateUrn() {
        return aggregateUrn;
    }

    public String getAggregateUrl() {
        return aggregateUrl;
    }

    public Boolean getImportVlans() {
        return importVlans;
    }

    public Set<Integer> getUnavailableVlans() {
        return Collections.unmodifiableSet(unavailableVlans);
    }

    /**
     * get available VLans from the best available source.
     */
    public VlanRange getAvailableVlans() {
        assert unavailableVlans != null;
        VlanRange availableVlans = null;
        if (advertisementVlanRangeAvailability != null)
            availableVlans = new VlanRange(advertisementVlanRangeAvailability);
        if (availableVlans == null && scsVlanRangeAvailability != null)
            availableVlans = new VlanRange(scsVlanRangeAvailability);
//        never believe a manifest unless there really is no other info  (a manifest sets the vlans to just the suggested! See also FEDIBBTDEV-1411)
        if (availableVlans == null && lastManifestVlanRangeAvailability != null) {
            assert false : "This should not occur";
            availableVlans = new VlanRange(lastManifestVlanRangeAvailability);
            log.warn("Using manifest VLAN data. This is not ever a good source, something must be wrong with the SCS data?");
        }
        if (availableVlans == null)
            throw new RuntimeException("No data on available VLANs known");
        availableVlans.removeAll(unavailableVlans);

        //check everything that depends on this. And remove all VLANs that are not shared.
        VlanRange availableVlansWithDepends = new VlanRange(availableVlans);
        assert Objects.equals(availableVlans, availableVlansWithDepends) :
                "availableVlans=" + availableVlans + " availableVlansWithDepends=" + availableVlansWithDepends;
        assert Collections.disjoint(unavailableVlans, availableVlans);
        assert Collections.disjoint(unavailableVlans, availableVlansWithDepends);
        assert Objects.equals(availableVlans, availableVlansWithDepends);
        for (StitchingHopData dep : dependingOnThis) {
            availableVlansWithDepends.retainAll(dep.getAvailableVlans());
            assert Collections.disjoint(unavailableVlans, availableVlansWithDepends);
        }
        if (!availableVlansWithDepends.isEmpty()) {
            assert Collections.disjoint(unavailableVlans, availableVlansWithDepends);
            return availableVlansWithDepends;
        } else {
            log.warn("Combined with hops depending on this, there are no available VLANs: " +
                    "will ignore hops depending on this so we can try anyway.");
            assert Collections.disjoint(unavailableVlans, availableVlans);
            return availableVlans;
        }
    }

    public String getAvailableVlansString() {
        return getAvailableVlans().toString();
    }

//    public String getSuggestedVlanRange() {
//        return getSuggestedVlan()+"";
//    }

    public Integer getSuggestedVlan() {
        VlanRange availableVlans = getAvailableVlans();

        if (availableVlans.isEmpty())
            return null;

        if (suggestedVlan != null && availableVlans.contains(suggestedVlan))
            return suggestedVlan;

        Integer scsSuggested = null;
        if (scsSuggestedVLANRange != null)
            try {
                scsSuggested = Integer.parseInt(scsSuggestedVLANRange);
            } catch (NumberFormatException e) {
                log.warn("suggested vlan in stitching rspec extension is not a number: \"" + scsSuggestedVLANRange + "\"", e);
                scsSuggested = null;
            }
        Integer lastManifestSuggested = null;
        if (lastManifestSuggestedVLANRange != null)
            try {
                lastManifestSuggested = Integer.parseInt(lastManifestSuggestedVLANRange);
            } catch (NumberFormatException e) {
                log.warn("suggested vlan in stitching rspec extension is not a number: \"" + lastManifestSuggestedVLANRange + "\"", e);
                lastManifestSuggested = null;
            }

        if (scsSuggested != null && !availableVlans.contains(scsSuggested))
            scsSuggested = null;
        if (lastManifestSuggested != null && !availableVlans.contains(lastManifestSuggested))
            lastManifestSuggested = null;

        if (lastManifestSuggested != null)
            return lastManifestSuggested;
        if (scsSuggested != null)
            return scsSuggested;

        return availableVlans.iterator().next();
    }

    public Integer getLastManifestVlan() {
        Integer lastManifestSuggested = null;
        if (lastManifestSuggestedVLANRange != null)
            try {
                return Integer.parseInt(lastManifestSuggestedVLANRange);
            } catch (NumberFormatException e) {
                log.warn("suggested vlan in stitching rspec extension is not a number: \"" + lastManifestSuggestedVLANRange + "\"", e);
            }

        return null;
    }

    public String getRspecStitchingHopId() {
        return hopElId;
    }

    public Element getRspecStitchingHopElement(boolean useAny) {
        Element vlanRangeAvailabilityEl =
                XmlUtil.getDomElementFirstChildElementNS(scSpecificInfo_L2scEl, StitchingData.NAMESPACE_STITCHING, "vlanRangeAvailability");
        Element suggestedVLANRangeEl =
                XmlUtil.getDomElementFirstChildElementNS(scSpecificInfo_L2scEl, StitchingData.NAMESPACE_STITCHING, "suggestedVLANRange");
        vlanRangeAvailabilityEl.setTextContent(getAvailableVlans().toString());
        if (useAny)
            suggestedVLANRangeEl.setTextContent("any");
        else {
            suggestedVLANRangeEl.setTextContent(getSuggestedVlan() + "");
        }

        assert hopElement != null;
        return hopElement;
    }

    /**
     * Get the stitching element, but with an alternative available and suggested.
     * This does not have any side effects, as the xml elements are never parsed again, only overwritten.
     * (so this never alters vlanRangeAvailability or suggestedVLANRange)
     */
    public Element getRspecStitchingHopElement(String overwriteVlanRangeAvailability, String overwriteSuggestedVLANRange) {
        assert overwriteVlanRangeAvailability != null;
        assert overwriteSuggestedVLANRange != null;

        Element vlanRangeAvailabilityEl =
                XmlUtil.getDomElementFirstChildElementNS(scSpecificInfo_L2scEl, StitchingData.NAMESPACE_STITCHING, "vlanRangeAvailability");
        Element suggestedVLANRangeEl =
                XmlUtil.getDomElementFirstChildElementNS(scSpecificInfo_L2scEl, StitchingData.NAMESPACE_STITCHING, "suggestedVLANRange");
        vlanRangeAvailabilityEl.setTextContent(overwriteVlanRangeAvailability);
        suggestedVLANRangeEl.setTextContent(overwriteSuggestedVLANRange);

        assert hopElement != null;
        return hopElement;
    }

    public StitchingExtensionSource getStitchingExtensionSource() {
        return stitchingExtensionSource;
    }

    //link name and hop URN uniquely identify this object
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        StitchingHopData that = (StitchingHopData) o;

        if (!Objects.equals(hopUrn, that.hopUrn)) return false;
        if (!Objects.equals(linkName, that.linkName)) return false;

        return true;
    }

    @Override
    public int hashCode() {
        int result = linkName.hashCode();
        result = 31 * result + hopUrn.hashCode();
        return result;
    }

    @Override
    public String toString() {
        List<String> depUrns =
                dependencies.stream()
                        .map(stitchingHopData -> stitchingHopData.linkName + " " + stitchingHopData.getHopUrn())
                        .collect(Collectors.toList());

        String res = "\"StitchingHopData\" : {\n";
        res = toStringHelper(res, "linkName", linkName);
        res = toStringHelper(res, "hopUrn", hopUrn);
        res = toStringHelper(res, "aggregateUrn", aggregateUrn);
        res = toStringHelper(res, "aggregateUrl", aggregateUrl);
        res = toStringHelper(res, "importVlans", importVlans);
        res = toStringHelper(res, "getAvailableVlans()", getAvailableVlans());
        res = toStringHelper(res, "getSuggestedVlan()", getSuggestedVlan());
        res = toStringHelper(res, "dependencies (link+urn)", depUrns);
        res += "}\n";
        return res;
    }

    private static String toStringHelper(String res, String objectName, Object objectValue) {
        if (objectValue == null)
            return res;
        return res + TextUtil.indent(4, "\"" + objectName + "\" : \"" + objectValue + "\"") + "\n";
    }
}
