package be.iminds.ilabt.jfed.experimenter_gui.config;

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.TaskExecution;
import be.iminds.ilabt.jfed.highlevel.model.SfaModel;
import be.iminds.ilabt.jfed.highlevel.tasks.FedmonResultsTask;
import be.iminds.ilabt.jfed.highlevel.tasks.GetAdvertisementTask;
import be.iminds.ilabt.jfed.highlevel.tasks.HighLevelTaskFactory;
import be.iminds.ilabt.jfed.lowlevel.fedmon.TestResult;
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.FXRspecNode;
import be.iminds.ilabt.jfed.rspec.rspec_source.AdvertisementRspecSource;
import be.iminds.ilabt.jfed.util.GeniUrn;
import be.iminds.ilabt.jfed.util.ProgressHandler;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.scene.control.ProgressBar;
import org.slf4j.*;

import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.*;

/**
 * AdvertisementFetchManager
 */
@Singleton
public class AdvertisementFetchManager {
    private static final Logger LOG = LoggerFactory.getLogger(AdvertisementFetchManager.class);

    private static final double PROGRESS_FETCH_REQUESTED = ProgressBar.INDETERMINATE_PROGRESS;
    private static final double PROGRESS_FETCH_COMPLETE = 0.0;
    private static final double PROGRESS_PARSE_COMPLETE = 0.95;

    private final HighLevelTaskFactory hltf;

    private final JFedGuiConfigImpl config;

    private final SfaModel sfaModel;

    private final Map<Server, AvailableResources> availableResources;
    private final Map<Server, AllResources> allResources;

    private Map<GeniUrn, TestResult> fedmonResults;

//    private final ObjectProperty<AdvertisementRspecSource> anyAdvertisement = new SimpleObjectProperty<>();


    @Inject
    public AdvertisementFetchManager(
            JFedGuiConfigImpl config,
            HighLevelTaskFactory hltf,
            SfaModel sfaModel) {
        this.config = config;
        this.hltf = hltf;
        this.sfaModel = sfaModel;

        this.availableResources = new HashMap<>();
        this.allResources = new HashMap<>();
    }

//    public AdvertisementRspecSource getAnyAdvertisement(Server server) {
//        return anyAdvertisement.get();
//    }
//
//    public ObjectProperty<AdvertisementRspecSource> anyAdvertisementProperty(Server server) {
//        return anyAdvertisement;
//    }

    public AvailableResources getAvailableResources(Server server) {
        AvailableResources res = availableResources.get(server);
        if (res == null) {
            final AvailableResources newRes = new AvailableResources(server);
            res = newRes;
            availableResources.put(server, newRes);
            res.advertisementRspecSourceProperty().addListener(observable -> {
                if (newRes.getAdvertisementRspecSource() != null) {
                    config.availableAdvertisementByServerId.
                            computeIfAbsent(server.getId(), key -> new SimpleObjectProperty<>()).
                            setValue(newRes.getAdvertisementRspecSource());
                    config.anyAdvertisementByServerId.
                            computeIfAbsent(server.getId(), key -> new SimpleObjectProperty<>()).
                            setValue(newRes.getAdvertisementRspecSource());
                }
            });
        }
        assert res != null;
        return res;
    }

    public AllResources getAllResources(Server server) {
        assert server != null;
        AllResources res = allResources.get(server);
        if (res == null) {
            final AllResources newRes = new AllResources(server);
            res = newRes;
            allResources.put(server, newRes);
            res.advertisementRspecSourceProperty().addListener(observable -> {
                if (newRes.getAdvertisementRspecSource() != null) {
                    config.allAdvertisementByServerId.
                            computeIfAbsent(server.getId(), key -> new SimpleObjectProperty<>()).
                            setValue(newRes.getAdvertisementRspecSource());
                    config.anyAdvertisementByServerId.
                            computeIfAbsent(server.getId(), key -> new SimpleObjectProperty<>()).
                            setValue(newRes.getAdvertisementRspecSource());
                }
            });
        }
        assert res != null;
        return res;
    }


    public class AvailableResources extends Resources {
        public AvailableResources(Server server) {
            super(server);
        }

        @Override
        public GetAdvertisementTask getAdvertisementTask() {
            return hltf.getAvailableAdvertisement(server);
        }

        @Override
        public FedmonResultsTask getFedmonResultsTask(GeniUrn testbedUrn) {
            return hltf.getFedmonResults(testbedUrn.getValue(), "autonodelogin");
        }

        @Override
        public AdvertisementRspecSource getAdvertisementRspecSource() {
            return sfaModel.getAvailableAdvertisementRspec(server);
        }

        @Override
        public ObjectProperty<AdvertisementRspecSource> advertisementRspecSourceProperty() {
            return sfaModel.availableAdvertisementRspecProperty(server);
        }

        @Override
        public boolean mustRemoveUnavailable() {
            return true;
        }

