package be.iminds.ilabt.jfed.highlevel.model;

import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Server;
import be.iminds.ilabt.jfed.highlevel.util.SliceRegistryUtil;
import be.iminds.ilabt.jfed.lowlevel.AnyCredential;
import be.iminds.ilabt.jfed.lowlevel.GeniUserProvider;
import be.iminds.ilabt.jfed.lowlevel.SfaCredential;
import be.iminds.ilabt.jfed.lowlevel.TestbedInfoSource;
import be.iminds.ilabt.jfed.lowlevel.api_wrapper.StatusDetails;
import be.iminds.ilabt.jfed.lowlevel.api_wrapper.UserAndSliceApiWrapper;
import be.iminds.ilabt.jfed.lowlevel.authority.AuthorityFinder;
import be.iminds.ilabt.jfed.rspec.model.ModelRspecType;
import be.iminds.ilabt.jfed.rspec.rspec_source.AdvertisementRspecSource;
import be.iminds.ilabt.jfed.util.GeniUrn;
import be.iminds.ilabt.jfed.util.JavaFXLogger;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import javafx.collections.ObservableSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.*;
import java.util.stream.Collectors;

/**
 * SfaModel is passive, it just listens to replies from the SA and AM servers, and offers access to data from them.
 * It caches and informs listeners
 * <p/>
 * It also offers access to the connection pool, and the logged in user
 * It also has methods that will schedule and execute requests. Note that all updates caused by this execution is still
 * received by the passive monitoring of sfaModel.
 */
@Singleton
public class SfaModel {
    private static final Logger LOG = LoggerFactory.getLogger(SfaModel.class);

    private static final Comparator<Slice> SLICE_COMPARATOR =
            Comparator.comparing(Slice::getUrnString, String::compareToIgnoreCase);

    private final ObservableList<Slice> slices = FXCollections.observableArrayList();

    private final JavaFXLogger logger;
    UserAndSliceApiWrapper.SubAuthoritySupport subAuthoritySupport = null;
    private final ObservableMap<GeniUrn, Slice> slicesByUrn = FXCollections.observableHashMap();
    private List<AnyCredential> userCredentials;
    private final HashMultimap<String, AnyCredential> userCredentialsBySubAuthority = HashMultimap.create();
    private final Multimap<GeniUrn, String> userKeys = HashMultimap.create();
    private final ObservableSet<String> allSubAuthorityNames = FXCollections.observableSet(new HashSet<>());
    private final ObservableSet<String> userSubAuthorityNames = FXCollections.observableSet(new HashSet<>());
    /**
     * Users stored in the key 'null' are users of the global authority
     */
    private final Multimap<String, GeniUrn> usersBySubAuthorityName = HashMultimap.create();

    private final TestbedInfoSource testbedInfoSource;
    private final AuthorityFinder authorityFinder;
    private final ParameterHistoryModel parameterHistoryModel;
    private final GeniUserProvider geniUserProvider;
    private final SliceRegistryUtil sliceRegistryUtil;

    @Inject
    public SfaModel(JavaFXLogger logger, TestbedInfoSource testbedInfoSource, AuthorityFinder authorityFinder, ParameterHistoryModel parameterHistoryModel,
                    GeniUserProvider geniUserProvider, @Nullable SliceRegistryUtil sliceRegistryUtil) {
        this.logger = logger;
        this.testbedInfoSource = testbedInfoSource;
        this.authorityFinder = authorityFinder;
        this.parameterHistoryModel = parameterHistoryModel;
        this.geniUserProvider = geniUserProvider;
        this.sliceRegistryUtil = sliceRegistryUtil;

        SfaModelAbstractListener planetlabSfaRegistryInterfaceListener =
                new PlanetlabSfaRegistryInterfaceListener(this, parameterHistoryModel);
        logger.addResultListener(planetlabSfaRegistryInterfaceListener);

        SfaModelAbstractListener sliceAuthorityListener = new SfaModelSliceAuthorityListener(this, testbedInfoSource);
        logger.addResultListener(sliceAuthorityListener);
        SfaModelAggregateManager3Listener am3Listener = new SfaModelAggregateManager3Listener(this, testbedInfoSource);
        logger.addResultListener(am3Listener);
        SfaModelAggregateManager2Listener am2Listener = new SfaModelAggregateManager2Listener(this, testbedInfoSource);
        logger.addResultListener(am2Listener);
        SfaUniformFederationSliceAuthorityApiListener ufSaListener =
                new SfaUniformFederationSliceAuthorityApiListener(this, testbedInfoSource);
        logger.addResultListener(ufSaListener);
        SfaUniformFederationMemberAuthorityApiListener ufMaListener =
                new SfaUniformFederationMemberAuthorityApiListener(this, geniUserProvider);
        logger.addResultListener(ufMaListener);

        this.userCredentials = null;
    }

