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

import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Server;
import be.iminds.ilabt.jfed.highlevel.controller.JFedHighLevelException;
import be.iminds.ilabt.jfed.lowlevel.AnyCredential;
import be.iminds.ilabt.jfed.lowlevel.Gid;
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.authority.AuthorityFinder;
import be.iminds.ilabt.jfed.rspec.model.ModelRspecType;
import be.iminds.ilabt.jfed.rspec.model.RspecNode;
import be.iminds.ilabt.jfed.rspec.rspec_source.ManifestRspecMerger;
import be.iminds.ilabt.jfed.rspec.rspec_source.ManifestRspecSource;
import be.iminds.ilabt.jfed.rspec.rspec_source.RequestRspecSource;
import be.iminds.ilabt.jfed.util.GeniUrn;
import javafx.beans.property.*;
import javafx.beans.value.ChangeListener;
import javafx.collections.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.time.Instant;
import java.util.*;
import java.util.stream.Collectors;

/**
 * Slice
 * <p/>
 * Example slice Urn:  "urn:publicid:IDN+"+authority.getUrnTld()+"+slice+"+sliceName;  //+ potentially subAuthority
 */
public class Slice {
    private static final Logger LOG = LoggerFactory.getLogger(Slice.class);
    private static final String SLICE_RESOURCE_TYPE = "slice";
    final ChangeListener<StatusDetails> statusChangeListener =
            (observableValue, oldStatus, newStatus) -> updateStatus();

    final ChangeListener<ManifestRspecSource> manifestRspecChangeListener =
            (observableValue, oldRSpecInfo, newRSpecInfo) -> {
                LOG.debug("Change in sliver manifest rspec: joining manifests");
                joinManifestRspecs();
            };

    final ListChangeListener<Sliver> sliverListChangeListener = new ListChangeListener<Sliver>() {
        @Override
        public void onChanged(Change<? extends Sliver> change) {
            while (change.next()) {
                for (Sliver sliver : change.getRemoved()) {
                    sliver.statusProperty().removeListener(statusChangeListener);
                    sliver.manifestRspecProperty().removeListener(manifestRspecChangeListener);

                    //check if we have at least one sliver left of the current authority
                    boolean authHasOtherSliversInSlice = false;
                    for (Sliver otherSliver : slivers) {
                        if (otherSliver.getServer() == sliver.getServer())
                            authHasOtherSliversInSlice = true;
                    }

                    if (!authHasOtherSliversInSlice)
                        manifestComponentManagers.remove(sliver.getServer());
                }
                for (Sliver sliver : change.getAddedSubList()) {
                    sliver.statusProperty().addListener(statusChangeListener);
                    sliver.manifestRspecProperty().addListener(manifestRspecChangeListener);

                    manifestComponentManagers.add(sliver.getServer());
                }
            }
            updateStatus();
            LOG.debug("Sliver added or removed: joining manifests");
            joinManifestRspecs();
        }
    };
    private final GeniUrn urn;
    private final StringProperty uuid = new SimpleStringProperty();
    private final StringProperty creator_uuid = new SimpleStringProperty();
    private final StringProperty creator_urn = new SimpleStringProperty();
    private final ObjectProperty<StatusDetails> status =
            new SimpleObjectProperty<>(new StatusDetails(StatusDetails.SliverStatus.UNINITIALISED));
    private final Set<GeniUrn> users = new HashSet<>();

    /**
     * Source of this information:
     * - field _EMULAB_SLIVERS_ACTIVE in SLICE info
     * - SLIVER_INFO exists for the slice
     * - Terminate called (= Delete on all resource managers)
     * if null, server did not report whether there are active slivers
     * if true, it's known there are active slivers
     * if false, there are no active slivers
     */
    private final ObjectProperty<Boolean> hasActiveSlivers = new SimpleObjectProperty<>();

    /**
     * Component managers in manifests. This data is retrieved by the Resolve slice call on the Slice Authority
     */
    private final ObservableSet<Server> manifestComponentManagers = FXCollections.observableSet(new HashSet<>());

    /**
     * Authorities to connect to for Status and Manifest calls
     *
     * (in recover cases these can't always be linked to manifestComponentManagers)
     */
    private final ObservableSet<Server> connectAuthorities = FXCollections.observableSet(new HashSet<>());

