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

import be.iminds.ilabt.jfed.connectivity_tester.ConnectivityTest;
import be.iminds.ilabt.jfed.connectivity_tester.SafeConnectivityTest;
import be.iminds.ilabt.jfed.connectivity_tester.SslHostAndPortTest;
import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Server;
import be.iminds.ilabt.jfed.lowlevel.ApiInfo;
import be.iminds.ilabt.jfed.lowlevel.GeniUser;
import be.iminds.ilabt.jfed.lowlevel.GeniUserProvider;
import be.iminds.ilabt.jfed.lowlevel.TestbedInfoSource;
import be.iminds.ilabt.jfed.lowlevel.connection.JFedConnection;
import be.iminds.ilabt.jfed.lowlevel.ssh_key_info.SshKeyInfoFactory;
import be.iminds.ilabt.jfed.preferences.CorePreferenceKey;
import be.iminds.ilabt.jfed.preferences.JFedGuiPreferences;
import be.iminds.ilabt.jfed.preferences.ProxyPreferencesManager;
import javafx.application.Platform;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.stage.Stage;
import org.slf4j.LoggerFactory;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.Optional;
import java.util.concurrent.FutureTask;

/**
 * LoginConnectivityTest
 */
public abstract class LoginConnectivityTest {
    private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(LoginConnectivityTest.class);

    private final Server userAuthority;
    private final ProxyPreferencesManager proxyPrefMan;
    private final GeniUserProvider geniUserProvider;
    private final TestbedInfoSource testbedInfoSource;
    private final JFedGuiPreferences jFedPreferences;


    public LoginConnectivityTest(Server userAuthority, ProxyPreferencesManager proxyPrefMan,
                                 GeniUserProvider geniUserProvider, TestbedInfoSource testbedInfoSource,
                                 JFedGuiPreferences jFedPreferences) {
        this.userAuthority = userAuthority;
        this.proxyPrefMan = proxyPrefMan;
        this.geniUserProvider = geniUserProvider;
        this.testbedInfoSource = testbedInfoSource;
        this.jFedPreferences = jFedPreferences;
    }

    //used to show feedback messages. implementers should note these can be called on any thread.
    protected abstract void busyMessage(String message);

    protected abstract void okMessage(String message);

    protected abstract void errorMessage(String message);

    /**
     * called when all testing is finished.  (so to use this testing, create a class extending this, and implement this method)
     *
     * @param success    true when connectivity is ok, false otherwise.
     * @param abortLogin true when the user wants to abort login (and login with another account). false when the user wants to login, even though the connectivity test failed.
     */
    protected abstract void connectivityTestFinished(boolean success, boolean abortLogin);

    /**
     * get a javafx Stage for showing dialogs.
     */
    protected abstract Stage getDialogStage();