    private GeniUrn getLoggedInUserUrn() {
        if (!geniUserProvider.isUserLoggedIn())
            return null;
        return geniUserProvider.getLoggedInGeniUser().getUserUrn();
    }

    private static CredForSubAuthInfo areUserCredentialsForSubAuth(List<AnyCredential> userCredentials) {
        if (userCredentials == null) return null;

        CredForSubAuthInfo res = new CredForSubAuthInfo();
        res.isAnyCredForASubAuthority = false;
        res.isAnyCredNotForASubAuthority = false;
        userCredentials.stream().filter(c -> c.isTargetSubAuthority() != null).forEach(c -> {
            if (!res.isAnyCredForASubAuthority && c.isTargetSubAuthority())
                res.isAnyCredForASubAuthority = true;
            if (!res.isAnyCredNotForASubAuthority && !c.isTargetSubAuthority())
                res.isAnyCredNotForASubAuthority = true;
            if (c.isTargetSubAuthority()) {
                SfaCredential userSfaCredential = (SfaCredential) c;
                String targetUrnStr = userSfaCredential.getTargetUrn();
                assert targetUrnStr != null;
                GeniUrn targetUrn = GeniUrn.parse(targetUrnStr);
                assert targetUrn != null;
                String subAuthName = targetUrn.getEncodedSubAuthName();
                assert subAuthName != null;

                if (res.targetSubAuth != null && !Objects.equals(res.targetSubAuth, subAuthName))
                    LOG.error("CRITICAL WARNING: in credential list returned, I found a credential for subauthority=" + res.targetSubAuth + " AND for subauthority=" + subAuthName);
                res.targetSubAuth = subAuthName;
            }
        });
        return res;
    }

    private static Collection<String> getTargetUrns(@Nonnull Collection<AnyCredential> creds) {
        return creds.stream().filter(c -> c instanceof SfaCredential)
                .map(c -> ((SfaCredential) c).getTargetUrn())
                .collect(Collectors.toSet());
    }

    /*
    * Are the  newCreds more recent than the oldCreds?
    * */
    private static Boolean moreRecent(@Nullable Collection<AnyCredential> oldCreds,
                                      @Nullable Collection<AnyCredential> newCreds) {
        Date newestOldDate = null;
        Date newestNewDate = null;

        if (oldCreds == null || oldCreds.isEmpty()) return true;
        if (newCreds == null || newCreds.isEmpty()) return false;

        for (AnyCredential oldCred : oldCreds) {
            Date d = oldCred.getExpiresDate();
            if (d != null && (newestOldDate == null || newestOldDate.after(d)))
                newestOldDate = d;
        }
        for (AnyCredential newCred : newCreds) {
            Date d = newCred.getExpiresDate();
            if (d != null && (newestNewDate == null || newestNewDate.after(d)))
                newestNewDate = d;
        }

        if (newestOldDate == null) return null;
        if (newestNewDate == null) return null;

        return newestNewDate.after(newestOldDate);
    }

    /**
     * forget all state. (state will come back by listening to Results)
     */
    public synchronized void clear() {
        slices.clear();
        slicesByUrn.clear();
        userCredentials.clear();
        userKeys.clear();
        allSubAuthorityNames.clear();
        userSubAuthorityNames.clear();
        usersBySubAuthorityName.clear();
    }

    public JavaFXLogger getLogger() {
        return logger;
    }

    void logNotExistSlice(@Nonnull GeniUrn sliceUrn) {
        Slice existingSlice = slicesByUrn.get(sliceUrn);
        if (existingSlice != null) {
            //TODO: remove all slivers
            slices.remove(existingSlice);
            slicesByUrn.remove(existingSlice.getUrn());
        }
    }

