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

import be.iminds.ilabt.jfed.experimenter_gui.config.JFedExperimenterGuiConfigProvider;
import be.iminds.ilabt.jfed.experimenter_gui.config.UserInfoProvider;
import be.iminds.ilabt.jfed.experimenter_gui.connectivity_tester.ConnectivityTesterDialog;
import be.iminds.ilabt.jfed.experimenter_gui.ui.status.TaskStatusIndicator;
import be.iminds.ilabt.jfed.experimenter_gui.util.ConnectivityDetector;
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.user.*;
import be.iminds.ilabt.jfed.lowlevel.userloginmodel.InvalidLoginException;
import be.iminds.ilabt.jfed.lowlevel.userloginmodel.InvalidPasswordException;
import be.iminds.ilabt.jfed.preferences.GuiPreferenceKey;
import be.iminds.ilabt.jfed.preferences.JFedGuiPreferences;
import be.iminds.ilabt.jfed.preferences.ProxyPreferencesManager;
import be.iminds.ilabt.jfed.lowlevel.ssh_key_info.putty.PpkHelper;
import be.iminds.ilabt.jfed.ui.javafx.GlyphUtils;
import be.iminds.ilabt.jfed.ui.javafx.dialogs.JFDialogs;
import be.iminds.ilabt.jfed.util.JFedCommonDialogs;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.TilePane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.controlsfx.dialog.ExceptionDialog;
import org.controlsfx.glyphfont.FontAwesome;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.inject.Inject;
import javax.inject.Provider;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.Objects;

/**
 * This UserLogin-window gives the user the possiblity to choose between three options:
 * - Login with Fed4FIRE account
 * - Login with GENI account
 * - Login with certificate file.
 * <p>
 * In the first two cases, an URLLogin-window must open to the appropriate login portal website, in the other case,
 * the user is presented with a file-selection box and password input option.
 * <p/>
 * User: twalcari
 * Date: 10/31/13
 * Time: 5:27 PM
 */

public class SimpleUserLoginController {
    private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(SimpleUserLoginController.class);
    private static final String CUSTOM_INPUT = "<CUSTOM INPUT>";
    private static final Duration EXPIRATION_WARNING_DURATION = Duration.ofDays(30);
    protected static final String CACHED_LOGIN = "<CACHED LOGIN>";

    @FXML
    private Label usernameLabel;
    @FXML
    private Label authorityLabel;
    @FXML
    private Label expireLabel;
    @FXML
    private TextField fileInput;
    @FXML
    private Button fileBrowseButton;
    @FXML
    private PasswordField passwordField;
    @FXML
    private Label messageLabel;
    @FXML
    private Button advancedButton;
    @FXML
    private Button resetButton;
    @FXML
    private HBox bottomBar;
    @FXML
    private Button loginButton;
    @FXML
    private TaskStatusIndicator statusIndicator;

    @FXML
    private VBox noInternetVBox;
    @FXML
    private CheckBox forceInternetCheckbox;

    @FXML
    private TilePane loginMethodsPane;
    @FXML
    private VBox localLoginPane;
    @FXML
    private Button fed4fireLoginButton;
    @FXML
    private Button geniLoginButton;
    @FXML
    private Button localLoginButton;

    private final UserLoginInfoFactory userLoginInfoFactory;


    private final Provider<ConnectivityTesterDialog> connectivityTesterDialogProvider;

    private final ProxyPreferencesManager proxyPreferencesManager;

    private final TestbedInfoSource testbedInfoSource;

    private final JFedGuiPreferences jFedPreferences;

    private final ConnectivityDetector connectivityDetector;

    private final JFedExperimenterGuiConfigProvider configProvider;
    private final UserInfoProvider userInfoProvider;

    private final ObjectProperty<UserLoginInfo> userLoginInfo = new SimpleObjectProperty<>();

    private Result result;

