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

import be.iminds.ilabt.jfed.experiment.Experiment;
import be.iminds.ilabt.jfed.experiment.ExperimentPart;
import be.iminds.ilabt.jfed.experiment.SfaExperimentPart;
import be.iminds.ilabt.jfed.experiment.tasks.ExperimentTaskStatus;
import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Server;
import be.iminds.ilabt.jfed.highlevel.controller.TaskThread;
import be.iminds.ilabt.jfed.highlevel.jobs.parts.ExperimentPartController;
import be.iminds.ilabt.jfed.highlevel.jobs.parts.ExperimentPartControllerManager;
import be.iminds.ilabt.jfed.highlevel.jobs.parts.NonSfaExperimentPartController;
import be.iminds.ilabt.jfed.highlevel.jobs.sfa_parts.RegisterSliversToSlaExperimentPartStateSlice;
import be.iminds.ilabt.jfed.highlevel.jobs.sfa_parts.UpdateSfaExperimentPartUntilReadyStateSlice;
import be.iminds.ilabt.jfed.highlevel.jobs.states.JobStateFactory;
import be.iminds.ilabt.jfed.highlevel.jobs.states.ProvisionSliversOnAuthorityState;
import be.iminds.ilabt.jfed.highlevel.jobs.states.ShareLanAtAuthorityPartState;
import be.iminds.ilabt.jfed.highlevel.model.InternalState;
import be.iminds.ilabt.jfed.highlevel.tasks.HighLevelTaskFactory;
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.rspec.model.RspecLink;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;
import java.util.stream.Collectors;

/**
 * User: twalcari
 * Date: 12/2/2015
 * Time: 16:43
 */
public class ProvisionExperimentJob extends AbstractJob<Boolean> {
    private static final Logger LOG = LoggerFactory.getLogger(ProvisionExperimentJob.class);

    //this variable is typically set for debugging purposes
    private Integer waitForReadyDelay = null;

    private final JobStateFactory jobStateFactory;

    private final AggregateManagerWrapperFactory aggregateManagerWrapperFactory;
    private final ExperimentPartControllerManager epcManager;

    private Boolean success = null;

    private final boolean noWaitUntilReady;

    public ProvisionExperimentJob(Experiment experiment, HighLevelTaskFactory hltf,
                                  TaskThread tt, JobStateFactory jobStateFactory,
                                  AggregateManagerWrapperFactory aggregateManagerWrapperFactory, ExperimentPartControllerManager epcManager, List<UserSpec> userSpecs) {
        this(experiment, hltf, tt, jobStateFactory, aggregateManagerWrapperFactory, epcManager,  false /*noWaitUntilReady*/);
    }

    public ProvisionExperimentJob(Experiment experiment, HighLevelTaskFactory hltf,
                                  TaskThread tt, JobStateFactory jobStateFactory,
                                  AggregateManagerWrapperFactory aggregateManagerWrapperFactory, ExperimentPartControllerManager epcManager, boolean noWaitUntilReady) {
        super("Provision Experiment " + experiment.getName(), experiment, hltf, tt);
        this.jobStateFactory = jobStateFactory;
        this.aggregateManagerWrapperFactory = aggregateManagerWrapperFactory;
        this.epcManager = epcManager;
        this.noWaitUntilReady = noWaitUntilReady;

        assert experiment.getRequestedEndTime() != null;
    }

