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

import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Server;
import be.iminds.ilabt.jfed.lowlevel.TestbedInfoSource;
import be.iminds.ilabt.jfed.util.GeniUrn;
import be.iminds.ilabt.jfed.util.TextUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Element;

import javax.annotation.Nullable;
import java.util.*;
import java.util.stream.Collectors;

/**
 * StitchingCallData contains all data about a specific "CreateSLiver" call.
 * This means it contains all data about all hops for the called AM
 * <p/>
 * It is uniquely identified by the AM urn
 * <p/>
 * It also stores:
 * - which VLAN's have failed and which vlan succeeded
 * - how many other failures there where
 * - logs for all calls
 * - current status (simple): sliverExists true or false
 * <p/>
 * It also stores dependencies to other StitchingCallData. To create:
 * - first create it,
 * - then add all StitchingHopData,
 * - then call linkDeps
 */
public class StitchingCallData {
    private final Logger log;

    private final GeniUrn authUrn;
    private final String authUrnString;
    private final Server server;

    private final List<StitchingHopData> hopData = new ArrayList<>();

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

    private final Set<Integer> unavailableVlans = new HashSet<>();

    private boolean sawAdvertisementRspec = false;

    public enum State {NONE, ALLOCATED, ALLOCATING, LISTING_RESOURCES, ALLOCATING_BUT_MUST_DELETE, MUST_DELETE, DELETING}

    private State state = State.NONE;

    public StitchingCallData(String authUrnString, String authUrlString, TestbedInfoSource testbedInfoSource, Logger log) {
        if (log == null)
            this.log = LoggerFactory.getLogger(StitchingCallData.class);
        else
            this.log = log;
        this.authUrnString = authUrnString;
        this.authUrn = GeniUrn.parse(authUrnString);
        assert authUrn != null;

        //Do not use authorityFinder! It HAS to be exact.
        Server aggAuth = testbedInfoSource.getByUrnExact(authUrnString);

        if (aggAuth.getDefaultComponentManagerUrn() != null &&
                !Objects.equals(authUrnString, aggAuth.getDefaultComponentManagerUrn()))
            this.log.info("Created StitchingCallData for " + authUrnString + "  with new auth urn=" + aggAuth.getDefaultComponentManagerUrn());
        else
            this.log.info("Created StitchingCallData for " + authUrnString);

        server = aggAuth;
    }

    public void addHopData(StitchingHopData hop) {
        assert !hopData.contains(hop);
        hopData.add(hop);
    }

    public List<StitchingHopData> getAllHopData() {
        return hopData;
    }

    public void linkDeps(Collection<StitchingCallData> allData) {
        for (StitchingCallData d : allData)
            if (d != this && !dependencies.contains(d))
                for (StitchingHopData othersHop : d.hopData)
                    for (StitchingHopData myHop : hopData) {
                        if (myHop.getDependencies().contains(othersHop)) {
                            if (!dependencies.contains(d)) {
                                dependencies.add(d);
                                d.dependingOnThis.add(this);
                            }
                        }
                    }
    }


    /**
     * Update RSpec info from RSpec received from this authority in reply to a CreateSliver call.
     * <p/>
     * This will change the suggested and available VLAN.
     * <p/>
     * It will only change hops related to this authority!
     */
    public void processManifestRspec(String manifestRspec) {
        Element stitchingElement = StitchingData.extractStitchingElementFromRspec(manifestRspec, log);
        assert stitchingElement != null : "No stitching data in manifest RSpec";

        for (StitchingHopData hop : hopData) {
            hop.overwriteWithManifestRspec(manifestRspec, (Element) stitchingElement.cloneNode(true/*deep*/));
        }
    }

    public void exogeniBugWorkAround_registerVlan(int vlan) {
        hopData.stream()
                .filter(hop -> Objects.equals(hop.getAggregateUrn(), authUrnString))
                .forEach(hop -> hop.exogeniBugWorkAround_registerManifestVlan(vlan));
    }

