package be.iminds.ilabt.jfed.experimenter_gui.preferences.subsections;

import be.iminds.ilabt.jfed.connectivity_tester.ConnectivityTest;
import be.iminds.ilabt.jfed.connectivity_tester.SslHostAndPortTest;
import be.iminds.ilabt.jfed.experimenter_gui.ExperimenterGUI;
import be.iminds.ilabt.jfed.experimenter_gui.preferences.AbstractPreferencesSubPane;
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.PuTTYFilesKeyInfo;
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 be.iminds.ilabt.jfed.util.OSDetector;
import be.iminds.ilabt.jfed.util.tmp_file_helpers.TmpFile;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.RadioButton;
import javafx.scene.layout.BorderPane;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Date;
import java.util.Objects;
import java.util.concurrent.ExecutionException;

/**
 * ProxyPane
 */
public class ProxyPane extends AbstractPreferencesSubPane {
    private final static Logger LOG = LoggerFactory.getLogger(ProxyPane.class);

    @FXML
    protected RadioButton neverProxyForJFedRadioButton;
    @FXML
    protected RadioButton autoProxyForJFedRadioButton;
    @FXML
    protected RadioButton alwaysProxyForJFedRadioButton;
    @FXML
    protected RadioButton neverProxyForSshRadioButton;
    @FXML
    protected RadioButton autoProxyForSshRadioButton;
    @FXML
    protected RadioButton alwaysProxyForSshRadioButton;
    @FXML
    protected Button tryProxyButton;
    @FXML
    protected Label tryProxyResultLabel;
    @FXML
    protected ProgressIndicator proxyProgressBar;
    @FXML
    private BorderPane root;


    @Override
    public Node getRoot() {
        return root;
    }

    private final ExperimenterGUI experimenterGUI;
    private final GeniUserProvider geniUserProvider;
    private final ProxyPreferencesManager proxyPreferencesManager;
    private final TestbedInfoSource testbedInfoSource;
    private final JFedGuiPreferences jFedPreferences;

    @Inject
    ProxyPane(ExperimenterGUI experimenterGUI, GeniUserProvider geniUserProvider,
              ProxyPreferencesManager proxyPreferencesManager, TestbedInfoSource testbedInfoSource,
              JFedGuiPreferences jFedPreferences) {
        super("Proxy", false);
        this.experimenterGUI = experimenterGUI;
        this.geniUserProvider = geniUserProvider;
        this.proxyPreferencesManager = proxyPreferencesManager;
        this.testbedInfoSource = testbedInfoSource;
        this.jFedPreferences = jFedPreferences;
    }

    @FXML
    private void initialize() {
        //load settings
        String useProxyForJFed = jFedPreferences.getString(CorePreferenceKey.PREF_SSHPROXY_USE_FOR_JFED);
        String useProxyForSsh = jFedPreferences.getString(CorePreferenceKey.PREF_SSHPROXY_USE_FOR_SSH);
        if (useProxyForJFed.equalsIgnoreCase("NEVER")) neverProxyForJFedRadioButton.setSelected(true);
        if (useProxyForJFed.equalsIgnoreCase("AUTO")) autoProxyForJFedRadioButton.setSelected(true);
        if (useProxyForJFed.equalsIgnoreCase("ALWAYS")) alwaysProxyForJFedRadioButton.setSelected(true);
        if (useProxyForSsh.equalsIgnoreCase("NEVER")) neverProxyForSshRadioButton.setSelected(true);
        if (useProxyForSsh.equalsIgnoreCase("AUTO")) autoProxyForSshRadioButton.setSelected(true);
        if (useProxyForSsh.equalsIgnoreCase("ALWAYS")) alwaysProxyForSshRadioButton.setSelected(true);

    }

