package be.iminds.ilabt.jfed.experimenter_gui;

import be.iminds.ilabt.jfed.ApplicationParametersModule;
import be.iminds.ilabt.jfed.BaseGuiModule;
import be.iminds.ilabt.jfed.UncaughtGuiExceptionLogger;
import be.iminds.ilabt.jfed.experiment.Experiment;
import be.iminds.ilabt.jfed.experimenter_gui.ext_requests.EnvironmentParameterHandler;
import be.iminds.ilabt.jfed.experimenter_gui.ext_requests.ExternalUserRequestHandler;
import be.iminds.ilabt.jfed.experimenter_gui.ext_requests.FirstLoginParametersHandler;
import be.iminds.ilabt.jfed.experimenter_gui.ext_requests.JFedEnvironment;
import be.iminds.ilabt.jfed.experimenter_gui.login.UserLoginDirector;
import be.iminds.ilabt.jfed.experimenter_gui.login.UserLoginDirectorFactory;
import be.iminds.ilabt.jfed.experimenter_gui.preferences.PreferencesDialogFactory;
import be.iminds.ilabt.jfed.experimenter_gui.ui.AcceleratorService;
import be.iminds.ilabt.jfed.experimenter_gui.ui.PasswordInputDialog;
import be.iminds.ilabt.jfed.experimenter_gui.ui.UncaughtGuiExceptionToBugReport;
import be.iminds.ilabt.jfed.experimenter_gui.util.DateTimeUtils;
import be.iminds.ilabt.jfed.experimenter_gui.util.MacOpenURIHandler;
import be.iminds.ilabt.jfed.experimenter_gui.util.URIUtils;
import be.iminds.ilabt.jfed.fedmon.webapi.service.json.TriggeredMessage;
import be.iminds.ilabt.jfed.highlevel.HighLevelModule;
import be.iminds.ilabt.jfed.highlevel.LowLevelModule;
import be.iminds.ilabt.jfed.highlevel.UserLoginModelModule;
import be.iminds.ilabt.jfed.highlevel.controller.TaskThread;
import be.iminds.ilabt.jfed.highlevel.model.Sliver;
import be.iminds.ilabt.jfed.logging.JFedLoggerManager;
import be.iminds.ilabt.jfed.lowlevel.GeniUser;
import be.iminds.ilabt.jfed.lowlevel.GeniUserProvider;
import be.iminds.ilabt.jfed.lowlevel.ssh_key_info.PuTTYFilesKeyInfo;
import be.iminds.ilabt.jfed.lowlevel.ssh_key_info.SshKeyInfo;
import be.iminds.ilabt.jfed.lowlevel.ssh_key_info.SshKeyInfoFactory;
import be.iminds.ilabt.jfed.lowlevel.user.UserLoginInfo;
import be.iminds.ilabt.jfed.preferences.*;
import be.iminds.ilabt.jfed.ssh_terminal_tool.putty.PageantHelper;
import be.iminds.ilabt.jfed.ssh_terminal_tool.putty.SshKeyInfoUnlocker;
import be.iminds.ilabt.jfed.ssh_terminal_tool.unix.SshAgentHelper;
import be.iminds.ilabt.jfed.ui.javafx.GlyphUtils;
import be.iminds.ilabt.jfed.ui.javafx.dialogs.JFDialogs;
import be.iminds.ilabt.jfed.ui.javafx.util.ExtraInfoCallBackGUIs;
import be.iminds.ilabt.jfed.ui.javafx.util.StageUtils;
import be.iminds.ilabt.jfed.util.FXPlatformUtil;
import be.iminds.ilabt.jfed.util.GuiceContext;
import be.iminds.ilabt.jfed.util.JFedUtils;
import be.iminds.ilabt.jfed.util.OSDetector;
import be.iminds.jfed.gts_highlevel.GtsModule;
import com.google.inject.Module;
import javafx.application.Application;
import javafx.embed.swing.JFXPanel;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import org.controlsfx.glyphfont.FontAwesome;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.glyphfont.GlyphFont;
import org.controlsfx.glyphfont.GlyphFontRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import javax.inject.Provider;
import java.io.IOException;
import java.io.InputStream;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;

/**
 * User: twalcari
 * Date: 10/31/13
 * Time: 6:17 PM
 */