    @Inject
    public SimpleUserLoginController(UserLoginInfoFactory userLoginInfoFactory,
                                     Provider<ConnectivityTesterDialog> connectivityTesterDialogProvider,
                                     ProxyPreferencesManager proxyPreferencesManager, TestbedInfoSource testbedInfoSource,
                                     JFedGuiPreferences jFedPreferences,
                                     ConnectivityDetector connectivityDetector,
                                     JFedExperimenterGuiConfigProvider configProvider, UserInfoProvider userInfoProvider) {
        this.userLoginInfoFactory = userLoginInfoFactory;
        this.connectivityTesterDialogProvider = connectivityTesterDialogProvider;
        this.proxyPreferencesManager = proxyPreferencesManager;
        this.testbedInfoSource = testbedInfoSource;
        this.jFedPreferences = jFedPreferences;
        this.connectivityDetector = connectivityDetector;
        this.configProvider = configProvider;
        this.userInfoProvider = userInfoProvider;
    }

    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");

    @FXML
    public void initialize() {
        userLoginInfo.addListener(observable -> modelToGui());
        userLoginInfo.addListener(observable -> {
            if (userLoginInfo.get() != null && userLoginInfo.get().getUserUrn() != null)
                configProvider.prefetchConfig(userInfoProvider.createUserInfo(userLoginInfo.get().getUserUrn()));
        });

        fileInput.focusedProperty().addListener((observableValue, oldValue, newValue) -> {
            //when the input-field loses focus, it should process it's content
            if (oldValue && !newValue) {
                onFileInputAction();
            }

        });

        noInternetVBox.managedProperty().bind(noInternetVBox.visibleProperty());
        noInternetVBox.setVisible(connectivityDetector.isNoInternet());
        forceInternetCheckbox.setSelected(false);

        localLoginButton.setOnAction(e -> showLocalLoginMethod());

        statusIndicator.visibleProperty().bind(statusIndicator.statusProperty().isNotEqualTo(TaskStatusIndicator.Status.INACTIVE));
        statusIndicator.managedProperty().bind(statusIndicator.visibleProperty());

        new SystemProxyTestThread().start();
    }

    private void setMessage(String message, MessageType type) {
        messageLabel.setText(message);

        for (MessageType t : MessageType.values())
            messageLabel.getStyleClass().remove(t.getStyleName());

        messageLabel.getStyleClass().add(type.getStyleName());
    }

    private void setExpireLabelAndNotification(Date certExpire) {
        if (certExpire == null) {
            expireLabel.setText("-");
            expireLabel.setGraphic(null);
            return;
        }

        String dateString = DATE_FORMAT.format(certExpire);
        expireLabel.setText(dateString);

        Duration durationToExpiration = Duration.between(Instant.now(), certExpire.toInstant());

        if (durationToExpiration.isNegative()) {
            Node errorGlyph = GlyphUtils.createSmallGlyph(FontAwesome.Glyph.TIMES_CIRCLE, Color.RED);
            Tooltip.install(errorGlyph, new Tooltip("Your certificate has expired! Please select a valid certificate."));
            expireLabel.setGraphic(errorGlyph);
        } else if (durationToExpiration.compareTo(EXPIRATION_WARNING_DURATION) < 0) {
            Node warningGlyph = GlyphUtils.createSmallGlyph(FontAwesome.Glyph.WARNING, Color.YELLOW);
            Tooltip.install(warningGlyph,
                    new Tooltip(
                            String.format("Your certificate will expire in %d days! " +
                                            "Please contact your authority to get a new certificate.",
                                    ChronoUnit.DAYS.between(Instant.now(), certExpire.toInstant()))));
            expireLabel.setGraphic(warningGlyph);
        } else {
            Node checkGlyph = GlyphUtils.createSmallGlyph(FontAwesome.Glyph.CHECK, Color.GREEN);
            Tooltip.install(checkGlyph, new Tooltip("The expiry date of your certificate is sufficiently far in the future."));
            expireLabel.setGraphic(checkGlyph);
        }
    }

    /**
     * Resets the login-screen to the empty state.
     */
    private void clear() {
        fileInput.setText("");
        usernameLabel.setText("-");
        authorityLabel.setText("-");
        expireLabel.setText("-");
        setMessage("Please select your user certificate", MessageType.INFO);
    }