    public Slice logExistSlice(@Nonnull String sliceUrn) throws GeniUrn.GeniUrnParseException {
        return logExistSlice(new GeniUrn(sliceUrn));
    }

    @Nonnull
    public synchronized Slice logExistSlice(@Nonnull GeniUrn sliceUrn) {

        Slice existingSlice = slicesByUrn.get(sliceUrn);
        if (existingSlice == null) {
            Slice newSlice = new Slice(sliceUrn, testbedInfoSource, authorityFinder);
            parameterHistoryModel.addSliceUrn(sliceUrn.toString());

            newSlice.credentialsProperty().addListener((observableValue, oldGeniCredentials, newGeniCredentials) -> newGeniCredentials.forEach(parameterHistoryModel::addSliceCredential));

            //enter the new slice in the correct sorted position!
            int location = Collections.binarySearch(slices, newSlice, SLICE_COMPARATOR);

            //binary search should give a negative index, because the slice is not yet registered
            assert location < 0 :
                    "Slice was not found in slicesByUrn (keys=" + slicesByUrn.keySet() + ") but was found by binary search. urn=" + sliceUrn;
            location = -location - 1;

            slices.add(location, newSlice);
            slicesByUrn.put(sliceUrn, newSlice);
            return newSlice;
        } else
            return existingSlice;
    }

    @Nonnull
    public Slice logExistSliceUrn(@Nonnull Server auth, @Nonnull String sliceUrn) throws GeniUrn.GeniUrnParseException {
        assert sliceUrn.startsWith("urn:publicid:IDN") : "sliceUrn is not urn \"" + sliceUrn + "\"";
        assert sliceUrn.startsWith("urn:publicid:IDN+" + auth.getUrnTld()) :
                "sliceUrn is not for the correct authority: urn \"" + sliceUrn + "\" auth: \"" + auth.getDefaultComponentManagerUrn() + "\"";

        return logExistSlice(sliceUrn);
    }

    @Nonnull
    public Slice logExistSliceUrn(@Nonnull Server auth, @Nonnull GeniUrn sliceUrn) {
        assert sliceUrn.getTopLevelAuthority().startsWith(auth.getUrnTld()) :
                "sliceUrn is not for the correct authority: urn \"" + sliceUrn + "\" auth: \"" + auth.getDefaultComponentManagerUrn() + "\"";
        return logExistSlice(sliceUrn);
    }


    /**
     * @param sliceUrn  urn of the slice the sliver belongs to
     * @param sliverUrn urn of the silver
     */
    public void logNotExistSliver(@Nonnull String sliceUrn, @Nonnull String sliverUrn, Sliver.Source source) throws GeniUrn.GeniUrnParseException {
        logNotExistSliver(new GeniUrn(sliceUrn), new GeniUrn(sliverUrn), source);
    }

    /**
     * @param sliceUrn  urn of the slice the sliver belongs to
     * @param sliverUrn urn of the silver
     */
    public void logNotExistSliver(@Nonnull GeniUrn sliceUrn, @Nonnull GeniUrn sliverUrn, Sliver.Source source) {
        ////// HACK for PLE ////////
        /*
        hack for ple: handle the fact that ple requires a component_manager_id of
        urn:publicid:IDN+ple:ibbtple+authority+cm in the request RSpec but it returns
        urn:publicid:IDN+ple+authority+cm
        */
        if (Objects.equals(sliverUrn.getTopLevelAuthority(), "ple")) {
            LOG.warn("HACK: correcting sliver-urn from +ple+ to +ple:ibbtple+");
            sliverUrn = sliverUrn.replaceTopLevelAuthority("ple:ibbtple");
        }
        //////// HACK for PLE ////////

        Slice slice = logExistSlice(sliceUrn);
        Sliver sliver = slice.findSliver(sliverUrn);

        if (sliver == null) {
            LOG.info("Sliver {} is not present in model, so we cannot mark it as non-existing", sliverUrn);
            return;
        }

        if (sliver.getSource() == Sliver.Source.SLICE_AUTHORITY) {
            if (Sliver.Source.SLICE_AUTHORITY.compareTo(source) < 0) {
                //the slice authority contains outdated info, let's remove this sliver also there!
                if (sliceRegistryUtil != null) {
                    LOG.debug("Sliver {} is only known by the slice authority, but not mentioned by {}." +
                            " We will remove it from the slice authority now.", sliver.getUrn(), source.name());
                    sliceRegistryUtil.unregisterSliversAtSATaskExecution(slice, sliver.getServer(), Collections.singletonList(sliver.getUrn()), null);
                }
            } else {
                //we can remove the sliver from the model, as SLICE_AUTHORITY was the only source that told us that the sliver existed, and it doesn't say it anymore!
                LOG.debug("Sliver {} was only registered in SLICE_AUTHORITY, and was deleted there. We can safely delete it from our model", sliver.getUrn());
                slice.removeSliver(sliver);
            }
        } else {
            LOG.debug("Setting sliver {} to 'not existing'", sliver);
            //do not explicitely remove it from the model, but mark it as "non existing"
            sliver.setStatus(new StatusDetails(StatusDetails.SliverStatus.UNALLOCATED, sliver.getUrn()));
            sliver.setStatusString("non existing");
            sliver.setManifestRspec(null);
        }
    }