public class Launch extends Application implements Provider<Stage>, GeniUserProvider, SshKeyInfoUnlocker {
    private static final int MINIMUM_JAVA_UPDATE_VERSION = 60;

    private static final Logger LOG;

    static {
        LOG = LoggerFactory.getLogger(Launch.class);

        /* solution for MAC OS HeadlessException bug. See https://ibcn-jira.intec.ugent.be/browse/FEDIBBTDEV-239 */
        System.setProperty("java.awt.headless", "false");

        //Load "font awesome" now. Otherwise, it will be fetched from a CDN later.
        loadFontAwsome();
    }


    private final GuiceContext context = new GuiceContext(this, () -> {
        List<Module> modules = new ArrayList<>();

        Map<String, String> parameters = getAllParameters();
        modules.add(new ApplicationParametersModule(parameters));

        modules.add(new LowLevelModule());
        modules.add(new HighLevelModule());
        modules.add(new UserLoginModelModule(true));
        modules.add(new ExperimenterModule(this));
        modules.add(new BaseGuiModule());
        modules.add(new GtsModule());

        return modules;
    });
    @Inject
    private JFedLoggerManager loggerManager;
    @Inject
    private JFedGuiPreferences jFedPreferences;
    @Inject
    private JFedPreferencesApplier jFedPreferencesApplier;
    @Inject
    private TaskThread tt;
    @Inject
    private FXMLLoader loader;
    @Inject
    private PageantHelper pageantHelper;
    @Inject
    private PreferencesDialogFactory preferencesDialogFactory;
    private Stage primaryStage;
    @Inject
    private AcceleratorService acceleratorService;
    @Inject
    private FirstLoginParametersHandler firstLoginParametersHandler;
    @Inject
    private ExternalUserRequestHandler externalUserRequestHandler;
    @Inject
    private EnvironmentParameterHandler environmentParameterHandler;
    @Inject
    private Provider<MacOpenURIHandler> macOpenURIHandlerProvider;
    @Inject
    private JFedEnvironment environment;
    @Inject
    private UserLoginDirectorFactory userLoginDirectorFactory;
    @Inject
    private UncaughtGuiExceptionToBugReport uncaughtGuiExceptionToBugReport;


    //keep reference to prevent it from being Garbage-collected
    @SuppressWarnings({"unused", "FieldCanBeLocal"})
    private MacOpenURIHandler macOpenURIHandler = null;
    private ExperimenterGUI experimenterGUI;
    private Scene experimenterGuiScene;
    private GeniUser loggedInGeniUser;

    public static void main(String[] args) {
        launch(args);
    }

    public static void loadFontAwsome() {
        //initialize JavaFX-toolkit
        new JFXPanel();

        //https://bitbucket.org/controlsfx/controlsfx/issues/123/fontawesome
        LOG.trace("Pre-loading FontAwesome");
        //hint: make sure maven resource filtering is not currupting the ttf!
        InputStream is = Launch.class.getResourceAsStream("fontawesome-webfont.ttf");
        if (is != null) {
            GlyphFont fa = new FontAwesome(is);
            GlyphFontRegistry.register(fa);

            //the load is lazy. So the stream would normally only be read when needed later.
            //So below, we ensure the font is loaded now, by using it.
            GlyphFont fontAwesome = GlyphFontRegistry.font("FontAwesome");
            Glyph g = fontAwesome.create(FontAwesome.Glyph.EYE).color(Color.BLUE);
//            LOG.debug("Loaded eye bounds: " + g.getBoundsInLocal());
            try {
                is.close();
            } catch (IOException e) {
                LOG.debug("ignoring is.close() error", e);
            }
        } else {
            LOG.error("Could not initialise FontAwesome");
        }
    }

    @Override
    public void init() {
        //catch all exceptions which aren't caught anywhere else
        Thread.setDefaultUncaughtExceptionHandler(UncaughtGuiExceptionLogger.getInstance(null));
        context.init();

        loggerManager.updateLoggers();

        //register Mac URI handler when on Mac
        if (OSDetector.findOS() == OSDetector.OS.MAC) {
            macOpenURIHandler = macOpenURIHandlerProvider.get();
        }

        //process startup parameters on all platforms
        externalUserRequestHandler.handleParameters();
        environmentParameterHandler.handleParameters();

    }