    /**
     * @return true if configuration still possible
     * false if not enough available VLAN's are left
     */
    public boolean setVlanUnavailable(int vlan) {
        //find remaining available VLAN and update hops suggestedVLAN with it
        //keep in account that each hop with same urn but different linkName needs a different VLAN!

        log.trace("setVlanUnavailable({})", vlan);

        unavailableVlans.add(vlan);
        boolean depFailure;

        do {
            depFailure = false;

            Map<String, Set<Integer>> vlansUsedByHopUrn = new HashMap<>();
            for (StitchingHopData hop : hopData) {
                log.trace("hop=" + hop.getHopUrn() + "  suggestedVlan=" + hop.getSuggestedVlan());
                Set<Integer> s = vlansUsedByHopUrn.get(hop.getHopUrn());
                if (s == null)
                    s = new HashSet<>();
                Integer suggestedVlan = hop.getSuggestedVlan();
                if (suggestedVlan != null)
                    s.add(suggestedVlan);
                vlansUsedByHopUrn.put(hop.getHopUrn(), s);
            }
            log.trace("vlansUsedByHopUrn=" + vlansUsedByHopUrn);

            List<StitchingHopData> changedHops = new ArrayList<>();
            for (StitchingHopData hop : hopData) {
                Set<Integer> sameHopVlans = vlansUsedByHopUrn.get(hop.getHopUrn());
                Integer origVlan = hop.getSuggestedVlan();

                Set<Integer> allForbiddenVLANs = new HashSet<>(sameHopVlans);
                if (origVlan != null)
                    allForbiddenVLANs.remove(origVlan); //remove own vlan, will be added again line below if unavailable
                allForbiddenVLANs.addAll(unavailableVlans);

                //find a new VLAN if old is not available anymore
                log.debug("hop " + hop.getHopUrn() + " current suggestion=" + hop.getSuggestedVlan() + " updateSuggestedVlan: forbidden=" + allForbiddenVLANs);
                if (origVlan == null || allForbiddenVLANs.contains(origVlan)) {
                    Integer foundVlan =
                            hop.updateSuggestedVlan(allForbiddenVLANs, null/*no new suggested VLAN, find automatically*/);
                    log.debug("    updated hop VLAN to foundVlan={}", foundVlan);
                    if (foundVlan == null)
                        return false;
                    else
                        sameHopVlans.add(foundVlan);
                    if (origVlan != null && !Objects.equals(foundVlan, origVlan))
                        sameHopVlans.remove(origVlan);

                    changedHops.add(hop);
                } else
                    log.debug("   leaving hop VLAN unchanged");
            }


            //hop was updated. now find dependant hops, and update these as well.
            Set<Integer> allForbiddenVLANs = new HashSet<>();
            allForbiddenVLANs.add(vlan);
            while (!changedHops.isEmpty()) {
                List<StitchingHopData> curChangedHops = changedHops;
                changedHops = new ArrayList<>();
                for (StitchingHopData hop : curChangedHops) {
                    List<StitchingHopData> dependentHops = hop.getDependingOnThis();
                    for (StitchingHopData dependantHop : dependentHops) {
                        Integer foundVlan = dependantHop.updateSuggestedVlan(allForbiddenVLANs, hop.getSuggestedVlan());
                        if (foundVlan == null) {
                            //the dependant hop does not support the VLAN set in its dependency
                            //solution: should blacklist this VLAN, and start again.
                            unavailableVlans.add(hop.getSuggestedVlan());

                            depFailure = true;

                            //check if all vlans on dep are exhausted. In that case, all fails.
                            if (dependantHop.getAvailableVlansString().isEmpty())
                                return false;
                        }
                        //was: if (foundVlan == null) return false; //quick fix: just fail

                        //if this has dependent hops, change those as well.
                        changedHops.add(dependantHop);
                    }
                }
            }

        } while (depFailure);

        //all was successfully updated
        return true;
    }

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