    @Nullable
    public Sliver logExistSliver(@Nonnull String sliceUrnStr, @Nonnull String sliverUrnStr,
                                 @Nonnull Server auth, @Nonnull Sliver.Source source) throws GeniUrn.GeniUrnParseException {
        return logExistSliver(new GeniUrn(sliceUrnStr), new GeniUrn(sliverUrnStr), auth, source);

    }

    @Nullable
    public Sliver logExistSliver(@Nonnull GeniUrn sliceUrn, @Nonnull GeniUrn sliverUrn,
                                 @Nonnull Server auth, @Nonnull Sliver.Source source) {
        if (Objects.equals(sliverUrn.getEncodedTopLevelAuthority_withoutSubAuth(), auth.getDefaultComponentManagerAsGeniUrn().getEncodedTopLevelAuthority_withoutSubAuth())) {
            return logExistSliver(sliceUrn, sliverUrn, source);
        } else {
            LOG.warn("SfaModel.logExistSliver() -> Sliver does not match authority, will ignore it:" +
                    " sliverUrn=" + sliverUrn + " (" + sliverUrn.getEncodedTopLevelAuthority_withoutSubAuth() + ")" +
                    " authUrn=" + auth.getDefaultComponentManagerUrn() + " (" + auth.getDefaultComponentManagerAsGeniUrn().getEncodedTopLevelAuthority_withoutSubAuth() + ")");
            logExistAtLeastOneSliver(sliceUrn, auth, source);
            return null;
        }
    }

    /**
     * @param sliceUrn  urn of the slice the sliver belongs to, may be null if unknown
     * @param sliverUrn urn of the silver
     * @return the sliver or null if not stored
     */
    @Nonnull
    public Sliver logExistSliver(@Nonnull String sliceUrn, @Nonnull String sliverUrn,
                                 @Nonnull Sliver.Source source) throws GeniUrn.GeniUrnParseException {
        assert sliverUrn.contains("+sliver+") :
                "sliverUrn=\"" + sliverUrn + "\" is not a sliver urn (sliceUrn=" + sliceUrn + ")";
        assert sliceUrn.contains("+slice+") :
                "sliceUrn=\"" + sliceUrn + "\" is not a slice urn (sliverUrn=" + sliverUrn + ")";

        return logExistSliver(new GeniUrn(sliceUrn), new GeniUrn(sliverUrn), source);
    }