    @Override
    public void start(final Stage stage) {
        this.primaryStage = stage;

        if (!isRecentJavaVersion()) {
            JFDialogs.create()
                    .message("You are currently using a Java-version with known bugs, which can result in unstable behavior of jFed. Please update your Java installation to the latest version.")
                    .title("Outdated Java installation detected")
                    .masthead("Outdated Java installation detected")
                    .showWarning();
        }

        try {
            //check for filesystem access
            if (JFedUtils.getUserDataDirectory() == null) {

                JFDialogs.create()
                        //.owner(stage)
                        .message("Please verify your security settings and try again.\n\n" +
                                "If you are trying to run jFed through Safari on OS X, you need to change your Safari security settings.\n" +
                                "(cfr. http://doc.fed4fire.eu/firstexperiment.html#start-up-jfed)")
                        .masthead("jFed was unable to access the filesystem.")
                        .title("Fatal Error").showError();

                exit();
                return;
            }


            ExtraInfoCallBackGUIs.prepareExtraInfoCallback(stage);

            UserLoginInfo userLoginInfo = firstLoginParametersHandler.handleParameters();

            UserLoginDirector userLoginDirector =
                    userLoginDirectorFactory.createUserLoginDirector(environment.getLoginUrl(), userLoginInfo);

            UserLoginDirector.UserLoginResult result = userLoginDirector.showAndWait();

            if (result != null && result.getLoggedInUser() != null) {
                loggedInGeniUser = result.getLoggedInUser();

                boolean continueLogin = true;
                if (!loggedInGeniUser.getClientCertificateChain().isEmpty()) {
                    final X509Certificate cert = loggedInGeniUser.getClientCertificateChain().get(0);

                    try {
                        cert.checkValidity();
                    } catch (CertificateExpiredException ex2) {
                        continueLogin = showCertificateExpiredDialog(cert);
                    } catch (CertificateNotYetValidException ex2) {
                        continueLogin = showCertificateInvalidDialog(cert);
                    }
                }

                if (continueLogin)
                    onSuccessfulLogin(stage);
                else
                    exit();
            } else {
                exit();
            }
        } catch (Throwable ex) {
            //catch Throwable is allowed here, as we throw it afterwards
            LOG.error("Fatal error in jFed Experimenter Main Thread: " + ex.getMessage(), ex);
            JFDialogs.create()
                    //.owner(stage)
                    .message("jFed experimenter will terminate.")
                    .masthead("A fatal error occurred!")
                    .title("Fatal Error")
                    .showException(ex);

            throw new RuntimeException(ex);
        }
    }

    private boolean showCertificateInvalidDialog(X509Certificate cert) {
        boolean continueLogin;Alert alert = new Alert(Alert.AlertType.WARNING);
        alert.setTitle("Login certificate invalid");
        alert.setHeaderText("Login certificate invalid");
        final LocalDateTime startDateTime = LocalDateTime.ofInstant(cert.getNotBefore().toInstant(), ZoneId.systemDefault());
        alert.setContentText(String.format("Your login certificate is invalid, as its start date is %s. " +
                        "Please contact your authority for a valid certificate.",
                DateTimeUtils.DATE_HOUR_MINUTES_FORMATTER.format(startDateTime)));

        ButtonType continueButton =
                new ButtonType("Continue anyway", ButtonBar.ButtonData.CANCEL_CLOSE);
        ButtonType exitButton = new ButtonType("Exit", ButtonBar.ButtonData.OK_DONE);

        alert.getButtonTypes().setAll(exitButton, continueButton);

        continueLogin = alert.showAndWait().filter(button -> button == continueButton).isPresent();
        return continueLogin;
    }

    private boolean showCertificateExpiredDialog(X509Certificate cert) {
        boolean continueLogin;Alert alert = new Alert(Alert.AlertType.WARNING);
        alert.setTitle("Login certificate expired");
        alert.setHeaderText("Login certificate expired");
        final LocalDateTime endDateTime = LocalDateTime.ofInstant(cert.getNotAfter().toInstant(), ZoneId.systemDefault());
        alert.setContentText(String.format("Your login certificate has expired on %s. " +
                        "Please contact your authority to renew your certificate.",
                DateTimeUtils.DATE_HOUR_MINUTES_FORMATTER.format(endDateTime)));

        ButtonType continueButton =
                new ButtonType("Continue anyway", ButtonBar.ButtonData.CANCEL_CLOSE);
        ButtonType exitButton = new ButtonType("Exit", ButtonBar.ButtonData.OK_DONE);

        alert.getButtonTypes().setAll(exitButton, continueButton);

        continueLogin = alert.showAndWait().filter(button -> button == continueButton).isPresent();
        return continueLogin;
    }