    public void start() {
        Thread cttThread = new Thread(() -> {
            //now check the jFed proxy settings
            try {
                final boolean proxyAvailable = userAuthority != null &&
                        userAuthority.getTestbed() != null &&
                        userAuthority.getTestbed().getProxies() != null &&
                        !userAuthority.getTestbed().getProxies().isEmpty();
                final boolean proxyActivated = proxyPrefMan.isProxyEnabledForJfed();

                if (!proxyAvailable && proxyActivated) {

                    FutureTask<Boolean> alertTask = new FutureTask<>(() -> {
                        Alert noProxyAlert = new Alert(Alert.AlertType.WARNING);
                        noProxyAlert.initOwner(getDialogStage());
                        noProxyAlert.setContentText(String.format("jFed does not know a proxy for your user authority (%s)\n" +
                                        "However, the proxy is activated in the jFed settings\n\n" +
                                        "Do you want to deactivate the proxy in the settings and continue?",
                                userAuthority != null ? userAuthority.getName() : "null"));
                        noProxyAlert.setTitle("Deactivate Proxy?");
                        noProxyAlert.setHeaderText("Deactivate Proxy?");
                        noProxyAlert.getButtonTypes().setAll(ButtonType.YES, ButtonType.NO, ButtonType.CANCEL);

                        return noProxyAlert.showAndWait().filter(r -> r == ButtonType.YES).isPresent();
                    });

                    Platform.runLater(alertTask);

                    if (alertTask.get()) {
                        LOG.info("Deactivating proxy");
                        jFedPreferences.setString(CorePreferenceKey.PREF_SSHPROXY_USE_FOR_JFED,
                                ProxyPreferencesManager.NEVER_USE_PROXY);
                        jFedPreferences.setString(CorePreferenceKey.PREF_SSHPROXY_USE_FOR_SSH,
                                ProxyPreferencesManager.NEVER_USE_PROXY);
                        proxyPrefMan.updateProxySettings();
                    } else {
                        connectivityTestFinished(false/*connectivity success*/, true/*abort login*/);
                        return;
                    }
                }

                boolean proxyOk;
                boolean userAmWithoutProxyOk;
                boolean userAmWithProxyOk;

                if (proxyActivated) {
                    busyMessage("Proxy activated. Checking proxy ports...");

                    assert proxyAvailable;
                    proxyOk = blockingTestProxy();

                    if (proxyOk) {
                        userAmWithProxyOk = true;
                        userAmWithoutProxyOk = false; //untested, but doesn't matter, proxy works so we'll use it.
                    } else {
                        busyMessage("Proxy activated. No reachable proxy ports... Checking connectivity without proxy...");
                        userAmWithoutProxyOk = blockingTestConnectivity(false/*useProxy*/);
                        userAmWithProxyOk = false;
                    }

                    if (proxyOk) {
                        okMessage("Successfully connected using proxy.");
                        connectivityTestFinished(true/*connectivity success*/, false/*abort login*/);
                        return;
                    }

                    if (userAmWithoutProxyOk) {

                        FutureTask<Boolean> proxyUnreachableAlertTask =
                                new FutureTask<>(() -> {
                                    Alert proxyUnreachableAlert = new Alert(Alert.AlertType.WARNING);
                                    proxyUnreachableAlert.initOwner(getDialogStage());
                                    proxyUnreachableAlert.setContentText(
                                            "jFed could not contact the server using the proxy, but was able to contact it without proxy.\n" +
                                                    "Do you want to deactivate the proxy and continue?");
                                    proxyUnreachableAlert.setTitle("Proxy unreachable");
                                    proxyUnreachableAlert.setHeaderText("Proxy unreachable");
                                    proxyUnreachableAlert.getButtonTypes().setAll(ButtonType.YES, ButtonType.NO, ButtonType.CANCEL);

                                    return proxyUnreachableAlert.showAndWait().filter(r -> r == ButtonType.YES).isPresent();
                                });
                        Platform.runLater(proxyUnreachableAlertTask);
                        final boolean deactivateAndContinue = proxyUnreachableAlertTask.get();
                        if (deactivateAndContinue) {
                            LOG.info("Deactivating proxy");
                            jFedPreferences.setString(CorePreferenceKey.PREF_SSHPROXY_USE_FOR_JFED,
                                    ProxyPreferencesManager.NEVER_USE_PROXY);
                            jFedPreferences.setString(CorePreferenceKey.PREF_SSHPROXY_USE_FOR_SSH,
                                    ProxyPreferencesManager.NEVER_USE_PROXY);
                            proxyPrefMan.updateProxySettings();
                        }

                        connectivityTestFinished(true/*connectivity success*/, !deactivateAndContinue/*abort login*/);
                        return;
                    }
                } else {
                    if (proxyAvailable)
                        busyMessage("Proxy not activated. Testing connectivity...");
                    else
                        busyMessage("Testing connectivity...");
                    userAmWithoutProxyOk = blockingTestConnectivity(false/*useProxy*/);

                    if (!userAmWithoutProxyOk) {
                        if (proxyAvailable) {
                            busyMessage("No connectivity. Checking proxy ports...");
                            proxyOk = blockingTestProxy();
                            if (proxyOk) {
                                //Do test proxy further in this case.
                                busyMessage("No connectivity. Reachable proxy ports. Testing connectivity using proxy...");
                                userAmWithProxyOk = blockingTestConnectivity(true/*useProxy*/);
                            } else {
                                userAmWithProxyOk = false;
                            }
                        } else {
                            proxyOk = false;
                            userAmWithProxyOk = false;
                        }
                    } else {
                        //not tested, bu we don't care as it works without
                        proxyOk = false;
                        userAmWithProxyOk = false;
                    }

                    if (userAmWithoutProxyOk) {
                        okMessage("Successfully connected.");
                        connectivityTestFinished(true/*connectivity success*/, false/*abort login*/);
                        return;
                    }

                    if (proxyOk && userAmWithProxyOk) {
                        FutureTask<Boolean> activateProxyAlertTask = new FutureTask<>(() -> {
                            Alert enableProxyAlert = new Alert(Alert.AlertType.CONFIRMATION);
                            enableProxyAlert.initOwner(getDialogStage());
                            enableProxyAlert.setContentText("jFed could not contact the server without the proxy, but was able to contact it with proxy.\n\n" +
                                    "Do you want to activate the proxy and continue?");
                            enableProxyAlert.setTitle("Activate Proxy?");
                            enableProxyAlert.setHeaderText("Activate Proxy?");

                            return enableProxyAlert.showAndWait().filter(r -> r == ButtonType.YES).isPresent();
                        });
                        Platform.runLater(activateProxyAlertTask);

                        boolean activateAndContinue = activateProxyAlertTask.get();
                        if (activateAndContinue) {
                            LOG.info("Activating proxy");
                            jFedPreferences.setString(CorePreferenceKey.PREF_SSHPROXY_USE_FOR_JFED,
                                    ProxyPreferencesManager.ALWAYS_USE_PROXY);
                            jFedPreferences.setString(CorePreferenceKey.PREF_SSHPROXY_USE_FOR_SSH,
                                    ProxyPreferencesManager.ALWAYS_USE_PROXY);
                            proxyPrefMan.updateProxySettings();
                        }
                        connectivityTestFinished(true/*connectivity success*/, !activateAndContinue/*abort login*/);
                        return;
                    }
                }

                errorMessage("Connectivity Problem");
                boolean continueLogin;
                if (proxyAvailable) {
                    FutureTask<Boolean> continueWithoutProxyAlertTask =
                            new FutureTask<>(() -> {
                                Alert noConnectivityAlert = new Alert(Alert.AlertType.ERROR);
                                noConnectivityAlert.initOwner(getDialogStage());
                                noConnectivityAlert.setContentText("jFed could not contact the server with or without using the proxy.\n" +
                                        (proxyActivated ?
                                                "Note: The jFed proxy is activated in your settings and will stay activated."
                                                : "Note: The jFed proxy is deactivated in your settings and will stay deactivated.") +
                                        "\n\nDo you want to continue anyway? (Most likely, nothing will work)");
                                noConnectivityAlert.setTitle("Connectivity Problem");
                                noConnectivityAlert.setHeaderText("Connectivity Problem");
                                noConnectivityAlert.getButtonTypes().setAll(ButtonType.YES, ButtonType.NO, ButtonType.CANCEL);
                                return noConnectivityAlert.showAndWait().filter(r -> r == ButtonType.YES).isPresent();
                            });
                    Platform.runLater(continueWithoutProxyAlertTask);
                    continueLogin = continueWithoutProxyAlertTask.get();
                } else {
                    FutureTask<Boolean> continueWithoutConnectivityAlertTask =
                            new FutureTask<>(() -> {
                                Alert noConnectivityAndProxyAlert = new Alert(Alert.AlertType.ERROR);
                                noConnectivityAndProxyAlert.initOwner(getDialogStage());
                                noConnectivityAndProxyAlert.setContentText(
                                        String.format("jFed could not contact the server.\n" +
                                                        "Note: jFed knows no proxy for your user authority (%s), so no proxy was tried.\n\n" +
                                                        "Do you want to continue anyway? (Most likely, nothing will work)",
                                                userAuthority != null ? userAuthority.getName() : "null"));
                                noConnectivityAndProxyAlert.setTitle("Connectivity Problem");
                                noConnectivityAndProxyAlert.setHeaderText("Connectivity Problem");
                                noConnectivityAndProxyAlert.getButtonTypes().setAll(ButtonType.YES, ButtonType.NO, ButtonType.CANCEL);
                                return noConnectivityAndProxyAlert.showAndWait().filter(r -> r == ButtonType.YES).isPresent();
                            });
                    Platform.runLater(continueWithoutConnectivityAlertTask);
                    continueLogin = continueWithoutConnectivityAlertTask.get();
                }
                connectivityTestFinished(false/*connectivity success*/, !continueLogin/*abort login*/);
            } catch (Exception | AssertionError e) {
                LOG.error("Exception while testing connectivity", e);
                errorMessage("Exception while testing connectivity: " + e.getMessage());
                connectivityTestFinished(false/*connectivity success*/, true/*abort login*/);
            }
        });
        cttThread.setName("ConnectivityTestThread");
        cttThread.setDaemon(true);
        cttThread.start();
    }