    /**
     * @param sliceUrn  urn of the slice the sliver belongs to, may be null if unknown
     * @param sliverUrn urn of the silver
     * @return the sliver or null if not stored
     */
    @Nonnull
    public Sliver logExistSliver(@Nonnull GeniUrn sliceUrn, @Nonnull GeniUrn sliverUrn, Sliver.Source source) {

        ////// HACK for PLE ////////
        /*
        hack for ple: handle the fact that ple requires a component_manager_id of
        urn:publicid:IDN+ple:ibbtple+authority+cm in the request RSpec but it returns
        urn:publicid:IDN+ple+authority+cm
        */
        if (Objects.equals(sliverUrn.getTopLevelAuthority(), "ple")) {
            LOG.warn("HACK: correcting sliver-urn from +ple+ to +ple:ibbtple+");
            sliverUrn = sliverUrn.replaceTopLevelAuthority("ple:ibbtple");
        }
        //////// HACK for PLE ////////

        Slice slice = logExistSlice(sliceUrn);

        //noinspection SynchronizationOnLocalVariableOrMethodParameter
        synchronized (slice) {
            Sliver sliver = slice.findSliver(sliverUrn);
            if (sliver == null) {
                LOG.debug("logExistSliver did not find sliver by urn " + sliverUrn + " so it will create a sliver");

                Server auth =
                        testbedInfoSource.getFromAnyUrn(sliverUrn, TestbedInfoSource.SubAuthMatchAllowed.ALLOW_TOPLEVEL, TestbedInfoSource.SubAuthMatchPreference.PREFER_EXACT_SUBAUTHORITY);

                if (auth == null)
                    throw new NullPointerException("auth==null because sliverUrn could not be used to find authority: sliverUrn=\"" + sliverUrn + "\"");

                LOG.debug("Sliver '{}' is from authority {} according to {}",
                        sliverUrn, auth.getDefaultComponentManagerUrn(), source.name());

                sliver = new Sliver(sliverUrn, source, slice, null, null, auth);
                parameterHistoryModel.addSliverUrn(sliverUrn.toString());
                slice.addSliver(sliver);
            } else {
                LOG.debug("logExistSliver found sliver " + sliver + " by urn " + sliverUrn);

                if (sliver.getSource().compareTo(source) < 0) {
                    //we found a more accurate source!
                    sliver.setSource(source);
                    LOG.debug("logExistSliver found a more trusted source for sliver {}: {}", sliver.getUrn(), source);
                }
            }
            return sliver;
        }
    }

    public void logExistAtLeastOneSliver(@Nonnull String sliceUrn, @Nonnull Server auth, Sliver.Source source) throws GeniUrn.GeniUrnParseException {
        logExistAtLeastOneSliver(new GeniUrn(sliceUrn), auth, source);
    }

    public void logExistAtLeastOneSliver(@Nonnull GeniUrn sliceUrn, @Nonnull Server auth, Sliver.Source source) {
        Slice slice = logExistSlice(sliceUrn);
        List<Sliver> slivers = slice.findSlivers(auth);


        if (slivers == null || slivers.isEmpty()) {
            slice.setAuthorityInternalState(auth, InternalState.UNKNOWN);

            LOG.debug("logExistAtLeastOneSliver -> set internal state for authority {} on slice {} to UNKNOWN",
                    auth.getName(), sliceUrn);
        } //else: already known that sliver exists, so we do nothing
    }


    public void logNotExistSliverInSliceOnAuth(@Nonnull String sliceUrn, @Nonnull Server auth) {
        try {
            logNotExistSliverInSliceOnAuth(new GeniUrn(sliceUrn), auth);
        } catch (GeniUrn.GeniUrnParseException e) {
            LOG.warn("Could not register non-existence of sliver on {} in slice {}: invalid sliceUrn", auth.getName(), sliceUrn, e);
        }
    }

    /**
     * @param sliceUrn urn of the slice the sliver belongs to, may be null if unknown
     * @param auth     the authority of the sliver
     *                 <p>
     *                 See logExistSliverGeniSingle for details about "geni_single"
     */
    public void logNotExistSliverInSliceOnAuth(@Nonnull GeniUrn sliceUrn, @Nonnull Server auth) {
        Slice slice = slicesByUrn.get(sliceUrn);
        if (slice == null)
            return;

        List<Sliver> slivers = slice.findSlivers(auth);
        for (Sliver sliver : slivers) {
            sliver.setStatus(new StatusDetails(StatusDetails.SliverStatus.UNALLOCATED, sliver.getUrn()));
            sliver.setStatusString("non existing");
            sliver.setManifestRspec(null);
        }

        //Do not remove! Keep list of slivers, but mark as unallocated
        //slice.removeSliver(sliver);
    }

    public Slice getSlice(@Nonnull String sliceUrn) throws GeniUrn.GeniUrnParseException {
        return slicesByUrn.get(new GeniUrn(sliceUrn));
    }

    public Slice getSlice(@Nonnull GeniUrn sliceUrn) {
        return slicesByUrn.get(sliceUrn);
    }

