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

import be.iminds.ilabt.jfed.experiment.util.ExperimentRestoreInformation;
import be.iminds.ilabt.jfed.experimenter_gui.events.StartExperimentEvent;
import be.iminds.ilabt.jfed.experimenter_gui.reservations.StartEndDurationPane;
import be.iminds.ilabt.jfed.experimenter_gui.util.ui.UIUtils;
import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Server;
import be.iminds.ilabt.jfed.highlevel.model.SfaModel;
import be.iminds.ilabt.jfed.highlevel.model.Slice;
import be.iminds.ilabt.jfed.highlevel.tasks.HighLevelTaskFactory;
import be.iminds.ilabt.jfed.highlevel.tasks.ListSubAuthoritiesTask;
import be.iminds.ilabt.jfed.highlevel.tasks.UserAuthorityGetVersionTask;
import be.iminds.ilabt.jfed.lowlevel.ApiInfo;
import be.iminds.ilabt.jfed.lowlevel.GeniUserProvider;
import be.iminds.ilabt.jfed.lowlevel.api_wrapper.UserAndSliceApiWrapper;
import be.iminds.ilabt.jfed.lowlevel.authority.AuthorityFinder;
import be.iminds.ilabt.jfed.rspec.model.ModelRspecType;
import be.iminds.ilabt.jfed.rspec.model.javafx_impl.FXModelRspec;
import be.iminds.ilabt.jfed.rspec.rspec_source.RequestRspecSource;
import be.iminds.ilabt.jfed.ui.javafx.FXMLUtil;
import be.iminds.ilabt.jfed.ui.javafx.GlyphUtils;
import be.iminds.ilabt.jfed.ui.javafx.dialogs.JFDialogs;
import be.iminds.ilabt.jfed.util.ExecuteOnNotNull;
import be.iminds.ilabt.jfed.util.GeniUrn;
import com.google.common.eventbus.EventBus;
import javafx.application.HostServices;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.GridPane;
import javafx.scene.paint.Color;
import javafx.stage.Window;
import org.controlsfx.control.textfield.TextFields;
import org.controlsfx.glyphfont.FontAwesome;
import org.controlsfx.validation.ValidationMessage;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
import org.controlsfx.validation.decoration.GraphicValidationDecoration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Provider;
import java.net.URL;
import java.time.*;
import java.util.*;

/**
 * User: twalcari
 * Date: 2/24/14
 * Time: 4:03 PM
 */
//TODO implement includeSshKeys support
public class CreateReservationDialog extends GridPane {
    private static final Logger LOG = LoggerFactory.getLogger(CreateReservationDialog.class);

    private static final Duration MINIMUM_SLICE_DURATION = Duration.ofMinutes(1);
    private final RequestRspecSource requestRspecSource;
    private final HighLevelTaskFactory hltf;
    private final UserAuthorityGetVersionTask getVersionTask;
    private final Map<String, Slice> slices = new HashMap<>();
    private final GeniUserProvider geniUserProvider;
    private final SfaModel sfaModel;

    private final EventBus eventBus;
    private final Provider<HostServices> hostServicesProvider;
    private final AuthorityFinder authorityFinder;


    @FXML
    protected TextField nameTextField;
    @FXML
    protected Label subAuthLabel;
    @FXML
    protected CheckBox subAuthCheckBox;
    @FXML
    protected ProgressBar subAuthFetchProgressBar;
    @FXML
    protected ComboBox<String> subAuthComboBox;
    @FXML
    protected Button refreshSubAuthButton;
    @FXML
    protected StartEndDurationPane startEndDurationPane;
    @FXML
    protected Button showCalendarButton;

    private final Dialog dialog;
    private final ValidationSupport validationSupport;