    @Override
    public Boolean execute() throws Exception {
        if (!hasSfaParts())
            return true;

        boolean permanentFailure = false;


        final ProvisionPartsState provisionPartsState = new ProvisionPartsState();
        setAndRunState(provisionPartsState);

        if (!provisionPartsState.isPermanentFailure()) {
            //Now the Provision-step has passed without failure, we can register the slivers to the SLA-service
            if (experiment.isRequestSla()) {
                final RegisterSfaExperimentPartsToSlaCollectorState
                        registerSliversToSlaState = new RegisterSfaExperimentPartsToSlaCollectorState();
                setAndRunState(registerSliversToSlaState);
            }

            if (noWaitUntilReady) {
                success = true;
                return true;
            }
            final UpdateSfaPartsUntilReadyState updateUntilReadyState =
                    new UpdateSfaPartsUntilReadyState();
            setAndRunState(updateUntilReadyState);

            if (updateUntilReadyState.isPermanentFailure()) {
                permanentFailure = true;
                LOG.info("WaitForPartsReadyState reported permanent failure");
            }
        } else {
            permanentFailure = true;
            LOG.info("ProvisionPartsState reported permanent failure");
        }


        if (permanentFailure) {
            success = false;
            return false;
        }

        //everything is now successfully running
        if (experiment.getSlice() != null && experiment.getSlice().getRequestRspec() != null) {
            boolean hasAutoShareLans = false;
            Collection<? extends RspecLink> links = experiment.getSlice().getRequestRspec().getModelRspec().getLinks();
            for (RspecLink link : links) {
                List<String> autoShareLanNames = link.getAutoShareLanNames();
                if (autoShareLanNames != null && !autoShareLanNames.isEmpty()) {
                    hasAutoShareLans = true;
                    break;
                }
            }
            if (hasAutoShareLans) {
                LOG.info("Experiment has auto shared LANs. Will share them.");
                final AutoShareLanPartsState autoShareLanPartsState = new AutoShareLanPartsState();
                setAndRunState(autoShareLanPartsState);

                LOG.debug("Experiment auto shared LANs have been shared. Will wait for ready again.");
                permanentFailure = false;
                do {
                    final UpdateSfaPartsUntilReadyState updateUntilReadyState =
                            new UpdateSfaPartsUntilReadyState();
                    setAndRunState(updateUntilReadyState);

                    if (updateUntilReadyState.isPermanentFailure()) {
                        permanentFailure = true;
                        LOG.info("WaitForPartsReadyState reported permanent failure");
                    }
                } while (!permanentFailure);

                LOG.info("All is ready after sharing LANs -> all ready");
            } else
                LOG.info("Experiment has NO auto shared LANs -> all ready");

            if (!experiment.getKeypairs().isEmpty()) {
                setAndRunState(jobStateFactory.createDistributeSshKeysState(this));
            }

            setAndRunState(jobStateFactory.createExecuteAnsibleServicesState(this));
        } else
            LOG.warn("problem with experiment.getSlice().getRequestRspec()");


        success = true;
        return true;
    }

    public Boolean isSuccess() {
        return success;
    }

    public class AutoShareLanPartsState extends SlicedState<AutoShareLanPartStateSlice> {
        private final List<AutoShareLanPartStateSlice> slices;

        protected AutoShareLanPartsState() {
            super("Auto Sharing LANs in all ExperimentParts");

            slices = new ArrayList<>();

            List<SfaExperimentPart> sfaExperimentParts =
                    experiment.getParts().stream()
                            .filter(part -> part.getState() != InternalState.FAILED)
                            .filter(part -> part instanceof SfaExperimentPart)
                            .map(part -> (SfaExperimentPart) part)
                            .collect(Collectors.toList());

            Collection<? extends RspecLink> links = experiment.getSlice().getRequestRspec().getModelRspec().getLinks();

            for (SfaExperimentPart sfaExperimentPart : sfaExperimentParts) {
                Server auth = sfaExperimentPart.getConnectSfaAuthority();
                boolean hasAutoShareLans = false;

                for (RspecLink link : links) {
                    List<String> autoShareLanNames = link.getAutoShareLanNames();
                    if (autoShareLanNames != null && !autoShareLanNames.isEmpty() && link.getComponentManagerUrns().contains(auth.getDefaultComponentManagerAsGeniUrn())) {
                        hasAutoShareLans = true;
                        break;
                    }
                }

                if (hasAutoShareLans) {
                    LOG.debug("Auto Sharing LAN(s) in " + auth.getName());
                    AutoShareLanPartStateSlice slice = new AutoShareLanPartStateSlice(sfaExperimentPart);
                    slices.add(slice);
                } else {
                    LOG.debug("No Auto Shared LAN  in " + auth.getName());
                }
            }

            LOG.debug("AutoShareLanPartsState constructor finished with " + slices.size() + " slices.");

            assert !slices.isEmpty();
        }

        @Override
        public Collection<AutoShareLanPartStateSlice> getSlices() {
            return slices;
        }
    }

    public class ProvisionPartsState extends SlicedState<ProvisionSfaExperimentPartJobSlice> {
        private final List<ProvisionSfaExperimentPartJobSlice> slices;

        protected ProvisionPartsState() {
            super("Provision all ExperimentParts");

            slices = experiment.getParts().stream()
                    .filter(part -> part.getState() != InternalState.FAILED)
                    .filter(part -> part instanceof SfaExperimentPart)
                    .map(part -> new ProvisionSfaExperimentPartJobSlice((SfaExperimentPart) part))
                    .collect(Collectors.toList());
        }


