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

import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Server;
import be.iminds.ilabt.jfed.highlevel.controller.JFedHighLevelException;
import be.iminds.ilabt.jfed.highlevel.controller.Task;
import be.iminds.ilabt.jfed.highlevel.controller.TaskExecution;
import be.iminds.ilabt.jfed.highlevel.model.SfaModel;
import be.iminds.ilabt.jfed.highlevel.model.SfaModelAbstractListener;
import be.iminds.ilabt.jfed.highlevel.model.Slice;
import be.iminds.ilabt.jfed.highlevel.model.Sliver;
import be.iminds.ilabt.jfed.highlevel.util.AggregateManagerWrapperFactory;
import be.iminds.ilabt.jfed.highlevel.util.SliceRegistryUtil;
import be.iminds.ilabt.jfed.lowlevel.JFedException;
import be.iminds.ilabt.jfed.lowlevel.api.AbstractGeniAggregateManager;
import be.iminds.ilabt.jfed.lowlevel.api.user_spec.UserSpec;
import be.iminds.ilabt.jfed.lowlevel.api_wrapper.AggregateManagerWrapper;
import be.iminds.ilabt.jfed.rspec.model.*;
import be.iminds.ilabt.jfed.rspec.model.javafx_impl.FXModelRspec;
import be.iminds.ilabt.jfed.rspec.model.javafx_impl.FXRspecFactory;
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 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;

/**
 * User: twalcari
 * Date: 6/17/2015
 * Time: 11:50
 */
public final class CreateSliverTask extends CreateTask {
    private static final Logger LOG = LoggerFactory.getLogger(CreateSliverTask.class);
    //input parameters
    private final Slice slice;
    @Nullable
    private final Instant startTime;
    private final Instant endTime;
    private final List<UserSpec> userSpecs;

    @Nullable
    private final RequestRspecSource overridingRequestRspecSource;

    //helper fields
    private final Server server;

    //results
    private String manifestRspec;
    private Set<Sliver> createdSlivers = null;

    private final List<Task> extraDependencies = new ArrayList<>();


    private static final FXRspecFactory rspecFactory = FXRspecFactory.getInstance();
    private final AggregateManagerWrapperFactory aggregateManagerWrapperFactory;
    private final SliceRegistryUtil sliceRegistryUtil;
    private final SfaModel sfaModel;
    private final HighLevelTaskFactory highLevelTaskFactory;
    private AbstractGeniAggregateManager.AggregateManagerReply rawReply;


    /**
     * Use this constructor to create a sliver that will be used immediately!
     */
    public CreateSliverTask(Slice slice, @Nullable Instant startTime, Instant endTime, Server server, List<UserSpec> userSpecs,
                            @Nullable RequestRspecSource overridingRequestRspecSource,
                            AggregateManagerWrapperFactory aggregateManagerWrapperFactory,
                            SliceRegistryUtil sliceRegistryUtil, SfaModel sfaModel,
                            HighLevelTaskFactory highLevelTaskFactory) {
        super("Create Sliver @ " + server.getDefaultComponentManagerUrn());
        this.slice = slice;
        this.startTime = startTime;
        this.endTime = endTime;
        this.server = server;
        this.userSpecs = userSpecs;
        this.overridingRequestRspecSource = overridingRequestRspecSource;

        this.aggregateManagerWrapperFactory = aggregateManagerWrapperFactory;
        this.sliceRegistryUtil = sliceRegistryUtil;
        this.sfaModel = sfaModel;
        this.highLevelTaskFactory = highLevelTaskFactory;
    }