    /**
     * The Rspec which was used to create this slice.
     * This can however be empty if the slice is being reconstructed from the various manifests!
     */
    private final ObjectProperty<RequestRspecSource> requestRspec = new SimpleObjectProperty<>();

    /**
     * manifestRspec is actually a join af manifest Rspec from multiple authorities/slivers!
     */
    private final ObjectProperty<ManifestRspecSource> manifestRspec = new SimpleObjectProperty<>();
    private final ObjectProperty<Gid> gid = new SimpleObjectProperty<>();
    private final ReadOnlyObjectWrapper<Instant> reservationStartTime = new ReadOnlyObjectWrapper<>();
    private final ReadOnlyObjectWrapper<Instant> expirationDate = new ReadOnlyObjectWrapper<>();
    private final ReadOnlyObjectWrapper<Instant> creationDate = new ReadOnlyObjectWrapper<>();
    private final ObjectProperty<List<AnyCredential>> credentials = new SimpleObjectProperty<>();
    private final List<String> allReceivedRspecString = new ArrayList<>();

    /**
     * This map keeps the state of the slice per authority where the slice has resources.
     */
    private final ObservableMap<Server, InternalState> authorityStates = FXCollections.observableHashMap();
    private final ObservableList<Sliver> slivers = FXCollections.observableArrayList();

    private final TestbedInfoSource testbedInfoSource;
    private final AuthorityFinder authorityFinder;

    /**
     * constructor is limited to package, use SfaModel to get Slices
     */
    Slice(@Nonnull GeniUrn urn, @Nonnull TestbedInfoSource testbedInfoSource, @Nonnull AuthorityFinder authorityFinder) {
        assert (Objects.equals(urn.getResourceType(), SLICE_RESOURCE_TYPE));

        this.urn = urn;
        this.testbedInfoSource = testbedInfoSource;
        this.authorityFinder = authorityFinder;

        //automatically listen to sliver all status
        slivers.addListener(sliverListChangeListener);
    }

    /**
     * @return the URN of all users in the slice.
     * 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> getUsers() {
        return Collections.unmodifiableCollection(users);
    }

    /**
     * @param userUrns the urns of the users to store. These replace the previously stored (if any).
     *                 Never stores duplicate users.
     *                 May not be null, but may be empty.
     */
    public void setUsers(@Nonnull Collection<GeniUrn> userUrns) {
        users.clear();
        users.addAll(userUrns);
    }

    /**
     * @param userUrns the urns of the users to store.
     *                 These are added the previously stored (if any).
     *                 Never stores duplicate users.
     *                 May not be null, but may be empty.
     */
    public void addUsers(@Nonnull Collection<GeniUrn> userUrns) {
        users.addAll(userUrns);
    }

    /**
     * @param userUrn the urn of the users to store.
     *                This user is added the previously stored users (if any).
     *                Never stores duplicate users. May not be null.
     */
    public void addUser(@Nonnull GeniUrn userUrn) {
        users.add(userUrn);
    }

    /**
     * Create global SliverStatusDetails by combining information from all sliver statuses.
     * <p/>
     * This global "Slice-status" is typically a simple merge of the statuses of all slivers in the slice.
     * The globalStatus is than computed via {@link be.iminds.ilabt.jfed.lowlevel.api_wrapper.StatusDetails.SliverStatus#merge},
     * which takes into account the precendence-order defined in {@link be.iminds.ilabt.jfed.lowlevel.api_wrapper.StatusDetails.SliverStatus#PRECEDENCE_ORDER}.
     * <p/>
     * There is however one exception: when the creation of a sliver on one of the component managers fails,
     * that sliver will have the status SliverStatus.UNALLOCATED.
     * We should set the global status to FAIL then, as this means that a part of the slice-creation has failed.
     * It is however normal for a sliver to have the Status UNALLOCATED when it is being removed.
     * We take that into account by checking the property "deleteRequested".
     */
    private void updateStatus() {
        //do not use list of slivers if a manifest is present: use list of auth's in manifest in that case, and fail if any is unallocated

        //create aggregated status
        if (slivers.isEmpty()) {
            LOG.trace("No slivers in slice. Setting status to UNINITIALISED");

            StatusDetails newStatus = new StatusDetails(StatusDetails.SliverStatus.UNINITIALISED);
            status.set(newStatus);
            return;
        }

        StatusDetails newStatus = new StatusDetails(StatusDetails.SliverStatus.READY);

        for (Sliver sliver : slivers) {


/*       TODO: transfer this logic elsewhere

            StatusDetails sliverStatus;
            if (sliver.getStatus().getGlobalStatus() == StatusDetails.SliverStatus.UNALLOCATED
                    && sliver.getInternalState() != InternalState.DELETING
                    && sliver.getInternalState() != InternalState.DELETED) {
                sliverStatus = new StatusDetails(StatusDetails.SliverStatus.FAIL, sliver.getUrn());
            } else {
                sliverStatus = sliver.getStatus();
            }   */

            newStatus.add(sliver.getStatus());
        }

        status.set(newStatus);
    }