    public Map<String, String> getAllParameters() {

        Map<String, String> parameters = new HashMap<>();

        parameters.putAll(getParameters().getNamed());

        for (String unnamedParam : getParameters().getUnnamed()) {
            Map<String, String> uriResult = URIUtils.processJFedUri(unnamedParam);

            if (uriResult != null) {
                parameters.putAll(uriResult);
            }
        }
        return parameters;

    }

    private void onSuccessfulLogin(Stage stage) {
        try {
            //show information dialog about public/private keys on first run of jFed
            if (jFedPreferences.getInt(GuiPreferenceKey.PREF_FIRSTRUN_VERSION) < JFedPreferences.CURRENT_FIRSTRUN_VERSION) {

                if (OSDetector.findOS() == OSDetector.OS.WIN) {


                    JFDialogs.create()
                            // .owner(stage)
                            .message("The default preferences should be okay. If you have trouble running experiments or connecting to nodes, consider to turn on 'Proxy' in the jFed Proxy preferences.\n\n" +
                                    "Next you will see the preferences window, you can always revisit this by clicking the preferences button.\n\n" +
                                    "Please verify that jFed has found the PuTTY SSH software (you should see 4 green checkmarks). If not install first PuTTY (full suite) or point jFed to the right PuTTY install directory.")
                            .masthead("Initial configuration")
                            .title("Welcome to the jFed Experimenter Tool!")
                            .showInformation();


                } else {

                    JFDialogs.create()
                            //  .owner(stage)
                            .message("The default preferences should be okay. If you have trouble running experiments or connecting to nodes, consider to turn on 'Proxy' in the jFed Proxy preferences.\n\n" +
                                    "Next you will see the preferences window, you can always revisit this by clicking the preferences button.")
                            .masthead("Initial configuration")
                            .title("Welcome to the jFed Experimenter Tool!")
                            .showInformation();

                }

                preferencesDialogFactory.showDialog();

                jFedPreferences.setInt(GuiPreferenceKey.PREF_FIRSTRUN_VERSION, JFedPreferences.CURRENT_FIRSTRUN_VERSION);
            }

            jFedPreferencesApplier.applyPreferences();
            loadExperimenterGui();

            //catch all exceptions which aren't caught anywhere else
            uncaughtGuiExceptionToBugReport.setStage(stage);
            Thread.setDefaultUncaughtExceptionHandler(uncaughtGuiExceptionToBugReport);

            showExperimenterGui(stage);

            experimenterGUI.getTriggeredMessageHandler().onEvent(TriggeredMessage.GuiTrigger.LOGIN);
        } catch (Throwable ex) {
            //catch Throwable is allowed here, as we throw it afterwards
            LOG.error("Fatal error in jFed Experimenter Main Thread: " + ex.getMessage(), ex);
            JFDialogs.create()
                    // .owner(stage)
                    .message("jFed experimenter will terminate.")
                    .masthead("A fatal error occurred!")
                    .title("Fatal Error")
                    .showException(ex);

            throw ex;
        }
    }

    private void registerKeysWithPageant() {
        if (OSDetector.findOS() != OSDetector.OS.WIN)
            return;

        if (jFedPreferences.getBoolean(GuiPreferenceKey.PREF_SSHAGENT_USE, false)) {
            Set<PuTTYFilesKeyInfo> keys = new HashSet<>();

            PuTTYFilesKeyInfo sshKeyInfo = jFedPreferences.getCustomSshKeyInfo();
            if (sshKeyInfo == null && isUserLoggedIn()) {
                GeniUser geniUser = getLoggedInGeniUser();
                sshKeyInfo = SshKeyInfoFactory.createGeniUserSshKeyInfo(geniUser);
            }

            if (sshKeyInfo != null && !sshKeyInfo.hasUnencryptedPrivateKey()) {


            }
            keys.add(sshKeyInfo);

            if (!jFedPreferences.getString(CorePreferenceKey.PREF_SSHPROXY_USE_FOR_SSH).equalsIgnoreCase("NEVER") &&
                    isUserLoggedIn()) {
                final GeniUser geniUser = getLoggedInGeniUser();
                PuTTYFilesKeyInfo proxySshKeyInfo = SshKeyInfoFactory.createGeniUserSshKeyInfo(geniUser);
                keys.add(proxySshKeyInfo);
            }

            pageantHelper.registerKeys(keys, this);
        }
    }