    @Override
    public void doTask(TaskExecution taskExecution) throws JFedException, InterruptedException {
        // Preparation: get a list of existing slivers on the given authority for the given slice.
        // This allows us to see which slivers were created during the CreateSliver-call

        List<Sliver> existingSlivers = slice.findSlivers(server);

        FXModelRspec.RequestRspecSpecialCases rspecSpecialCase = FXModelRspec.RequestRspecSpecialCases.NONE;
        if ((server.isServerType("planetlab"))
                || server.getDefaultComponentManagerUrn().startsWith("urn:publicid:IDN+ple:ibbtple+authority+")) {
            rspecSpecialCase = FXModelRspec.RequestRspecSpecialCases.PLE;
        }

        AggregateManagerWrapper amWrapper =
                aggregateManagerWrapperFactory.getAggregateManagerWrapper(taskExecution, server);

        assert server.hasFlag(Server.Flag.featureReservationsNitos) ||
                !amWrapper.hasSeperateAllocateAndProvision() :
                "AM Wrapper supports provisioning, so we shouldn't be doing CreateSliver!";

        RequestRspecSource requestRspecSource =
                overridingRequestRspecSource != null ?
                        overridingRequestRspecSource : slice.getRequestRspec();
        LOG.trace("HLC.CreateSliverTask request RSpec before modification: expires=" + requestRspecSource.getModelRspec().getExpiresString());
        requestRspecSource = updateExpires(server, requestRspecSource, endTime);
        LOG.debug("HLC.CreateSliverTask request RSpec after modification: expires=" + requestRspecSource.getModelRspec().getExpiresString());
        String rspec = requestRspecSource.getRspecXmlString(rspecSpecialCase);


        LOG.info("createSlivers @ {}  with user SSH keys: {}", server.getDefaultComponentManagerUrn(), sfaModel.getUserKeys()); //missing here
        try {
            if (startTime == null) {

                //don't set date here if CreateSliver doesn't set it itself
                Instant usedExpirationDate =
                        amWrapper.isExirationdateSetByCreateSliver()
                                || server.hasFlag(Server.Flag.workaroundForceRenewAfterCreateSliver)
                                ? endTime : null;

                this.manifestRspec = amWrapper.createSliver(
                        slice.getUrn(),
                        slice.getCredentials(),
                        rspec,
                        userSpecs,
                        usedExpirationDate != null ? Date.from(usedExpirationDate) : null
                );
                rawReply = amWrapper.getLastReply();
            } else {

                if (server.hasFlag(Server.Flag.featureReservationsNitos)) {
                    ModelRspec requestWithLease =
                            createRequestWithNitosLease(slice.getRequestRspec().getRspecXmlString(), server,
                                    startTime, endTime);

                    RequestRspecSource requestWithLeaseSource = new RequestRspecSource(requestWithLease);

                    requestWithLeaseSource = updateExpires(server, requestWithLeaseSource, endTime);

                    manifestRspec = amWrapper.createSliver(slice.getUrn(),
                            slice.getCredentials(),
                            requestWithLeaseSource.getRspecXmlString(),
                            userSpecs,
                            Date.from(endTime));

                    rawReply = amWrapper.getLastReply();

                    List<GeniUrn> failedResources = new ArrayList<>();

                    if (manifestRspec != null) {
                        failedResources.addAll(checkForFailedNitosReservations(manifestRspec, server, requestWithLease));

                        //check if lease was successful, otherwise set manifestRspec to null to indicate a reservation failure
                        if (!failedResources.isEmpty()) {
                            manifestRspec = null;
                            throw new JFedHighLevelException("Could not make a reservation for node(s) " + failedResources.stream().map(GeniUrn::toString).collect(Collectors.joining(", ")));
                        } //else: success!
                    } else {
                        throw new JFedHighLevelException("Reservation call was unsuccessful");
                    }
                } else {
                    throw new JFedHighLevelException("Making a reservation is not supported on this authority!");
                }
            }
        } catch (AggregateManagerWrapper.NotEnoughFreeResourcesException e) {
            failedDueToNotEnoughFreeResources = true;
            this.manifestRspec = null;
            throw new JFedHighLevelException("Creation of the sliver failed: could not map to resources " +
                    "(resource shortage or unsupported topology)", e);
        }

        //Note: listener that handles createSliver should update the list of component managers, as it will have changed
        if (this.manifestRspec == null)
            throw new JFedHighLevelException("Creation of the sliver failed.");

        createdSlivers = new HashSet<>();
        Collection<Sliver> manifestSlivers =
                SfaModelAbstractListener.findSliversInManifest(sfaModel, server, slice.getUrn(), new ManifestRspecSource(manifestRspec, ModelRspecType.FX));
        if (manifestSlivers != null) createdSlivers.addAll(manifestSlivers);

        //add slivers found by sfaModel due to SliverStatus and Status calls. //AL2S is one case where this is neeed, see FEDIBBTDEV-1392
        createdSlivers.addAll(slice.getSlivers().stream().filter(server::equals).collect(Collectors.toSet()));

        //we don't want to process slivers that already existed!
        createdSlivers.removeAll(existingSlivers);

        if (!createdSlivers.isEmpty())
            sliceRegistryUtil.registerSliversAtSATaskExecution(createdSlivers, this);

        if (createdSlivers.isEmpty())
            throw new JFedHighLevelException("Could not find any slivers that were newly created! " +
                    "Pre-existing slivers: " + existingSlivers.stream().map(Sliver::getUrnString).collect(Collectors.joining(", ")));

    }

    @Override
    public List<Task> initDependsOn() {
        List<Task> res = new ArrayList<>();
        res.add(highLevelTaskFactory.getSliceCredential(slice));
        res.addAll(extraDependencies);
        return res;
    }

    public void addExtraDependency(Task t) {
        extraDependencies.add(t);
    }

    public Slice getSlice() {
        return slice;
    }

