package be.iminds.jfed.gts_highlevel.jobs.gts_parts;

import be.iminds.ilabt.jfed.experiment.tasks.ExperimentTaskStatus;
import be.iminds.ilabt.jfed.highlevel.controller.TaskExecution;
import be.iminds.ilabt.jfed.highlevel.jobs.ExperimentPartStateSlice;
import be.iminds.ilabt.jfed.highlevel.jobs.Job;
import be.iminds.ilabt.jfed.highlevel.jobs.SingleCallState;
import be.iminds.ilabt.jfed.highlevel.jobs.State;
import be.iminds.ilabt.jfed.highlevel.model.InternalState;
import be.iminds.ilabt.jfed.lowlevel.JFedException;
import be.iminds.ilabt.jfed.rspec.model.javafx_impl.FXGeantTestbedType;
import be.iminds.jfed.gts_highlevel.GtsHighLevelException;
import be.iminds.jfed.gts_highlevel.controller.GtsException;
import be.iminds.jfed.gts_highlevel.controller.GtsModel;
import be.iminds.jfed.gts_highlevel.model.GtsXmlProject;
import be.iminds.jfed.gts_highlevel.model.GtsXmlReservation;
import be.iminds.jfed.gts_highlevel.model.GtsXmlResource;
import be.iminds.jfed.gts_highlevel.tasks.GtsListResourcesTask;
import be.iminds.jfed.gts_highlevel.tasks.GtsTaskFactory;
import be.iminds.jfed.gts_highlevel.tasks.StartGtsResourcesTask;
import be.iminds.jfed.gts_highlevel.tasks.UpdateGtsReservationTask;
import com.google.common.base.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * User: twalcari
 * Date: 1/15/2016
 * Time: 12:13
 */
public class StartGtsExperimentPartStateSlice extends ExperimentPartStateSlice<GtsExperimentPart> {
    private static final Logger LOG = LoggerFactory.getLogger(StartGtsExperimentPartStateSlice.class);
    private static final int DEFAULT_WAIT_FOR_READY_DELAY = 5_000;
    private static final int MAX_ERROR_COUNT = 3;

    private final Marker logMarker;
    private final FXGeantTestbedType gtt;

    private final GtsXmlProject project;

    private final GtsModel model;
    private final GtsTaskFactory gtsTaskFactory;

    private final int waitForReadyDelay = DEFAULT_WAIT_FOR_READY_DELAY;


    public StartGtsExperimentPartStateSlice(Job<?> job, GtsExperimentPart experimentPart, GtsModel model, GtsTaskFactory gtsTaskFactory) {
        super(job, experimentPart);
        this.model = model;
        this.gtsTaskFactory = gtsTaskFactory;

        this.gtt = experimentPart.getGeantTestbedType();
        this.project = experimentPart.getContainer().getGtsProject();

        this.logMarker = MarkerFactory.getMarker("GTS-" + experiment.getName() + "-" + gtt.getName());
    }

    @Override
    public ExperimentTaskStatus statefulRun() throws JFedException, InterruptedException {

        if (project == null)
            throw new GtsHighLevelException("Could not find GtsProject attached to this experiment!");

        LOG.trace(logMarker, "Starting GTS resources {} in project {}", gtt.getName(), project.getId());

        experimentPart.setState(InternalState.ALLOCATING);

        StartGtsResourcesState startGtsResourcesState = new StartGtsResourcesState();
        setAndRunState(startGtsResourcesState);


        if (startGtsResourcesState.getStatus() == ExperimentTaskStatus.SUCCESS) {
            experimentPart.setState(InternalState.ALLOCATED);
        } else {
            experimentPart.setState(InternalState.FAILED);
            LOG.error(logMarker, "Could not start GTS resources", startGtsResourcesState.getTaskExecution().getException());
            throw new GtsHighLevelException("Could not start GTS resources", startGtsResourcesState.getTaskExecution().getException());
        }
        assert startGtsResourcesState.getTask().getType() != null;
        experimentPart.registerType(startGtsResourcesState.getTask().getType());

        assert startGtsResourcesState.getTask().getReservation() != null;

        LOG.debug("Registering result reservation {} for GTS type {}",
                startGtsResourcesState.getTask().getReservation().getId(), gtt.getName());
        experimentPart.setReservation(startGtsResourcesState.getTask().getReservation());

        experimentPart.setState(InternalState.WAIT_FOR_READY);
        //list resources before continuing, this allows GUI to show status
        setAndRunState(new GtsListResourcesState());

        //try to find resources associated with reservation
        assert experimentPart.getReservation() != null;
        GtsXmlResource rootResource = getReservationRootResource(experimentPart.getReservation());
        if (rootResource != null) {
            experimentPart.registerResource(rootResource);
        } else {
            LOG.warn(logMarker, "Could not identify resource linked to reservation {}", experimentPart.getReservation().getId());
        }

        setAndRunState(new UpdateUntilReadyState());

        //update one last time for the final state of the resources
        setAndRunState(new GtsListResourcesState());

        experimentPart.setState(InternalState.READY);

        return null;
    }