    @FXML
    public void unlockAndLogin() {
        final String password = passwordField.getText();

        final UserLoginInfo currentUserInfo = this.userLoginInfo.get();
        try {
            GeniUser loggedInUser = currentUserInfo.login(password.toCharArray());
            this.result = new LoggedInResult(currentUserInfo, loggedInUser);

            LOG.debug("Successfully logged in user {}", loggedInUser.getUserUrnString());
            setMessage("Password OK!\nLogging in...", MessageType.INFO);

            //create a PPK encrypted with the same password, and store that PPK. Then forget the password.
            if (!passwordField.getText().isEmpty())
                PpkHelper.makePpkIfNeeded(loggedInUser, password);

            //now start post-login tests
            setMessage("Password OK!\nChecking connection with server...", MessageType.INFO);

            statusIndicator.setStatus(TaskStatusIndicator.Status.BUSY);

            //now test connectivity before continuing
            startConnectivityTest(loggedInUser);
        } catch (InvalidPasswordException e) {
            LOG.error("Invalid password for user {}", currentUserInfo.getUserUrn(), e);
            setMessage("Password isn't correct.\nPlease enter your password again.", MessageType.ERROR);

        } catch (InvalidLoginException e) {
            LOG.error("Invalid login for user {}", currentUserInfo.getUserUrn(), e);

            ExceptionDialog dialog = new ExceptionDialog(e);
            dialog.setTitle("Login failed");
            dialog.setHeaderText("Login failed");
            dialog.setContentText("jFed detected a problem with your login certificate:\n\n" +
                    "   " + e.getMessage());

            dialog.showAndWait();
        }
    }