    private boolean proxyEnabledInPreferences() {
        return proxyPrefMan.isProxyEnabledForJfed();
    }

    private boolean blockingTestProxy() {
        busyMessage("Testing connectivity to proxy...");

        final ProxyPreferencesManager.TryProxyTask tryProxyTask = proxyPrefMan.getTryProxyTask(userAuthority);
        final Thread thread = new Thread(tryProxyTask);
        thread.setName("ProxyTestThread");
        thread.setDaemon(true);
        thread.start();

        try {
            thread.join(60 * 1000); //wait maximum 1 minute for this test.
        } catch (InterruptedException e) {
            LOG.error("Thread " + thread.getName() + " was interrupted.", e);
        }

        if (thread.isAlive()) {
            errorMessage("Proxy Test Task takes too long. Assuming failure.");
            LOG.error("Proxy Test Task takes too long. Assuming failure. (" + tryProxyTask.getState() + "). " + tryProxyTask.getMessage(), tryProxyTask.getException());
            return false;
        }
        if (!tryProxyTask.isFinished()) {
            errorMessage("Proxy Test Task Failed.");
            LOG.error("Proxy Test Task Failed (" + tryProxyTask.getState() + "). " + tryProxyTask.getMessage(), tryProxyTask.getException());
            return false;
        }

        boolean res = tryProxyTask.isSuccess();
        proxyPrefMan.updateProxySettings();
        return res;
    }