        @Override
        public boolean mustCallListResources() {
            return !server.hasFlag(Server.Flag.fedmonHasNodeAvailabilityDetails);
        }

        @Override
        public boolean mustCallFedmon() {
            return server.hasFlag(Server.Flag.fedmonHasNodeAvailabilityDetails);
        }
    }


    public class AllResources extends Resources {
        public AllResources(Server server) {
            super(server);
        }

        @Override
        public GetAdvertisementTask getAdvertisementTask() {
            return hltf.getAllAdvertisement(server);
        }

        @Override
        public FedmonResultsTask getFedmonResultsTask(GeniUrn testbedUrn) {
            return hltf.getFedmonResults(testbedUrn.getValue(), "autonodelogin");
        }

        @Override
        public AdvertisementRspecSource getAdvertisementRspecSource() {
            return sfaModel.getAllAdvertisementRspec(server);
        }

        @Override
        public ObjectProperty<AdvertisementRspecSource> advertisementRspecSourceProperty() {
            return sfaModel.allAdvertisementRspecProperty(server);
        }

        @Override
        public boolean mustRemoveUnavailable() {
            return false;
        }

        @Override
        public boolean mustCallListResources() {
            return true;
        }

        @Override
        public boolean mustCallFedmon() {
            return server.hasFlag(Server.Flag.fedmonHasNodeAvailabilityDetails);
        }
    }


    public abstract class Resources {
        protected final Server server;

        public Resources(Server server) {
            assert server != null;
            this.server = server;
        }

        private final ObservableList<FXRspecNode> advertisementNodes = FXCollections.observableArrayList();

        private final ObjectProperty<RefreshAdvertisementTask> currentRefreshAdvertisementTask =
                new SimpleObjectProperty<>();

        public abstract FedmonResultsTask getFedmonResultsTask(GeniUrn testbedUrn);

        public abstract GetAdvertisementTask getAdvertisementTask();

        public abstract AdvertisementRspecSource getAdvertisementRspecSource();

        public abstract ObjectProperty<AdvertisementRspecSource> advertisementRspecSourceProperty();

        /**
         * if true, nodes reported as unavailable by FedMon will be removed.
         */
        public abstract boolean mustRemoveUnavailable();

        public ObservableList<FXRspecNode> getAdvertisementNodes() {
            return advertisementNodes;
        }

        /**
         * Checks if the "available component-ids" for this component manager are available
         */
        public boolean isComponentIdsAvailable() {
            return /*getAdvertisementRspecSource() != null ||*/
                    !config.getComponentInfo(server, this instanceof AllResources ? JFedGuiConfig.Scope.ALL : JFedGuiConfig.Scope.AVAILABLE_ONLY).isEmpty();
        }


        /**
         * Starts the refresh of the available componentIds in Component Manager.
         * This refresh is performed asynchronously, and a task is returned which allows you to follow the progress.
         *
         * @return The task that performs the action. Can be used by a GUI to track progress
         */
        public RefreshAdvertisementTask refreshAdvertisement() {
            LOG.debug("Called refreshAdvertisement("+server.getId()+")");
            if (currentRefreshAdvertisementTask.get() == null) {
                currentRefreshAdvertisementTask.set(new RefreshAdvertisementTask());
                new Thread(currentRefreshAdvertisementTask.get()).start();
            }

            return currentRefreshAdvertisementTask.get();
        }

        public abstract boolean mustCallListResources();
        public abstract boolean mustCallFedmon();