    public String getName() {
        return urn.getEncodedResourceName();
    }

    public GeniUrn getUrn() {
        return urn;
    }

    public String getUrnString() {
        return urn.toString();
    }

    public String getSubAuthorityName() {
        return urn.getEncodedSubAuthName();
    }

    public Boolean getHasActiveSlivers() {
        return hasActiveSlivers.get();
    }

    public ObjectProperty<Boolean> hasActiveSliversProperty() {
        return hasActiveSlivers;
    }

    public void setHasActiveSlivers(Boolean hasActiveSlivers) {
        this.hasActiveSlivers.set(hasActiveSlivers);
    }

    /**
     * Does not check if credential has expired!
     */
    public boolean hasCredential() {
        return credentials.get() != null && !credentials.get().isEmpty();
    }

    public List<AnyCredential> getCredentials() {
        return credentials.get();
    }

    public void setCredentials(@Nonnull List<AnyCredential> credentials) throws JFedHighLevelException {
        for (AnyCredential credential : credentials) {
            if (credential == null)
                throw new NullPointerException("credential in credential list is null");
            if (credential instanceof SfaCredential) {
                SfaCredential sc = (SfaCredential) credential;
                try {
                    GeniUrn sliceUrn = new GeniUrn(sc.getTargetUrn());
                    if (!Objects.equals(sliceUrn.getEncodedResourceType(), SLICE_RESOURCE_TYPE))
                        throw new JFedHighLevelException("This is not a slice credential!");

                    if (urn.getValue() != null && (!Objects.equals(sliceUrn, urn)))
                        throw new JFedHighLevelException("This credential is for another slice. this slice=" + urn.getValue() + ", while slice urn in credential=" + sliceUrn.toString());

                    if (getExpirationDate() == null && sc.getExpiresDate() != null)
                        setExpirationDate(sc.getExpiresDate().toInstant());

                } catch (GeniUrn.GeniUrnParseException e) {
                    LOG.error("Credential target is invalid URN: '{}'", sc.getTargetUrn());
                    throw new JFedHighLevelException("Credential target is invalid URN: '" + sc.getTargetUrn() + "'", e);
                }
            } else {
                LOG.warn("Cannot process credential of type {}", credential.getClass().getName());
            }
        }

        this.credentials.set(credentials);
    }

    public ObjectProperty<List<AnyCredential>> credentialsProperty() {
        return credentials;
    }

    public String getUuid() {
        return uuid.get();
    }

    public StringProperty uuidProperty() {
        return uuid;
    }

    public String getCreator_uuid() {
        return creator_uuid.get();
    }

    public StringProperty creator_uuidProperty() {
        return creator_uuid;
    }

    public String getCreator_urn() {
        return creator_urn.get();
    }

    public StringProperty creator_urnProperty() {
        return creator_urn;
    }

    public Gid getGid() {
        return gid.get();
    }

    public void setGid(Gid gid) {
        this.gid.set(gid);
    }

    public ObjectProperty<Gid> gidProperty() {
        return gid;
    }

    public ObservableSet<Server> getManifestComponentManagers() {
        return manifestComponentManagers;
    }

    public ObservableSet<Server> getConnectAuthorities() {
        return connectAuthorities;
    }