    private boolean blockingTestConnectivity(boolean useProxy) {
        if (userAuthority.getServices() == null || userAuthority.getServices().isEmpty()) {
            LOG.warn("Could not find URL for AM to test connectivity");
            return false;
        }

        //choose a URL to contact. Prefer AM's
        URL url = ApiInfo.findUrl(userAuthority, new ApiInfo.Api(ApiInfo.ApiName.GENI_AM, 2));
        if (url == null) url = ApiInfo.findUrl(userAuthority, new ApiInfo.Api(ApiInfo.ApiName.GENI_AM, 3));
        if (url == null) url = ApiInfo.findUrl(userAuthority, new ApiInfo.Api(ApiInfo.ApiName.GENI_AM, 4));
        if (url == null) url = ApiInfo.findUrl(userAuthority, new ApiInfo.Api(ApiInfo.ApiName.GENI_AM, 1));
        if (url == null)
            try {
                url = new URL(userAuthority.getServices().get(0).getUrl());
            } catch (MalformedURLException e) {
                throw new RuntimeException("BasicTestbedInfoSource should have prevented this exception from being possible", e);
            }
        LOG.debug("Trying to contact '{}' for connectivity test.", url.toExternalForm());

        //sslHostAndPortTest.call() seems to hang on MAC. So we'll have to manually wait and interrupt it if it takes to long
        JFedConnection.ProxyInfo proxyInfo;
        if (useProxy) {
            GeniUser currentUser = geniUserProvider.getLoggedInGeniUser();
            final GeniUser geniUser = currentUser;
            proxyInfo =
                    proxyPrefMan.chooseUserProxy(userAuthority.getTestbed().getProxies(), currentUser, SshKeyInfoFactory.createGeniUserSshKeyInfo(geniUser));

            if (proxyInfo == null)
                LOG.warn("useProxy=true but proxyInfo=null");
        } else
            proxyInfo = null;

        final SslHostAndPortTest sslHostAndPortTest =
                new SslHostAndPortTest(url, null, null, proxyInfo, testbedInfoSource);

        SafeConnectivityTest<SslHostAndPortTest> safeSslHostAndPortTest =
                new SafeConnectivityTest<>(sslHostAndPortTest, 22 * 1000 /* 2 seconds extra than maximum timeout*/);

        final Optional<ConnectivityTest.ConnectivityTestResult> testResult = safeSslHostAndPortTest.call();

        if (!testResult.isPresent() || testResult.get().getStatus() != ConnectivityTest.Status.SUCCEEDED) {
            LOG.warn("Could not establish SSL-connection to {} with proxyInfo {}", url, proxyInfo);
            return false;
        }

        LOG.info("Successfully connected to the AM in connectivity test. useProxy=" + useProxy);

        return true;
    }
}