        public boolean isPermanentFailure() {
            for (ProvisionSfaExperimentPartJobSlice slice : slices) {
                if(slice.getResult() == ExperimentTaskStatus.FAILED)
                    return true;
            }
            return false;
        }

        @Override
        public Collection<ProvisionSfaExperimentPartJobSlice> getSlices() {
            return slices;
        }
    }

    public class ProvisionSfaExperimentPartJobSlice extends ExperimentPartStateSlice<SfaExperimentPart> {

        public ProvisionSfaExperimentPartJobSlice(SfaExperimentPart experimentPart) {
            super(ProvisionExperimentJob.this, experimentPart);
        }

        @Override
        public ExperimentTaskStatus statefulRun() throws JFedException, InterruptedException {
            if (!supportsAllocateProvision(experimentPart.getConnectSfaAuthority())) {
                LOG.debug("Skipping provisioning on {}, as it does not support it.", experimentPart.getName());
                return ExperimentTaskStatus.SUCCESS;
            }

            ProvisionSliversOnAuthorityState provisionSliversOnAuthorityState =
                    jobStateFactory.createProvisionSliversOnAuthorityState(ProvisionExperimentJob.this, experimentPart, experiment.getUserSpecs());

            setAndRunState(provisionSliversOnAuthorityState);

            return provisionSliversOnAuthorityState.getStatus();
        }

        private boolean supportsAllocateProvision(Server auth) {
            AggregateManagerWrapper amWrapper =
                    aggregateManagerWrapperFactory.getAggregateManagerWrapper(auth);

            return amWrapper.hasSeperateAllocateAndProvision();
        }
    }

    public class RegisterSfaExperimentPartsToSlaCollectorState extends SfaExperimentPartsSlicedState<RegisterSliversToSlaExperimentPartStateSlice> {

        private RegisterSfaExperimentPartsToSlaCollectorState() {
            super("Register resources at SLA Collector", getExperiment());
        }

        @Override
        public RegisterSliversToSlaExperimentPartStateSlice createJobSlice(SfaExperimentPart experimentPart) {
            return new RegisterSliversToSlaExperimentPartStateSlice(ProvisionExperimentJob.this, experimentPart, hltf);
        }
    }


    public class AutoShareLanPartStateSlice extends ExperimentPartStateSlice<SfaExperimentPart> {

        public AutoShareLanPartStateSlice(SfaExperimentPart experimentPart) {
            super(ProvisionExperimentJob.this, experimentPart);
        }

        @Override
        public ExperimentTaskStatus statefulRun() throws JFedException, InterruptedException {
            if (!supportsShareLan(experimentPart.getConnectSfaAuthority())) {
                LOG.warn("Share LAN on {} is not supported. Will try anyway.", experimentPart.getName());
                //return ExperimentTaskStatus.WARNING;
            }
            Collection<? extends RspecLink> links = experiment.getSlice().getRequestRspec().getModelRspec().getLinks();

            ExperimentTaskStatus worstStatus = null;
            Server auth = experimentPart.getConnectSfaAuthority();
            LOG.debug("Start sharing LAN(s) on " + auth.getDefaultComponentManagerAsGeniUrn());
            for (RspecLink link : links) {
                List<String> autoShareLanNames = link.getAutoShareLanNames();
                if (autoShareLanNames != null && !autoShareLanNames.isEmpty() && link.getComponentManagerUrns().contains(auth.getDefaultComponentManagerAsGeniUrn())) {
                    String sharedLanName = autoShareLanNames.get(0);

                    if (autoShareLanNames.size() > 1)
                        LOG.warn("There is more than one auto share LAN name per link (for link\"" + link.getClientId() + "\"), ignoring all but first.");

                    LOG.debug("Sharing LAN " + link.getClientId() + " as " + sharedLanName + " on " + auth.getDefaultComponentManagerAsGeniUrn());

                    ShareLanAtAuthorityPartState shareLanState =
                            jobStateFactory.createShareLanState(ProvisionExperimentJob.this, experimentPart, link.getClientId(), sharedLanName);

                    setAndRunState(shareLanState);
                    ExperimentTaskStatus status = shareLanState.getStatus();
                    LOG.debug("Shared LAN " + link.getClientId() + " as " + sharedLanName + " on " + auth.getDefaultComponentManagerUrn() + " -> status = " + status);

                    if (status != ExperimentTaskStatus.SUCCESS || worstStatus == null) {
                        worstStatus = status;
                    }
                }
            }

            //there should be at least one link shared, or this shouldn't have been called.
            assert worstStatus != null;

            LOG.debug("Stopped sharing LAN(s) on " + auth.getDefaultComponentManagerUrn() + " -> worst status = " + worstStatus);

            return worstStatus;
        }