    public ObservableList<Slice> getSlices() {
        return /*FXCollections.unmodifiableObservableList(*/slices/*)*/;
    }


    //////////////////////////////////////////////////////////////////
    ////////////////////////// UserCredentials ///////////////////////
    //////////////////////////////////////////////////////////////////


    /**
     * add a key for the logged in user
     */
    public void addUserKey(@Nonnull String key) {
        if (getLoggedInUserUrn() == null)
            LOG.warn("No logged in user, so SfaModel cannot register key " + key);
        else
            addUserKey(getLoggedInUserUrn(), key);
    }

    /**
     * add a key for any user. Will not add duplicate keys. May be used to add key for logged in suer as well.
     */
    public void addUserKey(@Nonnull GeniUrn user, String key) {
        if (key == null) {
            LOG.warn("addUserKey(" + user + ", null) called -> no key to register. Will ignore.");
            return;
        }
        addUserKeys(user, Collections.singletonList(key));
    }

    /**
     * Add zero, one or multiple keys for any user. Will not add duplicate keys.
     * May be used to add key for logged in user as well.
     */
    public void addUserKeys(@Nonnull GeniUrn user, @Nonnull Collection<String> newKeys) {
        //sanitize keys while adding them to userKeys
        newKeys.stream()
                .map(key -> key.replaceAll("\\r\\n|\\r|\\n", ""))
                .forEach(key -> userKeys.put(user, key));
    }


    /**
     * Get the logged in user's (ssh) keys
     *
     * @return a list of keys in String format. might return an empty list, never returns null
     */
    public Collection<String> getUserKeys() {
        if (getLoggedInUserUrn() == null) {
            LOG.warn("No logged in user, so SfaModel cannot answer getUserKeys()");
            return Collections.emptyList();
        } else
            return getUserKeys(getLoggedInUserUrn());
    }

    /**
     * Get a specified user's (ssh) keys
     *
     * @return a list of keys in String format. might return an empty list (no keys known), never returns null.
     */
    public Collection<String> getUserKeys(GeniUrn userUrn) {
        if (userKeys.containsKey(userUrn))
            return userKeys.get(userUrn);
        else
            return Collections.emptyList();
    }

    /**
     * Get non-subauth and sub auth user credentials, all in one list.
     * The wrappers use this list and filter it when needed.
     */
    public synchronized List<AnyCredential> getAllUserCredentials() {
        List<AnyCredential> res = new ArrayList<>();
        if (userCredentials != null)
            res.addAll(userCredentials);

        userCredentialsBySubAuthority.values().forEach(res::add);
        return res;
    }

    public synchronized List<AnyCredential> getUserCredentials() {
        if (LOG.isDebugEnabled()) {
            Collection<String> targetUrn = getTargetUrns(userCredentials);
            LOG.debug("SfaModel: retrieving user credential with target: " + targetUrn);
        }
        return userCredentials;
    }

    public synchronized void setUserCredentials(@Nonnull List<AnyCredential> userCredentials) {
        LOG.trace("SfaModel: setUserCredentials userCredentials.size()=" + userCredentials.size());
        CredForSubAuthInfo subAuthInfo = areUserCredentialsForSubAuth(userCredentials);

        if (subAuthInfo != null && subAuthInfo.isAnyCredForASubAuthority) {
            setUserCredentials(userCredentials, subAuthInfo.targetSubAuth);
        } else
            setUserCredentials(userCredentials, null);
    }

    public synchronized Collection<AnyCredential> getUserCredentials(@Nullable String subAuthority) {
        if (subAuthority == null) return getUserCredentials();
        if (LOG.isDebugEnabled()) {
            Collection<String> targetUrn = getTargetUrns(userCredentialsBySubAuthority.get(subAuthority));
            LOG.debug("SfaModel: retrieving user credential for SUB AUTHORITY \"" + subAuthority + "\" with target: " + targetUrn);
        }
        return userCredentialsBySubAuthority.get(subAuthority);
    }

    //////////////////////////////////////////////////////////////////
    ////////////////////////// Sub Authorities ///////////////////////
    //////////////////////////////////////////////////////////////////