    public CreateReservationDialog(Window parentWindow, RequestRspecSource requestRspecSource,
                                   HighLevelTaskFactory hltf, GeniUserProvider geniUserProvider,
                                   SfaModel sfaModel,
                                   EventBus eventBus,
                                   Provider<HostServices> hostServicesProvider, AuthorityFinder authorityFinder) {

        this.requestRspecSource = requestRspecSource;
        this.hltf = hltf;
        this.geniUserProvider = geniUserProvider;
        this.sfaModel = sfaModel;
        this.eventBus = eventBus;
        this.hostServicesProvider = hostServicesProvider;
        this.authorityFinder = authorityFinder;

        //load the dialog contents from an FXML
        FXMLUtil.injectFXML(this);

        validationSupport = new ValidationSupport();
        validationSupport.registerValidator(nameTextField, false, sliceNameValidator);
        validationSupport.<String>registerValidator(subAuthComboBox,
                Validator.createPredicateValidator(
                        s -> !subAuthCheckBox.isSelected() || !subAuthComboBox.getSelectionModel().isEmpty(),
                        "Please choose a project in which this experiment should be run!"));
        TextFields.bindAutoCompletion(nameTextField, getActiveSliceNames());

        validationSupport.registerValidator(startEndDurationPane.getEndTimeTextField(), false, (control, o) -> {
            ValidationResult validationResult = new ValidationResult();
            validationResult.addErrorIf(startEndDurationPane.getEndTimeTextField(), "Reservation end time must come after start time!",
                    startEndDurationPane.getEndDateTime().isBefore(startEndDurationPane.getStartDateTime()));
            return validationResult;
        });

        validationSupport.registerValidator(startEndDurationPane.getEndDatePicker(), false, (control, o) -> {
            ValidationResult validationResult = new ValidationResult();
            validationResult.addErrorIf(startEndDurationPane.getEndDatePicker(), "Reservation end time must be in the future!",
                    startEndDurationPane.getEndDateTime().isBefore(LocalDateTime.now()));
            return validationResult;
        });

        validationSupport.setValidationDecorator(new GraphicValidationDecoration());

        subAuthCheckBox.selectedProperty().addListener(observable -> {
            if (subAuthCheckBox.isSelected())
                enableSubAuthoritiesList();
            else
                disableSubAuthoritiesList();
        });
        initializeSubAuthorities();

        final InvalidationListener sliceNameInvalidationListener = observable -> {
            String sliceName = nameTextField.getText();

            if (slices.containsKey(sliceName)) {
                showFixedSliceDetails(slices.get(sliceName));
            } else {
                enableSliceDetailsEditing();
            }
        };

        nameTextField.textProperty().addListener(sliceNameInvalidationListener);


        dialog = new Dialog<>();
        dialog.initOwner(parentWindow);
        dialog.setTitle("Create an experiment reservation");
        dialog.setGraphic(GlyphUtils.createDialogGlyph(FontAwesome.Glyph.CALENDAR, Color.ORANGERED));
        dialog.setHeaderText("Enter experiment reservation details");
        dialog.getDialogPane().setContent(UIUtils.wrapWithDecorationPane(this));

        ButtonType startExperimentButtonType = new ButtonType("Create Reservation", ButtonBar.ButtonData.OK_DONE);
        dialog.getDialogPane().getButtonTypes().setAll(startExperimentButtonType, ButtonType.CANCEL);

        Button startButton = (Button) dialog.getDialogPane().lookupButton(startExperimentButtonType);
        startButton.disableProperty().bind(validationSupport.invalidProperty());
        startButton.addEventFilter(ActionEvent.ACTION, event -> {
            checkAndStartExperiment();
            event.consume();
        });

        nameTextField.setOnAction(ae -> checkAndStartExperiment());
        //start a getVersionTask if necessary, this will allow us to check if we need to overwrite existing recovery information or not

        getVersionTask = this.hltf.getUserAuthorityGetVersionTask();
        if (!getVersionTask.isKnown()) {
            LOG.debug("CreateSliceDialog: getVersion not known. Requesting.");
            this.hltf.submitTask(getVersionTask);
        }


        List<GeniUrn> cmUrns = requestRspecSource.getAllComponentManagerUrns();

        List<URL> reservationPages = new ArrayList<>();
        for (GeniUrn cmUrn : cmUrns) {
            Server auth =
                    this.authorityFinder.findByUrn(cmUrn, AuthorityFinder.Purpose.CREATE_SLIVER);

            final URL reservationPage = ApiInfo.findUrl(auth, ApiInfo.ApiName.FED4FIRE_RESERVATION_CALENDER_PAGE, 1);
            if (reservationPage != null) {
                reservationPages.add(reservationPage);
            }
        }

        showCalendarButton.setDisable(reservationPages.isEmpty());
        showCalendarButton.setOnAction(event -> reservationPages.forEach(page -> this.hostServicesProvider.get().showDocument(page.toExternalForm())));


        new ExecuteOnNotNull<Scene>(sceneProperty()) {
            @Override
            public void run() {
                nameTextField.requestFocus();
            }
        };
    }

    private ObservableList<String> getActiveSliceNames() {
        ObservableList<String> sliceNames = FXCollections.observableArrayList();
        for (Slice slice : sfaModel.getSlices()) {
            //only add if slice expiration is unknown, or if it has more than 1 minute left before it expires

            LOG.debug("CreateSliceDialog slice list checks " + slice.getUrnString() + " exp=" + slice.getExpirationDate());

            if (slice.getExpirationDate() == null ||
                    Duration.between(Instant.now(), slice.getExpirationDate()).compareTo(MINIMUM_SLICE_DURATION) > 0) {

                sliceNames.add(slice.getName());
                slices.put(slice.getName(), slice);
            } else
                LOG.debug("CreateSliceDialog skips slice " + slice.getUrnString() + " because duration is less than " +
                        MINIMUM_SLICE_DURATION + "   (duration=" +
                        Duration.between(Instant.now(), slice.getExpirationDate()) + ")");
        }
        return sliceNames;
    }