    @Nullable
    private GtsXmlResource getReservationRootResource(@Nonnull GtsXmlReservation reservation) {
        for (GtsXmlResource gtsResource : model.getResources().values()) {
            if (Objects.equals(reservation.getId(), gtsResource.getReservations().getId()))
                return gtsResource;
        }
        return null;
    }

    private final class UpdateUntilReadyState extends State {
        private final UpdateGtsReservationTask updateTask;


        private int triesCount = 0;
        private int errorCount = 0;

        protected UpdateUntilReadyState() {
            super("Update reservations in project '" + project.getId() + "' for '" + experimentPart.getReservation().getId() + "'");
            this.updateTask =
                    gtsTaskFactory.createUpdateGtsReservationTask(project, experimentPart.getReservation().getId());
        }

        @Nonnull
        @Override
        protected ExperimentTaskStatus executeState(Job<?> job) throws InterruptedException, JFedException {
            assert experimentPart.getReservation() != null;

            boolean finalState = false;
            boolean fatalStop = false;

            while (!finalState && !fatalStop) {

                //Wait the requested time before querying status
                int waited = 0;
                while (waited < waitForReadyDelay) {
                    updateMessage(String.format("Checked status %d times. Next check in %d seconds",
                            triesCount,
                            roundUp((waitForReadyDelay - waited), 1000)));

                    Thread.sleep(1000);
                    waited += 1000;
                }

                //Check status, and keep user updated on the progress
                CountDownLatch statusTaskExecutionReadyLatch = new CountDownLatch(1);

                TaskExecution<UpdateGtsReservationTask> taskExecution = job.submitTask(updateTask,
                        (task, taskExecution1, state) -> statusTaskExecutionReadyLatch.countDown());
                triesCount++;

                int punctuationCount = -1;
                do {
                    updateMessage(String.format("Checked status %d times. Checking%s",
                            triesCount,
                            Strings.repeat(".", 1 + ++punctuationCount % 3)));
                } while (!statusTaskExecutionReadyLatch.await(1, TimeUnit.SECONDS));

                if (taskExecution.getState() == TaskExecution.TaskState.SUCCESS) {
                    if (!updateTask.isFinalState()) {
                        LOG.debug("Reservation {} is in transitioning state {}. Updating again in 10 seconds....",
                                experimentPart.getReservation().getId(), updateTask.getResult().getStatus());
                    } else {
                        LOG.info("Reservation {} reached final state {}. Quiting updates.",
                                experimentPart.getReservation().getId(), updateTask.getResult().getStatus());
                        updateMessage("");
                        finalState = true;
                    }
                } else if (taskExecution.getState() == TaskExecution.TaskState.FAILED
                        && taskExecution.getException() instanceof GtsException
                        && taskExecution.getException().getMessage().startsWith("Could not find reservation ")) {
                    LOG.error("Could not find reservation {} in reservations list received! Quiting updates.", experimentPart.getReservation().getId());
                    updateMessage("Could not find reservation. Aborting checks!");
                    fatalStop = true;
                } else {
                    errorCount++;
                    if (errorCount >= MAX_ERROR_COUNT) {
                        updateMessage("Got " + errorCount + " errors. Aborting checks!");
                        fatalStop = true;
                    }
                }
            }

            if (fatalStop)
                return ExperimentTaskStatus.FAILED;
            else if (updateTask.getResult() != null && updateTask.getResult().getStatus() == GtsXmlReservation.Status.ACTIVE)
                return ExperimentTaskStatus.SUCCESS;
            else
                return ExperimentTaskStatus.WARNING;


        }


    }

    private static long roundUp(long num, long divisor) {
        return (num + divisor - 1) / divisor;
    }

    private final class StartGtsResourcesState extends SingleCallState<StartGtsResourcesTask> {
        protected StartGtsResourcesState() {
            super(gtsTaskFactory.createStartGtsResourcesTask(gtt.getName(), gtt.getDsl(), project));
        }
    }

    private final class GtsListResourcesState extends SingleCallState<GtsListResourcesTask> {
        protected GtsListResourcesState() {
            super(gtsTaskFactory.createGtsListResourcesTask(project));
        }
    }
}