    public synchronized void setUserCredentials(@Nonnull List<AnyCredential> userCredentials, @Nullable String subAuthority) {
        LOG.trace("SfaModel: setUserCredentials (subAuthority=" + subAuthority + ") userCredentials.size()=" + userCredentials.size());

        if (subAuthority == null) {
            if (moreRecent(this.userCredentials, userCredentials) != Boolean.TRUE) {
                LOG.info("Ignoring received credentials, as current ones are more recent");
                return;
            }

            CredForSubAuthInfo subAuthInfo = areUserCredentialsForSubAuth(userCredentials);
            //noinspection ConstantConditions
            assert !subAuthInfo.isAnyCredForASubAuthority :
                    "subAuthInfo.isAnyCredForASubAuthority=" + subAuthInfo.isAnyCredForASubAuthority +
                            " isAnyCredNotForASubAuthority=" + subAuthInfo.isAnyCredNotForASubAuthority + " for userCredentials=" + userCredentials;

            if (LOG.isDebugEnabled()) {
                Collection<String> targetUrns = getTargetUrns(userCredentials);
                LOG.debug("SfaModel: saving user credential with targets: " + targetUrns);
                if (targetUrns != null && !targetUrns.isEmpty()) {
                    for (String targetUrn : targetUrns) {
                        GeniUrn tu = GeniUrn.parse(targetUrn);
                        if (tu != null)
                            assert tu.getEncodedSubAuthName() == null;
                    }
                }
            }

            LOG.trace("SfaModel: storing userCredentials (no subauth): userCredentials.size()=" + userCredentials.size());

            userCredentials.forEach(parameterHistoryModel::addUserCredential);

            this.userCredentials = userCredentials;
        } else {
            CredForSubAuthInfo subAuthInfo = areUserCredentialsForSubAuth(userCredentials);
            //noinspection ConstantConditions
            assert subAuthInfo.isAnyCredForASubAuthority :
                    "subAuthInfo.isAnyCredForASubAuthority=" + subAuthInfo.isAnyCredForASubAuthority +
                            " isAnyCredNotForASubAuthority=" + subAuthInfo.isAnyCredNotForASubAuthority + " for userCredentials=" + userCredentials;
            //noinspection ConstantConditions
            assert !subAuthInfo.isAnyCredNotForASubAuthority :
                    "subAuthInfo.isAnyCredForASubAuthority=" + subAuthInfo.isAnyCredForASubAuthority +
                            " isAnyCredNotForASubAuthority=" + subAuthInfo.isAnyCredNotForASubAuthority + " for userCredentials=" + userCredentials;

            Collection<AnyCredential> oldUserCredentials = userCredentialsBySubAuthority.get(subAuthority);

            if (moreRecent(oldUserCredentials, userCredentials) != Boolean.TRUE) {
                LOG.info("Ignoring received credentials for subAuth {}, as current ones are more recent", subAuthority);
                return;
            }

            if (LOG.isDebugEnabled()) {
                Collection<String> targetUrns = getTargetUrns(userCredentialsBySubAuthority.get(subAuthority));

                LOG.debug("SfaModel: saving SUB AUTHORITY user credential for \"" + subAuthority + "\" with target: " + targetUrns);
                if (targetUrns != null && !targetUrns.isEmpty()) {
                    for (String targetUrn : targetUrns) {
                        GeniUrn tu = GeniUrn.parse(targetUrn);
                        if (tu != null)
                            assert tu.getEncodedSubAuthName() != null;
                    }
                }
            }

            LOG.trace("SfaModel: storing userCredentials (subauth=" + subAuthority + "): userCredentials.size()=" + userCredentials.size());

            //also save in parameterHistoryModel
            userCredentials.forEach(parameterHistoryModel::addUserCredential);

            //update
            userCredentialsBySubAuthority.replaceValues(subAuthority, userCredentials);
        }
    }

    public UserAndSliceApiWrapper.SubAuthoritySupport getSubAuthoritySupport() {
        return subAuthoritySupport;
    }

    public void setSubAuthoritySupport(UserAndSliceApiWrapper.SubAuthoritySupport subAuthoritySupport) {
        this.subAuthoritySupport = subAuthoritySupport;
    }

    public void addLoggedInUserSubAuthorityName(@Nonnull String subAuthorityName) {
        allSubAuthorityNames.add(subAuthorityName);
        userSubAuthorityNames.add(subAuthorityName);
    }