    private void convertPuttyKeysIfNeeded() {
        //make proxy keys if proxy might be used for SSH (AUTO or ALWAYS, so not if NEVER)
        convertPuttyKeysIfNeeded(
                !jFedPreferences.getString(CorePreferenceKey.PREF_SSHPROXY_USE_FOR_SSH).equalsIgnoreCase("NEVER")
        );
    }

    private void convertPuttyKeysIfNeeded(boolean makeProxyKeys) {
        if (OSDetector.os == OSDetector.OS.WIN) {
            //make putty key for SSH connections if needed
            PuTTYFilesKeyInfo sshKeyInfo = jFedPreferences.getCustomSshKeyInfo();
            if (sshKeyInfo != null) {
                TmpFile puttyKeyFile = sshKeyInfo.getPuttyKeyFile();
                puttyKeyFile.store();
                assert puttyKeyFile.getFile().exists();
                puttyKeyFile.delete(); //remove again if tmp file
            }
            //make putty key for proxy connections, if needed
            if (makeProxyKeys) {
                //proxy can be used for SSH
                final GeniUser geniUser = geniUserProvider.getLoggedInGeniUser();
                sshKeyInfo = SshKeyInfoFactory.createGeniUserSshKeyInfo(geniUser);
                TmpFile puttyKeyFile = sshKeyInfo.getPuttyKeyFile();
                puttyKeyFile.store();
                assert puttyKeyFile.getFile().exists();
                puttyKeyFile.delete(); //remove again if tmp file
            }
        }
    }


    private Task<Boolean> tryProxyTask;

    @FXML
    protected void onTryButtonAction() {
        assert tryProxyTask == null;
        assert (geniUserProvider.isUserLoggedIn());

        tryProxyButton.setDisable(true);
        neverProxyForJFedRadioButton.requestFocus(); //prevent reset button from getting focus

        proxyPreferencesManager.resetProxyReachabilityCache();
        tryProxyResultLabel.setText("Proxy port scan in progress...");
        proxyProgressBar.setProgress(0);
        proxyProgressBar.setVisible(true);

        tryProxyTask =
                proxyPreferencesManager.getTryProxyTask(geniUserProvider.getLoggedInGeniUser());

        proxyProgressBar.progressProperty().bind(tryProxyTask.progressProperty());
        tryProxyResultLabel.textProperty().bind(tryProxyTask.messageProperty());
        tryProxyTask.setOnSucceeded(workerStateEvent -> Platform.runLater(this::doProxyLoginTest));
        //start the task
        Thread t = new Thread(tryProxyTask, "Try-Proxy");
        t.setDaemon(true);
        t.start();

        convertPuttyKeysIfNeeded(true/*always make proxy keys*/);
    }

    private Thread sslHostAndPortTestThread;
    private SslHostAndPortTest sslHostAndPortTest;
    private ConnectivityTest.ConnectivityTestResult sslHostAndPortTestResult;