    public List<Integer> getSuggestedVlans() {
        return hopData.stream().map(StitchingHopData::getSuggestedVlan).distinct().collect(Collectors.toList());
    }


    public boolean isAllocated() {
        if (getServer().isEdgeVlan()) return true;
        return state == State.ALLOCATED;
    }

    public void setState(State newState) {
        this.state = newState;
        if (newState == State.ALLOCATED || newState == State.ALLOCATING)
            for (StitchingCallData dep : dependencies)
                assert dep.isAllocated() || dep.getServer().isEdgeVlan();
    }

    public State getState() {
        return state;
    }

    public boolean areAllDepsReady() {
        for (StitchingCallData dep : dependencies) {
            if (!dep.isAllocated() && !dep.getServer().isEdgeVlan())
                return false;
        }

        return true;
    }

    public boolean needsAdvertisementRspec() {
        if (dependencies.isEmpty()) //only needed if we have dependencies.
            return false;
        if (!getServer().hasFlag(Server.Flag.featureStitchingAdvertisementRspecUptodate))
            return false;
        if (sawAdvertisementRspec)
            return false;
        return true;
    }

    /**
     * parse advertisement stitching info.
     */
    public void seeAdvertismentRspec(@Nullable String rspec) {
        sawAdvertisementRspec = true;

        if (rspec != null) {
            List<AdvertisementStitchInfo> advertisementStitchInfos = AdvertisementStitchInfo.parse(rspec);
            for (StitchingHopData hop : hopData) {
                AdvertisementStitchInfo bestMatchingInfo = null;
                for (AdvertisementStitchInfo info : advertisementStitchInfos) {

                    if (bestMatchingInfo == null && Objects.equals(info.getLinkId(), hop.getHopUrn()))
                        bestMatchingInfo = info;
                    //we don't know getHopRemoteUrn , so this is a problem
//                if (info.getLinkId().equals(hop.getHopUrn()) && info.getRemoteLinkId().equals(hop.getHopRemoteUrn()))
//                    bestMatchingInfo = info;
                }
                if (bestMatchingInfo != null)
                    hop.processAdvertismentInfo(bestMatchingInfo.getVlanRangeAvailability());
            }
        }
    }

    /**
     * it is useful to get the advertisement for everything depending on this, if they support up to date advertisements
     */
    public boolean areAllDependingOnThisReady() {
        for (StitchingCallData dep : dependingOnThis) {
            if (dep.getServer().hasFlag(Server.Flag.featureStitchingAdvertisementRspecUptodate) && !dep.sawAdvertisementRspec)
                return false;
        }

        return true;
    }

    public List<StitchingCallData> getDependencies() {
        return Collections.unmodifiableList(dependencies);
    }

    public List<StitchingCallData> getDependingOnThis() {
        return Collections.unmodifiableList(dependingOnThis);
    }

    public GeniUrn getAuthUrn() {
        return authUrn;
    }

    public String getAuthUrnString() {
        return authUrnString;
    }

    public Server getServer() {
        return server;
    }

    @Override
    public String toString() {
        List<String> depUrns =
                dependencies.stream().map(StitchingCallData::getAuthUrnString).collect(Collectors.toList());
        String hopDataString = "[\n";
        for (StitchingHopData stitchingHopData : hopData)
            hopDataString += TextUtil.indent(4, stitchingHopData + ",\n");
        hopDataString += "]";

        String res = "\"StitchingCallData\" : {\n";
        res = toStringHelper(res, "authUrnString", authUrnString);
        res = toStringHelper(res, "hopDataString", hopDataString);
        res = toStringHelper(res, "dependencies (urns)", 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";
    }

    //same authUrnString is normally same StitchingCallData
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        StitchingCallData that = (StitchingCallData) o;

        assert authUrnString.equals(that.authUrnString) == (this == o);

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

        return true;
    }

    @Override
    public int hashCode() {
        return authUrnString.hashCode();
    }
}
