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

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.Slice;
import be.iminds.ilabt.jfed.highlevel.model.Sliver;
import be.iminds.ilabt.jfed.highlevel.util.AggregateManagerWrapperFactory;
import be.iminds.ilabt.jfed.lowlevel.JFedException;
import be.iminds.ilabt.jfed.lowlevel.api.user_spec.UserSpec;
import be.iminds.ilabt.jfed.lowlevel.api_wrapper.AggregateManagerWrapper;
import be.iminds.ilabt.jfed.fedmon.webapi.service.json.*;
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 java.time.Instant;
import java.util.*;
import java.util.stream.Collectors;

/**
 * User: twalcari
 * Date: 6/16/2015
 * Time: 15:29
 */
public final class CreateSliverReservationTask extends CreateTask {
    private static final Logger LOG = LoggerFactory.getLogger(CreateSliverReservationTask.class);

    //input parameters
    private final Slice slice;
    private final Instant reservationStartTime;
    private final Instant reservationEndTime;
    private final List<UserSpec> userSpecs;
    private final Server auth;


    //results
    private String manifestRspec;
    private String status;
    private Boolean success = null;
    private Set<Sliver> createdSlivers  = null;
    private final List<GeniUrn> failedResources = new ArrayList<>();


    //environment parameters
    private final FXRspecFactory rspecFactory = FXRspecFactory.getInstance();
    private final AggregateManagerWrapperFactory aggregateManagerWrapperFactory;
    private final HighLevelTaskFactory highLevelTaskFactory;

    /**
     * Use this constructor to make a future reservation
     */
    public CreateSliverReservationTask(@Nonnull Slice slice, Server auth, @Nonnull Instant reservationStartTime,
                                       @Nonnull Instant reservationEndTime, List<UserSpec> userSpecs,
                                       AggregateManagerWrapperFactory aggregateManagerWrapperFactory,
                                       HighLevelTaskFactory highLevelTaskFactory) {
        super("Create Sliver @ " + auth.getDefaultComponentManagerUrn());
        this.slice = slice;
        this.auth = auth;
        this.reservationStartTime = reservationStartTime;
        this.reservationEndTime = reservationEndTime;
        this.userSpecs = userSpecs;

        this.aggregateManagerWrapperFactory = aggregateManagerWrapperFactory;
        this.highLevelTaskFactory = highLevelTaskFactory;
    }


    @Override
    protected void doTask(TaskExecution taskExecution) throws JFedException, InterruptedException {

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

        try {
            if (auth.hasFlag(Server.Flag.featureReservationsGeni)) {
                AggregateManagerWrapper.AllocateReply allocateReply = amWrapper.allocate(slice.getUrn(),
                        slice.getCredentials(),
                        slice.getRequestRspec().getRspecXmlString(),
                        Date.from(reservationStartTime),
                        Date.from(reservationEndTime)
                );

                if (allocateReply != null)
                    manifestRspec = allocateReply.getManifest();
            } else if (auth.hasFlag(Server.Flag.featureReservationsNitos)) {
                ModelRspec requestWithLease =
                        createRequestWithNitosLease(slice.getRequestRspec().getRspecXmlString(), auth,
                                reservationStartTime, reservationEndTime);

                RequestRspecSource requestWithLeaseSource = new RequestRspecSource(requestWithLease);

                requestWithLeaseSource = updateExpires(auth, requestWithLeaseSource, reservationEndTime);

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

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

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

                    //check if lease was successful, otherwise set manifestRspec to null to indicate a reservation failure
                    if (!failedResources.isEmpty()) {
                        status =
                                "Could not make a reservation for resource(s) " + failedResources.stream().map(GeniUrn::toString).collect(Collectors.joining(", "));
                        manifestRspec = null;
                        throw new JFedHighLevelException("Could not make a reservation for node(s) " + failedResources.stream().map(GeniUrn::toString).collect(Collectors.joining(", ")));
                    } else {
                        //success!
                        success = true;
                    }
                } else {
                    status = "Reservation call was unsuccessful";
                    success = false;
                }
            } else {
                throw new JFedHighLevelException("Making a reservation is not supported on this authority!");
            }
        } catch (JFedException ex) {
            success = false;
            throw ex;
        }
    }

    private 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.
     */
    protected 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);

        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;
    }

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

    public String getManifestRspec() {
        return manifestRspec;
    }

    public Server getServer() {
        return auth;
    }

    @Override
    public Collection<Sliver> getCreatedSlivers() {
        throw new UnsupportedOperationException();
    }

    public String getStatus() {
        return status;
    }

    public List<GeniUrn> getFailedResources() {
        return failedResources;
    }

    public Boolean isSuccess() {
        return success;
    }
}