    private void showFixedSliceDetails(Slice slice) {
        if (slice.getSubAuthorityName() != null) {
            subAuthCheckBox.setSelected(true);
            subAuthComboBox.setValue(slice.getSubAuthorityName());
        } else {
            subAuthCheckBox.setSelected(false);
            subAuthComboBox.setValue(null);
        }

        subAuthCheckBox.setDisable(true);
        subAuthComboBox.setDisable(true);
    }

    private void enableSliceDetailsEditing() {
        subAuthCheckBox.setDisable(false);
        subAuthComboBox.setDisable(false);
    }


    /**
     * The length of a slice-name is limited because of the maximum hostname-length under Linux,
     * which must be less than 63 characters.
     * <p/>
     * Emulab-based testbeds use the following convention to generate hostnames:
     * <nodename>.<projectname>.<user-authority-without-subauthority>.<testbed-authority>.
     * <p/>
     * For example:
     * Emulab.net-node with a GENI-account: node0.project1.ch-geni-net.emulab.net
     */
    private int getMaximumSliceNameLength() {
        if (requestRspecSource.isXmlBased())
            return 8; //fallback in case we cannot parse the Rspec


        int nodeNameAndTestBedAuthorityLength =
                ((FXModelRspec) requestRspecSource.getModelRspec(ModelRspecType.FX)).getNodes()
                        .stream()
                        .mapToInt(rspecNode -> {
                            int totalLength = 0;
                            if (rspecNode.getClientId() != null)
                                totalLength += rspecNode.getClientId().length();
                            if (rspecNode.getComponentManagerId() != null)
                                totalLength += rspecNode.getComponentManagerId().getEncodedTopLevelAuthority().length();
                            return totalLength;
                        })
                        .max().orElse(15);//compute length of first and last part of slice-name.


        int maxLength = 63 - 3/* number of dots in the hostname */;
        maxLength -= nodeNameAndTestBedAuthorityLength;
        maxLength -= geniUserProvider.getLoggedInGeniUser()
                .getUserAuthorityServer().getDefaultComponentManagerAsGeniUrn().getEncodedTopLevelAuthority().length();


        return maxLength;
    }

    private final Validator<String> sliceNameValidator = (control, sliceName) -> {
        ValidationResult result = new ValidationResult();
        if (sliceName == null) {
            result.add(ValidationMessage.error(control, "Experiment name cannot be empty!"));
            return result;
        }

        if (sliceName.length() == 0) {
            result.add(ValidationMessage.error(control, "Experiment name cannot be empty!"));
        } else if (sliceName.length() > getMaximumSliceNameLength()) {
            result.add(ValidationMessage.error(control,
                    String.format("Experiment name cannot be longer than %d characters", getMaximumSliceNameLength())));
        }

        for (char c : sliceName.toCharArray()) {
            boolean validChar = (c >= 'a' && c <= 'z')
                    || (c >= 'A' && c <= 'Z')
                    || (c >= '0' && c <= '9');

            if (!validChar) {
                result.add(ValidationMessage.error(control,
                        String.format("Experiment name may not contain the character '%c'." +
                                " Only characters [a-zA-Z0-9] are allowed", c)));
            }
        }

        return result;
    };

    private void initializeSubAuthorities() {
        //initialize subauthorities-list
        if (sfaModel.getSubAuthoritySupport()
                == UserAndSliceApiWrapper.SubAuthoritySupport.SUB_AUTHORITY_FORBIDDEN) {
            subAuthCheckBox.setSelected(false);
            subAuthCheckBox.setDisable(true);

            subAuthFetchProgressBar.setVisible(false);
            subAuthComboBox.setDisable(true);
            subAuthComboBox.setItems(FXCollections.observableArrayList("Not supported"));
            refreshSubAuthButton.setDisable(true);
        } else {

            subAuthCheckBox.setSelected(true);
            if (sfaModel.getSubAuthoritySupport()
                    == UserAndSliceApiWrapper.SubAuthoritySupport.SUB_AUTHORITY_MANDATORY) {
                subAuthCheckBox.setDisable(true);
            } else {
                subAuthCheckBox.setDisable(false);
                //default: use a sub authority (because it is generally nicer)
            }
            enableSubAuthoritiesList();
        }
    }