    public Server getServer() {
        return server;
    }


    @Override
    public Set<Sliver> getCreatedSlivers() {
        return Collections.unmodifiableSet(createdSlivers);
    }

    public AbstractGeniAggregateManager.AggregateManagerReply getRawReply() {
        return rawReply;
    }

    public static ModelRspec createRequestWithNitosLease(@Nonnull String requestRspec, @Nonnull Server auth,
                                                         @Nonnull Instant reservationStartTime,
                                                         @Nonnull Instant reservationEndTime) throws JFedHighLevelException {
        //create a copy of the request rspec
        RequestRspecSource requestCopy =
                new RequestRspecSource(new StringRspec(requestRspec));

        if (requestCopy.getModelRspec(ModelRspecType.FX) == null) {
            throw new JFedHighLevelException("Could not parse request rspec into model: got null!");
        }
        if (requestCopy.isLosingData(ModelRspecType.FX)) {
            throw new JFedHighLevelException("Lost data while parsing request rspec: " + requestCopy.getLosingDataDifferences(ModelRspecType.FX));
        }

        ModelRspec requestWithLease = requestCopy.getModelRspec(ModelRspecType.FX);

        String clientLeaseId = UUID.randomUUID().toString();

        //we have to edit the rspec to include the lease request
        Lease newLease = rspecFactory.createLease(requestWithLease, null/*lease id*/,
                clientLeaseId,
                Date.from(reservationStartTime),
                Date.from(reservationEndTime));

        requestWithLease.addLease(newLease);

        requestWithLease.getNodes().stream()
                .filter(node -> Objects.equals(auth.getDefaultComponentManagerAsGeniUrn(), node.getComponentManagerId()))
                .forEach(node -> {
                    node.getLeaseIdRefs().clear();
                    node.addLeaseIdRef(clientLeaseId);
                });

        requestWithLease.getChannels().stream()
                .filter(channel -> Objects.equals(auth.getDefaultComponentManagerAsGeniUrn(), channel.getComponentManagerId()))
                .forEach(channel -> {
                    channel.getLeaseIdRefs().clear();
                    channel.addLeaseIdRef(clientLeaseId);
                });
        return requestWithLease;
    }

    /**
     * Checks the manifest that was received of a Nitos Reservations-compatible testbed, and checks if all reservations
     * were succesful.
     *
     * @return A list with resources that could not be reserved. A succesful reservation will return an empty list.
     */
    public static List<GeniUrn> checkForFailedNitosReservations(@Nonnull String manifestRspec, @Nonnull Server auth,
                                                                @Nonnull ModelRspec requestWithLease) {
        List<GeniUrn> failedResources = new ArrayList<>();

        //check if lease was successful, otherwise set manifestRspec to null to indicate a reservation failure

        ManifestRspecSource manifestRspecSource = new ManifestRspecSource(new StringRspec(manifestRspec));
        FXModelRspec modelRspec = manifestRspecSource.getModelRspec(ModelRspecType.FX);

        assert modelRspec != null;

        failedResources.addAll(
                modelRspec.getNodes().stream()
                        .filter(rspecNode -> Objects.equals(auth.getDefaultComponentManagerAsGeniUrn(), rspecNode.getComponentManagerId()))
                        .filter(rspecNode -> rspecNode.getLeaseIdRefs().isEmpty())
                        .map(RspecNode::getComponentId)
                        .collect(Collectors.toList())
        );


        failedResources.addAll(
                modelRspec.getChannels().stream()
                        .filter(channel -> Objects.equals(auth.getDefaultComponentManagerAsGeniUrn(), channel.getComponentManagerId()))
                        .filter(channel -> channel.getLeaseIdRefs().isEmpty())
                        .map(Channel::getComponentId)
                        .collect(Collectors.toList())
        );

        //sometimes, nodes are fully removed from the manifest instead of being returned without lease.
        // Also support this scenario!
        failedResources.addAll(
                requestWithLease.getNodes().stream()
                        .filter(node -> Objects.equals(auth.getDefaultComponentManagerAsGeniUrn(), node.getComponentManagerId()))
                        .filter(node -> modelRspec.getNodeByClientId(node.getClientId()) == null)
                        .map(RspecNode::getComponentId)
                        .collect(Collectors.toList())
        );

        //while we're at it: do the same for channels
        failedResources.addAll(
                requestWithLease.getChannels().stream()
                        .filter(channel -> Objects.equals(auth.getDefaultComponentManagerAsGeniUrn(), channel.getComponentManagerId()))
                        .filter(channel -> modelRspec.getChannelByComponentId(channel.getComponentId()) == null)
                        .map(Channel::getComponentId)
                        .collect(Collectors.toList())
        );

        return failedResources;
    }
}