    private void doProxyLoginTest() {
        assert Platform.isFxApplicationThread();
        assert tryProxyTask != null;
        assert sslHostAndPortTest == null;
        assert sslHostAndPortTestThread == null;
        assert sslHostAndPortTestResult == null;

        boolean portsAvailableHelper = false;
        try {
            portsAvailableHelper = tryProxyTask.get();
        } catch (InterruptedException | ExecutionException e) {
            LOG.error("An error occured while trying the proxies");
        }
        final boolean portsAvailable = portsAvailableHelper;

        final GeniUser user = geniUserProvider.getLoggedInGeniUser();
        final Server userAuthority = user.getUserAuthorityServer();
        final GeniUser geniUser = user;
        final JFedConnection.ProxyInfo proxyInfo =
                proxyPreferencesManager.chooseUserProxy(userAuthority.getTestbed().getProxies(), user, SshKeyInfoFactory.createGeniUserSshKeyInfo(geniUser));

        if (portsAvailable) {
            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 = new SslHostAndPortTest(url, null, null, proxyInfo, testbedInfoSource);

            sslHostAndPortTestThread = new Thread(() -> {
                sslHostAndPortTestResult = null;
                try {
                    //this is called in a seperate thread and then joined, to force a timeout.
                    Runnable run = () -> {
                        try {
                            sslHostAndPortTestResult = sslHostAndPortTest.call();
                        } catch (Exception | AssertionError e) {
                            LOG.error("Exception in sslHostAndPortTest.call()", e);
                            sslHostAndPortTestResult = null;
                        }
                    };
                    Thread conTestThread = new Thread(run);
                    Date startTime = new Date();
                    Date stopTime;
                    conTestThread.start();
                    try {
                        conTestThread.join(22 * 1000); //wait 2 seconds longer than connection timeouts  (2 times 10s)
                        stopTime = new Date();
                    } catch (InterruptedException e) {
                        LOG.warn("conTestThread.join(12*1000); was interrupted", e);
                        stopTime = new Date();
                    }
                    long duration_ms = stopTime.getTime() - startTime.getTime();
                    LOG.info("sslHostAndPortTest.call() took " + duration_ms + "ms. alive=" + conTestThread.isAlive() + " result=" + sslHostAndPortTestResult);
                    if (conTestThread.isAlive()) {
                        LOG.warn("sslHostAndPortTest.call() did not end in time.");
                        conTestThread.interrupt();
                    }
                } catch (AssertionError | Exception ex) {
                    LOG.error("Exception trying to setup SSL connection through proxy SSH connection", ex);
                    if (sslHostAndPortTestResult == null)
                        sslHostAndPortTestResult = new ConnectivityTest.ConnectivityTestResult(
                                ConnectivityTest.Status.FAILED,
                                "sslHostAndPortTest.call(): " + ex.getMessage(),
                                ex
                        );
                }

                if (sslHostAndPortTestResult == null)
                    sslHostAndPortTestResult = new ConnectivityTest.ConnectivityTestResult(
                            ConnectivityTest.Status.FAILED,
                            "connectivity test failed for unknown reason");

                LOG.info("Final proxy SSL over SSH test result: " + sslHostAndPortTestResult);

                Platform.runLater(() -> processProxyLoginTestResults(portsAvailable, sslHostAndPortTestResult.getStatus() == ConnectivityTest.Status.SUCCEEDED));
            });

            proxyProgressBar.progressProperty().unbind();
            tryProxyResultLabel.textProperty().unbind();
            proxyProgressBar.setProgress(0.9);
            proxyProgressBar.setVisible(true);
            tryProxyResultLabel.setText("Proxy login test in progress...");
            sslHostAndPortTestThread.start();
        } else {
            //proxy port scan failed
            proxyProgressBar.progressProperty().unbind();
            tryProxyResultLabel.textProperty().unbind();
            proxyProgressBar.setProgress(1.0);
            proxyProgressBar.setVisible(true);
            tryProxyResultLabel.setText("No proxies reachable.");
            processProxyLoginTestResults(portsAvailable, false);
        }
    }