    public StatusDetails getStatus() {
        return status.get();
    }

    public void setStatus(StatusDetails status) {
        this.status.set(status);
    }

    public ObjectProperty<StatusDetails> statusProperty() {
        return status;
    }

    public List<String> getAllLastReceivedRspecString() {
        return allReceivedRspecString;
    }

    private final ManifestRspecMerger manifestRspecMerger = new ManifestRspecMerger(ModelRspecType.FX);

    private void joinManifestRspecs() {
        LOG.trace("joinManifestRspecs()");

        if (getRequestRspec() != null) {
            manifestRspecMerger.setRequest(getRequestRspec());
        } else {
            for (Sliver sliver : slivers) {
                //use request as base manifest for merging (since real manifests might not contain nodes from all cm's)
                if (sliver.getRequestRspec() != null) {
                    manifestRspecMerger.setRequest(sliver.getRequestRspec());
                    break;
                }
            }
        }

        for (Sliver sliver : slivers) {
            Server auth = sliver.getServer();
            ManifestRspecSource sliverManifestRspec = sliver.getManifestRspec();
            manifestRspecMerger.setManifest(auth.getDefaultComponentManagerAsGeniUrn(), sliverManifestRspec); //sliverManifestRspec is allowed to be null. This means that no resources are available at the authority
        }

        ManifestRspecSource joined = manifestRspecMerger.getMergedManifest();

        //log extra debugging info
        LOG.trace("updating manifestRequest property");
        if (LOG.isDebugEnabled()) {
            if (joined != null && joined.getModelRspec() != null && joined.getModelRspec().getNodes() != null) {
                LOG.debug("   nodes in joined rspec: ");
                for (RspecNode node : joined.getModelRspec().getNodes()) {
                    LOG.debug("       - node id=\"" + node.getUniqueId()
                            + "\" client_id=\"" + node.getClientId()
                            + "\" comp_id=\"" + node.getComponentId()
                            + "\" manager=\"" + node.getComponentManagerId()
                            + "\" loginservices=" + node.getLoginServices().size());
                }
            } else if (joined != null && joined.getModelRspec() != null)
                LOG.debug("   no nodes in joined rspec: ptr->null");
            else
                LOG.debug("   no nodes in joined rspec");

        }

        manifestRspec.set(joined);
    }

    public void addSliver(Sliver sliver) {
        slivers.add(sliver);
    }

    /**
     * DANGEROUS function! Slivers should normally expire, and be kept in memory.
     * <p/>
     * Currently, the only reason to allow slivers to be removed is when a non-existing sliver was mentioned by the
     * slice authority.
     */
    void removeSliver(Sliver sliver) {
        slivers.remove(sliver);
    }

    @Nonnull
    public ObservableList<Sliver> getSlivers() {
        return /*FXCollections.unmodifiableObservableList(*/slivers/*)*/;
    }

    public Collection<Server> getAuthorities() {
        return authorityStates.keySet();
    }


    /**
     * Finds all slivers for which the Top Level Authority (without subauth) matches that of the given authority
     */
    public List<Sliver> findSlivers(@Nonnull Server auth) {
        return findSlivers(auth.getDefaultComponentManagerAsGeniUrn());
    }

    /**
     * Finds all slivers for which the Top Level Authority (without subauth) matches that of the given authority-urn
     *
     * @param cmUrn the URN of a componentManager
     */
    public List<Sliver> findSlivers(@Nonnull GeniUrn cmUrn) {
        return slivers.stream()
                .filter(sliver -> sliver.getServer().getDefaultComponentManagerAsGeniUrn().hasCompatibleAuthority(cmUrn))
                .collect(Collectors.toList());
    }

    public Sliver findSliver(@Nonnull GeniUrn urn) {
        return slivers.stream()
                .filter(sliver -> Objects.equals(urn, sliver.getUrn()))
                .findFirst().orElse(null);
    }

    @Nullable
    public Instant getReservationStartTime() {
        return reservationStartTime.get();
    }

    public ReadOnlyObjectWrapper<Instant> reservationStartTimeProperty() {
        return reservationStartTime;
    }

    public void setReservationStartTime(Instant reservationStartTime) {
        this.reservationStartTime.set(reservationStartTime);
    }