    public void addAnySubAuthorityName(@Nonnull String subAuthorityName) {
        allSubAuthorityNames.add(subAuthorityName);
    }

    @Nonnull
    public ObservableSet<String> getAllSubAuthorityNames() {
        return /*FXCollections.unmodifiableObservableSet(*/allSubAuthorityNames/*)*/;
    }

    @Nonnull
    public ObservableSet<String> getLoggedInUserSubAuthorityNames() {
        return /*FXCollections.unmodifiableObservableSet(*/userSubAuthorityNames/*)*/;
    }

    /**
     * @param subAuthorityName the name of the sub authority, or null (meaning the "global" authority)
     * @return The URN of all users in the specified sub authority.
     * This includes the URN of the logged in user, if that user is known to be in the sub authority.
     * May return empty list, never returns null.
     */
    @Nonnull
    public Collection<GeniUrn> getUsersBySubAuthority(@Nullable String subAuthorityName) {
        return Collections.unmodifiableCollection(usersBySubAuthorityName.get(subAuthorityName));
    }

    public ParameterHistoryModel getParameterHistoryModel() {
        return parameterHistoryModel;
    }

    /**
     * @param subAuthorityName the name of the sub authority, or null (meaning the "global" authority)
     * @param userUrns         the urns to store. These replace the previously stored (if any). May not be null, but may be empty.
     */
    public void setUsersOfSubAuthority(@Nullable String subAuthorityName, @Nonnull Collection<GeniUrn> userUrns) {
        usersBySubAuthorityName.replaceValues(subAuthorityName, userUrns);
    }


    private final Map<Integer, ObjectProperty<AdvertisementRspecSource>> availableAdvertisementPropertyByServerId =
            new HashMap<>();
    private final Map<Integer, ObjectProperty<AdvertisementRspecSource>> allAdvertisementPropertyByServerId =
            new HashMap<>();

    public void setAdvertisementRspec(Server server, boolean available, String rspec) {
        assert server != null;
        assert rspec != null;
        ObjectProperty<AdvertisementRspecSource> prop = available ?
                availableAdvertisementPropertyByServerId.computeIfAbsent(server.getId(), id -> new SimpleObjectProperty<>())
                :
                        allAdvertisementPropertyByServerId.computeIfAbsent(server.getId(), id -> new SimpleObjectProperty<>());
        prop.setValue(new AdvertisementRspecSource(rspec, ModelRspecType.FX));
    }

    public AdvertisementRspecSource getAvailableAdvertisementRspec(Server server) {
        assert server != null;
        ObjectProperty<AdvertisementRspecSource> prop =
                availableAdvertisementPropertyByServerId.computeIfAbsent(server.getId(), id -> new SimpleObjectProperty<>());
        return prop.get();
    }

    public ObjectProperty<AdvertisementRspecSource> availableAdvertisementRspecProperty(Server server) {
        assert server != null;
        ObjectProperty<AdvertisementRspecSource> prop =
                availableAdvertisementPropertyByServerId.computeIfAbsent(server.getId(), id -> new SimpleObjectProperty<>());
        return prop;
    }

    public AdvertisementRspecSource getAllAdvertisementRspec(Server server) {
        assert server != null;
        ObjectProperty<AdvertisementRspecSource> prop =
                allAdvertisementPropertyByServerId.computeIfAbsent(server.getId(), id -> new SimpleObjectProperty<>());
        return prop.get();
    }

    public ObjectProperty<AdvertisementRspecSource> allAdvertisementRspecProperty(Server server) {
        assert server != null;
        ObjectProperty<AdvertisementRspecSource> prop =
                allAdvertisementPropertyByServerId.computeIfAbsent(server.getId(), id -> new SimpleObjectProperty<>());
        return prop;
    }


    private static class CredForSubAuthInfo {
        public boolean isAnyCredForASubAuthority = false;
        public boolean isAnyCredNotForASubAuthority = false;
        public String targetSubAuth = null;
    }

    /**
     * This method will clear the slice with the given urn. Note that a new Slice-object is created and registered!
     */
    public Slice resetSlice(@Nonnull GeniUrn sliceUrn) {
        Slice slice = logExistSlice(sliceUrn);
        slice.removeInactiveSlivers();

        return slice;
    }
}