        private boolean supportsShareLan(Server auth) {
            AggregateManagerWrapper amWrapper =
                    aggregateManagerWrapperFactory.getAggregateManagerWrapper(auth);

            return amWrapper.isShareLanSupported();
        }
    }

    public class UpdateSfaPartsUntilReadyState extends SlicedState<UpdateSfaExperimentPartUntilReadyStateSlice> {
        private final List<UpdateSfaExperimentPartUntilReadyStateSlice> slices;

        private UpdateSfaPartsUntilReadyState() {
            super("Waiting for resources to become ready");

            slices = experiment.getParts().stream()
                    .filter(part -> part.getState() != InternalState.FAILED)
                    .filter(part -> part instanceof SfaExperimentPart)
                    .map(part -> new UpdateSfaExperimentPartUntilReadyStateSlice(ProvisionExperimentJob.this, (SfaExperimentPart) part, waitForReadyDelay, jobStateFactory))
                    .collect(Collectors.toList());
        }


        public boolean isPermanentFailure() {
            return getSlices().stream().anyMatch(updateSlice -> updateSlice.getResult() != ExperimentTaskStatus.SUCCESS);
        }

        @Override
        public Collection<UpdateSfaExperimentPartUntilReadyStateSlice> getSlices() {
            return slices;
        }
    }

    public void setWaitForReadyDelay(Integer waitForReadyDelay) {
        this.waitForReadyDelay = waitForReadyDelay;
    }


    private Collection<SfaExperimentPart> getSfaParts() {
        return experiment.getParts().stream()
                .filter(part -> part instanceof SfaExperimentPart)
                .map(SfaExperimentPart.class::cast)
                .collect(Collectors.toList());

    }


    private Collection<NonSfaExperimentPartController> getNonSfaExperimentPartControllers() {

        Set<NonSfaExperimentPartController> controllers = new HashSet<>();

        for (ExperimentPart experimentPart : experiment.getParts()) {
            if (experimentPart instanceof SfaExperimentPart)
                continue;


            ExperimentPartController epc = epcManager.getController(experimentPart);
            if (!(epc instanceof NonSfaExperimentPartController))
                throw new RuntimeException("Expected a non-sfa ExperimentPartController");

            controllers.add((NonSfaExperimentPartController) epc);
        }

        return controllers;
    }

    private boolean hasNonSfaParts() {
        return experiment.getParts().stream().anyMatch(part -> !(part instanceof SfaExperimentPart));
    }

    private boolean hasSfaParts() {
        return experiment.getParts().stream().anyMatch(part -> (part instanceof SfaExperimentPart));
    }

    public final class CreateNonSfaExperimentPartsContainersState extends SlicedState<StateSlice> {

        private final List<StateSlice> slices;

        protected CreateNonSfaExperimentPartsContainersState() {
            super("Creating non-SFA experimentparts containers");
            slices = getNonSfaExperimentPartControllers().stream()
                    .map(epc -> epc.initializeContainer(ProvisionExperimentJob.this))
                    .collect(Collectors.toList());
        }

        @Override
        public Collection<StateSlice> getSlices() {
            return slices;
        }
    }


    public final class StartNonSfaPartsState extends SlicedState<StateSlice> {

        private final List<StateSlice> slices;

        protected StartNonSfaPartsState() {
            super("Starting non-SFA resources");

            slices = new ArrayList<>();

            for (ExperimentPart experimentPart : experiment.getParts()) {
                if (experimentPart instanceof SfaExperimentPart)
                    continue;

                ExperimentPartController epc = epcManager.getController(experimentPart);
                if (!(epc instanceof NonSfaExperimentPartController))
                    throw new RuntimeException("Expected a non-sfa ExperimentPartController");

                //noinspection unchecked
                slices.add(
                        ((NonSfaExperimentPartController) epc).start(ProvisionExperimentJob.this, experimentPart));

            }
        }

        @Override
        public Collection<StateSlice> getSlices() {
            return slices;
        }
    }

}