    @Nullable
    public Instant getExpirationDate() {
        return expirationDate.get();
    }

    public void setExpirationDate(Instant expirationDate) {
        this.expirationDate.set(expirationDate);
    }

    public ReadOnlyObjectProperty<Instant> expirationDateProperty() {
        return expirationDate.getReadOnlyProperty();
    }


    public ObjectProperty<RequestRspecSource> requestRspecProperty() {
        return requestRspec;
    }

    public RequestRspecSource getRequestRspec() {
        return requestRspec.get();
    }

    public void setRequestRspec(RequestRspecSource rspec) {
        requestRspec.set(rspec);

        if (rspec != null) {
            for (GeniUrn cmUrn : rspec.getAllComponentManagerUrns()) {
                if (findSlivers(cmUrn).isEmpty()) {
                    Server auth = authorityFinder.findByUrn(cmUrn, AuthorityFinder.Purpose.REQUEST_RSPEC);
                    //note that for exogeni, the sliver URN will have a sub authority, even if the CM urn does not have one.
                    // so there is NO way to create the correct sliver here.

                    if (auth != null) {
                        //we register the authority in the slice
                        authorityStates.put(auth, InternalState.UNALLOCATED);
                    } else {
                        LOG.warn("Could not find Server for urn '{}'. This will be ignored, but will cause problems w.r.t. determining the slice status, etc.!", cmUrn);
                    }
                }
            }
        }
    }

    @Nullable
    public ManifestRspecSource getManifestRspec() {
        return manifestRspec.get();
    }

    public ReadOnlyObjectProperty<ManifestRspecSource> manifestRspecProperty() {
        return manifestRspec;
    }

    @Nullable
    public ManifestRspecSource getSpecificManifestRspec(@Nonnull Server auth) {
        for (Sliver sliver : slivers) {
            Server sliverAuth = sliver.getServer();
            ManifestRspecSource sliverManifestRspec = sliver.getManifestRspec();

            if (Objects.equals(auth.getDefaultComponentManagerUrn(), sliverAuth.getDefaultComponentManagerUrn())) {
                if (sliverManifestRspec != null) {
                    return sliverManifestRspec;
                } else {
                    LOG.warn("Looking for specific manifest of authority {} in slice {}, but sliver {} does not have a manifest. Skipping",
                            auth.getName(), getName(), sliver.getUrnString());
                }
            }
        }
        return null;
    }

    @Override
    public String toString() {
        return "SfaModel Slice Info {" +
                "urn=" + urn +
                ", credentials.size()=" + (credentials.get() == null ? "null" : "" + credentials.get().size()) +
                ", hasCredential=" + hasCredential() +
                '}';
    }


    public void removeInactiveSlivers() {
        slivers.removeIf(sliver ->
                sliver.getStatus().getGlobalStatus() == StatusDetails.SliverStatus.UNALLOCATED ||
                        sliver.getStatus().getGlobalStatus() == StatusDetails.SliverStatus.UNINITIALISED);

        authorityStates.entrySet().removeIf(entry -> entry.getValue() == InternalState.DELETED);
    }

    public boolean isExpired() {
        return getExpirationDate() != null &&
                getExpirationDate().isBefore(Instant.now());
    }

    public void setAuthorityInternalState(Server auth, InternalState state) {
        authorityStates.put(auth, state);
    }

    public InternalState getAuthorityInternalState(Server authority) {
        return authorityStates.get(authority);
    }

    public ObservableMap<Server, InternalState> getAuthorityStates() {
        return /*FXCollections.unmodifiableObservableMap(*/authorityStates/*)*/;
    }

    /**
     * Normally this proprety is set by the slice authority.
     *
     * However, when the slice authority does not return a UUID,
     * we retrieve one from the SLA Collector service and use that one.
     */
    public void setUuid(String uuid) {
        this.uuid.set(uuid);
    }

    @Nullable
    public Instant getCreationDate() {
        return creationDate.get();
    }

    public ReadOnlyObjectProperty<Instant> creationDateProperty() {
        return creationDate.getReadOnlyProperty();
    }

    public void setCreationDate(Instant creationDate) {
        this.creationDate.set(creationDate);
    }
}