    private void processProxyLoginTestResults(boolean portsAvailable, boolean loginWorks) {
        assert Platform.isFxApplicationThread();

        tryProxyResultLabel.textProperty().unbind();
        if (portsAvailable) {
            autoProxyForSshRadioButton.setDisable(false);
            autoProxyForJFedRadioButton.setDisable(false);
            alwaysProxyForSshRadioButton.setDisable(false);
            alwaysProxyForJFedRadioButton.setDisable(false);

            String proxies = "";
            String prevHostname = "";
            for (ProxyPreferencesManager.ProxyPort pi : proxyPreferencesManager.getReachableProxies()) {
                if (Objects.equals(pi.getHostname(), prevHostname))
                    proxies += "," + pi.getPort();
                else
                    proxies += " " + pi;

                prevHostname = pi.getHostname();
            }
            if (loginWorks)
                tryProxyResultLabel.setText("Proxy reachable:" + proxies);
            else {
                //This test fails for unknown reasons from time to time, see FEDIBBTDEV-633. If this can be fixed, it can be enabled.
                //Stacktrace:
                //   java.net.SocketTimeoutException, exception_stacktrace=java.net.SocketTimeoutException: The kexTimeout (8000 ms) expired.
                //        at ch.ethz.ssh2.Connection.connect(Connection.java:819)
                //        at be.iminds.ilabt.jfed.util.GanymedSshSocketImplFactory.<init>(GanymedSshSocketImplFactory.java:54)
                //        at be.iminds.ilabt.jfed.util.GanymedSshSocketImplFactory.getFactory(GanymedSshSocketImplFactory.java:20)
                //        at be.iminds.ilabt.jfed.connectivity_tester.SslHostAndPortTest.call(SslHostAndPortTest.java:86)
                //        at be.iminds.ilabt.jfed.experimenter_gui.preferences.PreferencesDialog$3$1.run(PreferencesDialog.java:385)
                //        at java.lang.Thread.run(Unknown Source)

//                Stage stage = (Stage) tryProxyButton.getScene().getWindow();
//                Dialogs.showWarningDialog(stage, "There are reachable proxy ports, however, the proxy login test failed. You can try to use the proxy anyway, but it might not work.");
//                tryProxyResultLabel.setText("Proxy might be reachable:" + proxies);


                tryProxyResultLabel.setText("Proxy reachable:" + proxies);
            }
        } else {
            autoProxyForSshRadioButton.setDisable(true);
            autoProxyForJFedRadioButton.setDisable(true);
            alwaysProxyForSshRadioButton.setDisable(true);
            alwaysProxyForJFedRadioButton.setDisable(true);

            tryProxyResultLabel.setText("No proxies reachable.");
        }

        tryProxyTask = null;
        sslHostAndPortTest = null;
        sslHostAndPortTestThread = null;
        sslHostAndPortTestResult = null;
        tryProxyButton.setDisable(false);
        proxyProgressBar.setProgress(1.0);
        proxyProgressBar.setVisible(false);
    }

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

    @Override
    public boolean save() {
        if (neverProxyForJFedRadioButton.isSelected())
            jFedPreferences.setString(CorePreferenceKey.PREF_SSHPROXY_USE_FOR_JFED, ProxyPreferencesManager.NEVER_USE_PROXY);
        if (autoProxyForJFedRadioButton.isSelected())
            jFedPreferences.setString(CorePreferenceKey.PREF_SSHPROXY_USE_FOR_JFED, ProxyPreferencesManager.AUTO_USE_PROXY);
        if (alwaysProxyForJFedRadioButton.isSelected())
            jFedPreferences.setString(CorePreferenceKey.PREF_SSHPROXY_USE_FOR_JFED, ProxyPreferencesManager.ALWAYS_USE_PROXY);

        if (neverProxyForSshRadioButton.isSelected())
            jFedPreferences.setString(CorePreferenceKey.PREF_SSHPROXY_USE_FOR_SSH, ProxyPreferencesManager.NEVER_USE_PROXY);
        if (autoProxyForSshRadioButton.isSelected())
            jFedPreferences.setString(CorePreferenceKey.PREF_SSHPROXY_USE_FOR_SSH, ProxyPreferencesManager.AUTO_USE_PROXY);
        if (alwaysProxyForSshRadioButton.isSelected())
            jFedPreferences.setString(CorePreferenceKey.PREF_SSHPROXY_USE_FOR_SSH, ProxyPreferencesManager.ALWAYS_USE_PROXY);

        //convert PuTTY keys if needed. make proxy key only if proxy selected.
        convertPuttyKeysIfNeeded();

        //send proxy settings to ConnectionPool
        proxyPreferencesManager.updateProxySettings();
        experimenterGUI.updateConnectivityStatusLabel();


        return true;
    }
}