    private boolean isExperimentActive(Experiment experiment) {
        if (experiment.getSlice() == null)
            return false;

        for (Sliver sliver : experiment.getSlice().getSlivers()) {
            switch (sliver.getStatus().getGlobalStatus()) {
                case READY:
                case UNKNOWN:
                case CHANGING:
                    return true;
            }
        }
        return false;
    }

    private Scene loadExperimenterGui() {
        LOG.debug("Loading ExperimenterGUI");
        try {
            loader.setLocation(getClass().getResource("ExperimenterGUI.fxml"));
            experimenterGuiScene = new Scene(loader.load());
            experimenterGUI = loader.getController();
            acceleratorService.registerOnScene(experimenterGuiScene);

            return experimenterGuiScene;
        } catch (IOException ex) {
            LOG.error("Error while loading ExperimenterGUI", ex);
            throw new RuntimeException("Error while loading ExperimenterGUI", ex);
        }
    }

    public void showExperimenterGui(Stage stage) {
        LOG.debug("Showing ExperimenterGUI");
        stage.setScene(experimenterGuiScene);
        stage.setMinHeight(600);
        stage.setMinWidth(800);

        stage.setHeight(800);
        stage.setWidth(1200);

        stage.setTitle("jFed Experimenter Toolkit");
        StageUtils.setJFedStageIcons(stage);

        stage.setOnCloseRequest(
                event -> {
                    if (!experimenterGUI.onStageCloseRequest()) {
                        event.consume();
                    } else {
                        exit();
                    }
                });

        //Called just after the Window is shown.
        stage.setOnShown(windowEvent -> {
            registerKeysWithPageant();
            externalUserRequestHandler.activate();
        });

        stage.show();
    }

    public void exit() {
        LOG.info("Close was requested. Trying to terminate");

        if (experimenterGUI != null && experimenterGUI.getTriggeredMessageHandler() != null)
            experimenterGUI.getTriggeredMessageHandler().onEvent(TriggeredMessage.GuiTrigger.EXIT);

        tt.requestStop();

        //stop any running ssh-agent
        SshAgentHelper.stopSshAgent();

        System.exit(0);
    }

    @Override
    public Stage get() {
        return primaryStage;
    }

    public boolean isRecentJavaVersion() {
        String versionString = System.getProperty("java.version");

        if (versionString.startsWith("1.7")) {
            return false;
        } else if (versionString.startsWith("1.8")) {
            final int separationCharLocation = versionString.indexOf("_");
            if (separationCharLocation < 0) {
                LOG.warn("Unable to detect update version of Java in version-string {}", versionString);
                return true;
            }

            try {
                int updateVersion = Integer.parseInt(versionString.substring(separationCharLocation + 1));
                return updateVersion > MINIMUM_JAVA_UPDATE_VERSION;
            } catch (NumberFormatException ex) {
                LOG.warn("Could not determine Java update version in version-string {}", versionString);
                return true;
            }
        } else {
            LOG.warn("Could not determine major Java version in version-string {}", versionString);
            return true; //probably a future version: Java9 or higher
        }
    }


    @Override
    public GeniUser getLoggedInGeniUser() {
        return loggedInGeniUser;
    }

    @Override
    public boolean isUserLoggedIn() {
        return loggedInGeniUser != null;
    }

    @Override
    public boolean unlock(SshKeyInfo sshKeyInfo) {
        return FXPlatformUtil.runAndWait(() -> {
            boolean success = false;
            boolean cancelled = false;
            while (!success && !cancelled) {

                PasswordInputDialog pid = new PasswordInputDialog();
                pid.setTitle("SSH Agent");
                pid.setGraphic(GlyphUtils.createDialogGlyph(FontAwesome.Glyph.KEY, Color.GOLDENROD));
                pid.setHeaderText("SSH Agent");
                pid.setContentText("Enter passphrase for key '" + sshKeyInfo.getComment() + "'");

                Optional<String> result = pid.showAndWait();

                if (result.isPresent()) {
                    success = sshKeyInfo.unlock(result.get().toCharArray());
                } else {
                    cancelled = true;
                }

            }
            return success;
        });
    }
}