    /**
     * This method performs connectivity-tests, and calls closeLoginAndProceed() when successful
     *
     * @param loggedInUser currently logged in user
     */
    private void startConnectivityTest(GeniUser loggedInUser) {
        final GeniUserProvider geniUserProvider = new GeniUserProvider() {
            @Override
            public GeniUser getLoggedInGeniUser() {
                return loggedInUser;
            }

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

        final LoginConnectivityTest loginConnectivityTest =
                new LoginConnectivityTest(loggedInUser.getUserAuthorityServer(), proxyPreferencesManager, geniUserProvider, testbedInfoSource, jFedPreferences) {

                    @Override
                    protected void busyMessage(final String message) {
                        Platform.runLater(() -> {
                            setMessage(message, MessageType.INFO);
                            statusIndicator.setStatus(TaskStatusIndicator.Status.BUSY);
                        });
                    }

                    @Override
                    protected void okMessage(final String message) {
                        Platform.runLater(() -> {
                            setMessage(message, MessageType.INFO);
                            statusIndicator.setStatus(TaskStatusIndicator.Status.SUCCESS);
                        });
                    }

                    @Override
                    protected void errorMessage(final String message) {
                        Platform.runLater(() -> {
                            setMessage(message, MessageType.ERROR);
                            statusIndicator.setStatus(TaskStatusIndicator.Status.FAILED);
                        });
                    }

                    @Override
                    protected void connectivityTestFinished(final boolean success, final boolean abortLogin) {
                        Platform.runLater(() -> {
                            if (!abortLogin) {
                                if (success)
                                    setMessage("Connectivity OK, logging in.", MessageType.INFO);
                                else
                                    setMessage("Connectivity test failed. But logging in anyway!", MessageType.WARNING);
                                statusIndicator.setStatus(TaskStatusIndicator.Status.SUCCESS);

                                closeLoginAndProceed();
                            } else {
                                if (success)
                                    setMessage("Connectivity OK, but user aborted login.", MessageType.WARNING);
                                else
                                    setMessage("Connectivity test failed. Login Aborted.", MessageType.ERROR);
                                statusIndicator.setStatus(TaskStatusIndicator.Status.FAILED);
                            }
                        });
                    }

                    @Override
                    protected Stage getDialogStage() {
                        return (Stage) usernameLabel.getScene().getWindow();
                    }
                };
        loginConnectivityTest.start();
    }


    @FXML
    private void openCertificateFileChooser() {
        FileChooser fc = new FileChooser();

        //set the default open dir
        if (System.getProperty("user.home") != null)
            fc.setInitialDirectory(new File(System.getProperty("user.home")));

        if (jFedPreferences.containsPreference(GuiPreferenceKey.PREF_RSPECFOLDER_REQUEST)) {
            File dir = new File(jFedPreferences.getString(GuiPreferenceKey.PREF_RSPECFOLDER_REQUEST));
            if (dir.exists()) {
                fc.setInitialDirectory(dir);
            }
        }

        File file = fc.showOpenDialog(null);

        if (file == null)
            return;

        loadPemFile(file);
    }

    private void loadPemFile(@Nonnull File file) {
        try {
            UserLoginInfo newUserLoginInfo = userLoginInfoFactory.createKeyCertFileUserLoginInfo(file);

            userLoginInfo.set(newUserLoginInfo);

        } catch (IOException e) {
            LOG.error("Error while loading key-certificate-pair from file {}", file, e);
            JFDialogs.create()
                    .owner(fileBrowseButton.getScene().getWindow())
                    .message("The provided login certificate could not be properly processed. " +
                            "Please check if the file format is supported, " +
                            "or download the certificate again from your authority.")
                    .masthead("Invalid login certificate")
                    .title("Invalid login certificate")
                    .showException(e);

            clear();

            userLoginInfo.setValue(null);
        }
    }

    private void loadPemUrl(@Nonnull URL url) {
        //we assume that the url will contain an key-certificate-pair

        try {
            UserLoginInfo newUserLoginInfo = userLoginInfoFactory.createKeyCertUrlUserLoginInfo(url);

            userLoginInfo.set(newUserLoginInfo);

        } catch (IOException e) {
            LOG.error("Error while loading key-certificate-pair from URL", e);
            JFDialogs.create()
                    .owner(fileBrowseButton.getScene().getWindow())
                    .message("An error occurred when using the provided URL to fetch the login-information")
                    .masthead("Invalid URL")
                    .title("Invalid URL")
                    .showException(e);

            userLoginInfo.set(null);
        }
    }

    /**
     * This function gets all applicable information from the userLoginModel and puts it in the right fields of the GUI
     */
    private void modelToGui() {
        //always clear password field when refreshing
        passwordField.setText("");

        UserLoginInfo userLoginInfo = this.userLoginInfo.get();

        if (userLoginInfo != null) {
            if (userLoginInfo instanceof KeyCertFileUserLoginInfo) {
                KeyCertFileUserLoginInfo keyCertFileUserLoginInfo = (KeyCertFileUserLoginInfo) userLoginInfo;

                if (!UserLoginDirector.isCachedLogin(keyCertFileUserLoginInfo.getFile())) {
                    fileInput.setText(keyCertFileUserLoginInfo.getFile().getAbsolutePath());
                    fileInput.setDisable(false);
                } else {
                    fileInput.setText(CACHED_LOGIN);
                    fileInput.setDisable(true);
                }
            } else if (userLoginInfo instanceof KeyCertUrlUserLoginInfo) {
                KeyCertUrlUserLoginInfo keyCertUrlUserLoginInfo = (KeyCertUrlUserLoginInfo) userLoginInfo;
                fileInput.setText(keyCertUrlUserLoginInfo.getUrl().toExternalForm());
                fileInput.setDisable(false);
            } else if (userLoginInfo instanceof ManualUserLoginInfo) {
                fileInput.setText(CUSTOM_INPUT);
                fileInput.setDisable(true);
            }


            if (userLoginInfo.getUserUrn() != null)
                usernameLabel.setText(userLoginInfo.getUserUrn().getEncodedResourceName());
            else
                usernameLabel.setText("<Not available>");

            if (userLoginInfo.getUserAuthorityServer() != null)
                authorityLabel.setText(userLoginInfo.getUserAuthorityServer().getName());
            else
                authorityLabel.setText("<Not available>");

            setExpireLabelAndNotification(userLoginInfo.getExpirationDate());


            passwordField.setDisable(userLoginInfo.isUnlocked());
            if (!userLoginInfo.isUnlocked()) {
                setMessage("Enter the password associated with the certificate", MessageType.INFO);
                Platform.runLater(passwordField::requestFocus);
            } else {
                setMessage("No password required: You can login", MessageType.INFO);
                Platform.runLater(loginButton::requestFocus);
            }
        } else {
            fileInput.setText("");

            usernameLabel.setText("");
            authorityLabel.setText("");
            expireLabel.setText("");
        }
    }

    @FXML
    public void showAdvancedLogin() {
        result = new ShowAdvancedResult();
        close();
    }

    @FXML
    private void resetJFedAction(ActionEvent actionEvent) {
        JFedCommonDialogs.requestEraseJFedConfiguration(resetButton.getScene().getWindow());
    }

    private void closeLoginAndProceed() {
        if (noInternetVBox.isVisible()) {
            assert connectivityDetector.isNoInternet();
            if (forceInternetCheckbox.isSelected()) {
                LOG.debug("User has chosen to force jFed to keep checking internet connectivity");
                connectivityDetector.forceNoInternet(false);
            }
        }

        //forget password in GUI
        passwordField.setText("");

        close();
    }


    private void showConnectivityFailed(String shortMessage, String fullMessage) {
        statusIndicator.setStatus(TaskStatusIndicator.Status.FAILED);
        setMessage(shortMessage, MessageType.ERROR);

        JFDialogs.create()
                .owner(loginButton.getScene().getWindow())
                .message(fullMessage)
                .masthead("Could not contact the server")
                .showError();
    }

    @FXML
    private void showConnectivityTesterAction() {
        connectivityTesterDialogProvider.get().showDialog();
    }


    /**
     * This method is called when the user presses enter on the file input
     */
    @FXML
    private void onFileInputAction() {
        if (Objects.equals(fileInput.getText(), CUSTOM_INPUT))
            return;

        File file = new File(fileInput.getText());
        if (file.exists()) {
            loadPemFile(file);
        } else {
            //check if it is an url
            try {
                URL url = new URL(fileInput.getText());
                loadPemUrl(url);
            } catch (MalformedURLException e) {
                LOG.debug("Given input is not a file, nor an URL");

            }
        }


    }

    public Result getResult() {
        return result;
    }

    public UserLoginInfo getUserLoginInfo() {
        return userLoginInfo.get();
    }

    public void setUserLoginInfo(UserLoginInfo userLoginInfo) {
        this.userLoginInfo.set(userLoginInfo);
    }

    @FXML
    public void showFed4FireLogin() {
        result = new URLResult(URLLoginController.FED4FIRE_DOWNLOAD_URL);
        close();
    }

    @FXML
    public void showGeniLogin() {
        result = new URLResult(URLLoginController.GENI_DOWNLOAD_URL);
        close();
    }

    private void close() {
        ((Stage) loginButton.getScene().getWindow()).close();
    }

    private enum MessageType {
        INFO("label-info"),
        WARNING("label-warning"),
        ERROR("label-error");
        private final String styleName;

        MessageType(String styleName) {
            this.styleName = styleName;
        }

        private String getStyleName() {
            return styleName;
        }

    }


    public void showLocalLoginMethod() {
        loginMethodsPane.setVisible(false);
        localLoginPane.setVisible(true);
    }

    public static boolean isSupportedLoginMethod(@Nonnull UserLoginInfo userLoginInfo) {
        return userLoginInfo instanceof KeyCertFileUserLoginInfo
                || userLoginInfo instanceof KeyCertUrlUserLoginInfo
                || userLoginInfo instanceof ManualUserLoginInfo;
    }


    public interface Result {
    }

    public static class URLResult implements Result {
        private final String url;

        public URLResult(String url) {
            this.url = url;
        }

        public String getUrl() {
            return url;
        }
    }

    public static class LoggedInResult implements Result {
        private final UserLoginInfo userLoginInfo;
        private final GeniUser loggedInUser;

        public LoggedInResult(UserLoginInfo userLoginInfo, GeniUser loggedInUser) {
            this.userLoginInfo = userLoginInfo;
            this.loggedInUser = loggedInUser;
        }

        public UserLoginInfo getUserLoginInfo() {
            return userLoginInfo;
        }

        public GeniUser getLoggedInUser() {
            return loggedInUser;
        }
    }

    public static class ShowAdvancedResult implements Result {
    }

}