    private void showSubAuthoritiesList() {
        //this method should not be executed when SubAuthority-checkbox is not selected
        if (!subAuthCheckBox.isSelected()) {
            LOG.warn("unexpected: showSubAuthoritiesList() called while subAuthCheckBox.isSelected()=" + !subAuthCheckBox.isSelected());
            return;
        }

        List<String> subAuthNames = new ArrayList<>(sfaModel.getLoggedInUserSubAuthorityNames());
        Collections.sort(subAuthNames);
        subAuthComboBox.setItems(FXCollections.observableList(subAuthNames));

        subAuthComboBox.setDisable(false);

        if (!subAuthComboBox.getItems().isEmpty())
            subAuthComboBox.getSelectionModel().selectFirst();

        if (subAuthComboBox.getItems().isEmpty() && sfaModel.getSubAuthoritySupport() == UserAndSliceApiWrapper.SubAuthoritySupport.SUB_AUTHORITY_MANDATORY)
            JFDialogs.create()
                    .owner(subAuthComboBox)
                    .message("Selecting a project is mandatory for your experiment, but jFed did not receive any candidates." +
                            " Please contact your support team.")
                    .masthead("No projects available")
                    .showError();

        subAuthFetchProgressBar.setVisible(false);

        refreshSubAuthButton.setDisable(false);
    }

    private void disableSubAuthoritiesList() {
        subAuthLabel.setDisable(true);
        subAuthComboBox.getSelectionModel().clearSelection();
        subAuthComboBox.getItems().clear();
        subAuthComboBox.setDisable(true);

        subAuthFetchProgressBar.setVisible(false);

        refreshSubAuthButton.setDisable(true);
    }

    private void enableSubAuthoritiesList() {
        subAuthLabel.setDisable(false);

        if (shouldRefreshSubAuthorities()) {
            refreshSubAuthoritiesList();
        } else {
            showSubAuthoritiesList();
        }
    }

    private boolean shouldRefreshSubAuthorities() {
        ListSubAuthoritiesTask listSubAuthoritiesTask =
                hltf.listSubAuthorities();
        return listSubAuthoritiesTask.newExecutionNeeded();
    }

    @FXML
    private void refreshSubAuthoritiesList() {
        subAuthComboBox.setItems(FXCollections.emptyObservableList());
        subAuthComboBox.setDisable(true);

        subAuthFetchProgressBar.setVisible(true);

        refreshSubAuthButton.setDisable(true);

        //initialize task to fetch sub-authority names
        ListSubAuthoritiesTask listSubAuthoritiesTask = hltf.listSubAuthorities();

        listSubAuthoritiesTask.addCallback((task, taskExecution, state) -> Platform.runLater(this::showSubAuthoritiesList));

        hltf.submitTask(listSubAuthoritiesTask);
    }


    public void showDialog() {
        dialog.show();
    }

    /**
     * This method uses the inputs from the dialog to start the experiment.
     * These inputs should be validated before calling this function!
     */
    private void createReservation() {
        dialog.hide();

        String sliceName = nameTextField.getText();
        String subAuthName = subAuthCheckBox.isSelected() ? subAuthComboBox.getValue() : null;
        Instant startInstant =
                ZonedDateTime.of(startEndDurationPane.getStartDateTime(), ZoneId.systemDefault()).toInstant();
        Instant endInstant =
                ZonedDateTime.of(startEndDurationPane.getEndDateTime(), ZoneId.systemDefault()).toInstant();


        eventBus.post(new StartExperimentEvent(sliceName, subAuthName, startInstant, endInstant, requestRspecSource, false, false));
    }

    public void checkAndStartExperiment() {
        if (validationSupport.isInvalid()) {
            LOG.warn("Cannot start because validationSupport is currently invalid");
            return;
        }

        LOG.info("Trying to make a reservation with sliceName '{}' from {} until {}", nameTextField.getText(),
                startEndDurationPane.getStartDateTime(), startEndDurationPane.getEndDateTime());

        String sliceName = nameTextField.getText();

        //is it a new slice?
        if (slices.containsKey(sliceName)) {
            Slice slice = slices.get(sliceName);

            //a taskexecution for this task has already been requested in the constructor. It should finish soon!
            while (!getVersionTask.isKnown()) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException ignored) {
                }
            }

            //check if we are overwriting an existing backup, and warn the user
            if (!getVersionTask.isSupportSliverRegistration() && ExperimentRestoreInformation.exists(slice.getUrn())) {

                ExperimentRestoreInformation sri = new ExperimentRestoreInformation(slice.getUrn());
                if (sri.getExpirationTime() != null && sri.getExpirationTime().isAfter(Instant.now())) {

                    if (JFDialogs.create().message(String.format("There currently exists a recovery file for slice %s. " +
                            "By re-using this slice, this backup will be overwritten. " +
                            "Are you sure you want to continue?", sliceName))
                            .masthead("Slice Recovery Data")
                            .title("Slice Recovery Data")
                            .buttonTypes(ButtonType.YES, ButtonType.NO)
                            .showConfirm().filter(r -> r != ButtonType.YES).isPresent())
                        return;
                }
            }
        }
        createReservation();
    }

}