        public class RefreshAdvertisementTask extends Task<Void> {
            @Override
            protected Void call() throws Exception {
                GetAdvertisementTask fetchAdvertisementTask = mustCallListResources() ?
                        getAdvertisementTask() : null;

                TaskExecution taskExecution = mustCallListResources() ?
                        hltf.submitTask(fetchAdvertisementTask) : null;
                updateProgress(PROGRESS_FETCH_REQUESTED, 1);

                TaskExecution taskExecution2 = null;
                FedmonResultsTask fedmonNodeStatusTask = null;
                if (mustCallFedmon()) {
                    LOG.debug("Fetching fedmon results.");
                    fedmonNodeStatusTask = getFedmonResultsTask(server.getDefaultComponentManagerAsGeniUrn());

                    taskExecution2 = hltf.submitTask(fedmonNodeStatusTask);
                }

                while ((taskExecution != null && !taskExecution.isCompleted()) || (taskExecution2 != null && !taskExecution2.isCompleted())) {
                    Thread.sleep(100);
                }
                assert taskExecution == null  || taskExecution.isCompleted();
                assert taskExecution2 == null || taskExecution2.isCompleted();

                if (taskExecution != null && taskExecution.getState() != TaskExecution.TaskState.SUCCESS) {
                    throw new JFedHighLevelException("Unexpected state for getAdvertisement task: " + taskExecution.getState(),
                            taskExecution.getException());
                }

                //HACK wait for availableAdvertismentRspec to become available, this happens by a process in another thread..

                while (taskExecution != null && getAdvertisementRspecSource() == null) {
                    Thread.sleep(100);
                }

                updateProgress(PROGRESS_FETCH_COMPLETE, 1);

                assert taskExecution == null || getAdvertisementRspecSource() != null;

                if (taskExecution != null) {
                    ProgressHandler parserProgressHandler =
                            (progress, totalWork) -> updateProgress(PROGRESS_FETCH_COMPLETE +
                                            (PROGRESS_PARSE_COMPLETE - PROGRESS_FETCH_COMPLETE) * (progress / totalWork),
                                    1);

                    //register parserProgressHandler
                    getAdvertisementRspecSource().getModelRspec(parserProgressHandler);
                }

                if (fedmonNodeStatusTask != null) {
                    assert taskExecution2 != null;
                    if (taskExecution2.getState() != TaskExecution.TaskState.SUCCESS)
                        LOG.warn("Fedmon Status task status is "+taskExecution2.getState());

                    List<TestResult> testResults = fedmonNodeStatusTask.getResults();
                    if (fedmonResults == null)
                        fedmonResults = new HashMap<>();

                    if (testResults != null)
                        if (!testResults.isEmpty())
                            for (TestResult testResult : testResults)
                                if (testResult.getFixedNodeUrn() != null) {
                                    GeniUrn nodeUrn = GeniUrn.parse(testResult.getFixedNodeUrn());
                                    if (nodeUrn != null)
                                        fedmonResults.put(nodeUrn, testResult);
                                    else
                                        LOG.warn("Invalid node urn in fedmon reply: \"" + testResult.getFixedNodeUrn() + "\"");
                                } else
                                    LOG.warn("Node without urn in fedmon reply id=" + testResult.getId());
                        else
                            LOG.warn("Fedmon Status task has empty results");
                    else
                        LOG.warn("Fedmon Status task has null results");
                }

                updateProgress(PROGRESS_PARSE_COMPLETE, 1);

                return null;
            }

            @Override
            protected void scheduled() {
                updateProgress(0, 1);
            }

            @Override
            protected void succeeded() {
                updateComponentIds();

            }

            @Override
            protected void done() {
                //remove the current refresh task from the ComponentManagerInfo
                currentRefreshAdvertisementTask.set(null);
            }
        }

        public RefreshAdvertisementTask getCurrentRefreshAdvertisementTask() {
            return currentRefreshAdvertisementTask.get();
        }

        public ObjectProperty<RefreshAdvertisementTask> currentRefreshAdvertisementTaskProperty() {
            return currentRefreshAdvertisementTask;
        }

        /**
         * Returns a list with available component-Ids.
         * <p>
         * This method will only passivly fill the CompomentManagerInfo-object if there is
         * information available from a ListResources-call. It will not actively perform one (use refreshAdvertisement for that)!
         */
        public void updateComponentIds() {
            //check if we have an advertisement Rspec available. If not, we have nothing to populate!


            if (fedmonResults != null && !fedmonResults.isEmpty()) {
                config.setFedmonNodeTestInfo(server.getId(), fedmonResults);
            }

            List<FXRspecNode> nodes;
            if (getAdvertisementRspecSource() == null
                    || getAdvertisementRspecSource().getModelRspec(ModelRspecType.FX) == null) {

                LOG.info("No advertisement node-info available for testbed {}.", server.getName());
                return;
            } else {
                //user advertisement RSpec

                FXModelRspec modelRspec =
                        getAdvertisementRspecSource().getModelRspec(ModelRspecType.FX);

                //make a copy, so that it can be sorted!
                nodes = new ArrayList<>(modelRspec.getNodes());
                for (Iterator<FXRspecNode> it = nodes.listIterator(); it.hasNext(); ) {
                    FXRspecNode node = it.next();
                    if (node == null || node.getComponentId() == null) {
                        LOG.warn("Item in received node list has no component_id. Will be ignored!");
                        it.remove();
                        continue;
                    }
                    //use component_id as fallback when component_name is not present
                    if (node.getComponentName() == null)
                        node.setComponentName(node.getComponentId().toString());

                    //FEDIBBTDEV-1591 remove unavailble nodes that are reported in an "available" advertisement RSpec (= workaround for AM bug)
                    if (node.getAvailable() == Boolean.FALSE && mustRemoveUnavailable()) { //dirty
                        it.remove();
                        continue;
                    }
                }

                LOG.debug("Added {} nodes to the list of testbed {}",
                        modelRspec.getNodes().size(), server.getName());

                //TODO: do not use this anymore
                advertisementNodes.setAll(nodes);

                config.setAdvertisementRspec(server.getId(), this instanceof AvailableResources, nodes, new Date());
            }

//            //noinspection ConstantConditions
//            nodes.sort(Comparator.comparing(FXRspecNode::getComponentName));
//
//            //sort them according to their name


        }


    }
}
