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

import be.iminds.ilabt.jfed.experimenter_gui.ui.ConfirmingPasswordInputDialog;
import be.iminds.ilabt.jfed.lowlevel.GeniUser;
import be.iminds.ilabt.jfed.lowlevel.user.UserLoginInfo;
import be.iminds.ilabt.jfed.lowlevel.user.UserLoginInfoFactory;
import be.iminds.ilabt.jfed.lowlevel.user.UserLoginInfoManager;
import be.iminds.ilabt.jfed.lowlevel.userloginmodel.InvalidLoginException;
import be.iminds.ilabt.jfed.ui.javafx.userlogin.AdvancedUserLoginDialog;
import be.iminds.ilabt.jfed.util.JFedUtils;
import be.iminds.ilabt.jfed.util.KeyUtil;
import com.google.common.base.Charsets;
import com.google.common.io.Files;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import org.apache.commons.codec.digest.DigestUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.inject.Provider;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.util.Objects;
import java.util.Optional;

/**
 * This class coordinates the user login process. It uses {@link SimpleUserLoginController} and {@link URLLoginController} to allow the user to
 * obtain and unlock with his credentials.
 * <p>
 * <p>
 * User: twalcari
 * Date: 8/8/2016
 * Time: 14:12
 */
public class UserLoginDirector {
    public static final String LOGIN_CERTS_DIR = "login-certs";
    private static final Logger LOG = LoggerFactory.getLogger(UserLoginDirector.class);

    private final String loginUrl;
    private UserLoginInfo oldUserLoginInfo;
    private final UserLoginInfoFactory userLoginInfoFactory;
    private final UserLoginInfoManager userLoginInfoManager;
    private final Provider<URLLoginDialog> urlLoginDialogProvider;
    private final Provider<SimpleUserLoginDialog> simpleUserLoginDialogProvider;
    private final Provider<AdvancedUserLoginDialog> advancedUserLoginDialogProvider;

    /**
     * @param loginUrl                        An URL of a login-method that should be called.
     *                                        When present, this URL will always be used as the first option for users to login,
     *                                        and the initial choice-dialog will be skipped.
     * @param userLoginInfo                   The initial UserLoginInfo that can be used to populate the UserLoginDialog.
     * @param userLoginInfoFactory
     * @param userLoginInfoManager
     * @param urlLoginDialogProvider
     * @param simpleUserLoginDialogProvider
     * @param advancedUserLoginDialogProvider
     */
    public UserLoginDirector(@Nullable String loginUrl, @Nullable UserLoginInfo userLoginInfo,
                             UserLoginInfoFactory userLoginInfoFactory, UserLoginInfoManager userLoginInfoManager,
                             Provider<URLLoginDialog> urlLoginDialogProvider,
                             Provider<SimpleUserLoginDialog> simpleUserLoginDialogProvider,
                             Provider<AdvancedUserLoginDialog> advancedUserLoginDialogProvider) {
        this.loginUrl = loginUrl;
        this.userLoginInfoFactory = userLoginInfoFactory;
        this.userLoginInfoManager = userLoginInfoManager;
        this.urlLoginDialogProvider = urlLoginDialogProvider;
        this.simpleUserLoginDialogProvider = simpleUserLoginDialogProvider;
        this.advancedUserLoginDialogProvider = advancedUserLoginDialogProvider;
        this.oldUserLoginInfo = userLoginInfo;
    }

    public static File createFileForLoginCertificate(String certificate) {
        String certmd5 = DigestUtils.md5Hex(certificate);

        File certDir = getLoginCertificateCacheDir();
        if (!certDir.exists())
            //noinspection ResultOfMethodCallIgnored
            certDir.mkdirs();

        return getLoginCertificateFile(certmd5);
    }

    public static boolean saveCertificate(File certFile, String certificate) {
        try (BufferedWriter writer = Files.newWriter(certFile, Charsets.UTF_8)) {
            writer.write(certificate);
        } catch (IOException e) {
            LOG.error("Could not write certificate to file {}", certFile, e);
            return false;
        }
        return true;
    }

    @Nonnull
    private static File getLoginCertificateCacheDir() {
        return new File(JFedUtils.getUserDataDirectoryFile(), LOGIN_CERTS_DIR);
    }

    public static File getLoginCertificateFile(String certMd5) {
        return new File(getLoginCertificateCacheDir(), certMd5 + ".pem");
    }

    public static boolean isCachedLogin(File file) {
        return file != null && Objects.equals(file.getParentFile(), getLoginCertificateCacheDir());
    }


    /**
     * This function will call the {@link UserLoginDirector#doLogin()}-method,
     * and save the result when the login was successful
     *
     * @return
     */
    @Nullable
    public UserLoginResult showAndWait() {
        UserLoginResult result = doLogin();

        //save UserLoginInfo when successful.
        if (result != null && result.getLoggedInUser() != null) {
            userLoginInfoManager.save(result.getUserLoginInfo());
        }

        return result;
    }

    /**
     * Actual function that starts the login-process
     *
     * @return
     */
    @Nullable
    private UserLoginResult doLogin() {
        if (oldUserLoginInfo != null && oldUserLoginInfo.isUnlocked()) {
            try {
                return new UserLoginResult(oldUserLoginInfo, oldUserLoginInfo.login(null));
            } catch (InvalidLoginException e) {
                LOG.error("Unable to login with provided UserLoginInfo, while it was unlocked!", e);
            }
        }

        if (oldUserLoginInfo == null) {
            try {
                oldUserLoginInfo = userLoginInfoManager.load();
            } catch (InvalidLoginException e) {
                LOG.error("Could not load the old UserLoginInfo from UserLoginInfoManager", e);
                oldUserLoginInfo = userLoginInfoManager.defaults();
            }
        }

        UserLoginResult result;

        if (loginUrl != null) {
            result = showAndProcessURLLogin(loginUrl);

            if (result != null && result.getLoggedInUser() != null)
                return result;
        }

        result = showSimpleUserLoginDialog();

        return result;
    }

    private UserLoginResult showAndProcessURLLogin(String loginUrl) {
        LOG.info("Got login url '{}'. Showing dialog.", loginUrl);
        UserLoginInfo userLoginInfo = showURLLogin(loginUrl);

        if (userLoginInfo != null && userLoginInfo.isValid()) {
            if (userLoginInfo.isUnlocked()) {
                LOG.info("Got an unlocked certificate for {} from URLLogin", userLoginInfo.getUserUrn());
                GeniUser loggedInGeniUser = null;
                try {
                    loggedInGeniUser = userLoginInfo.login(null);
                    UserLoginResult result = new UserLoginResult(userLoginInfo, loggedInGeniUser);

                    result = askUnprotectedCertificateSave(result);

                    return result;
                } catch (InvalidLoginException e) {
                    LOG.error("Unable to login with downloaded UserLoginInfo, while it was unlocked!", e);
                    throw new RuntimeException("Unable to login with downloaded UserLoginInfo, while it was unlocked!", e);
                }
            } else {
                LOG.info("Got a locked certificate for {} from URLLogin", userLoginInfo.getUserUrn());
                //certificate is password protected, so we can save it locally!
                File certFile = createFileForLoginCertificate(userLoginInfo.getCertificateContent());
                saveCertificate(certFile, userLoginInfo.getCertificateContent());

                try {
                    userLoginInfo = userLoginInfoFactory.createKeyCertFileUserLoginInfo(certFile);
                } catch (IOException ex) {
                    LOG.error("Error while opening newly saved userLoginInfo", ex);
                }
                //we now have to ask the user for his password to unlock the certificate
                return showLocalLoginDialog(userLoginInfo);
            }
        } else {
            LOG.info("Did not get a login certicate from URLLogin");
            return null;
        }
    }

    private UserLoginInfo showURLLogin(String loginUrl) {
        URLLoginDialog urlLoginDialog = urlLoginDialogProvider.get();
        String certificateString = urlLoginDialog.showDialog(loginUrl);

        if (certificateString != null) {
            LOG.info("User successfully fetched certificate via {}", loginUrl);
            return userLoginInfoFactory.createManualUserLoginInfo(certificateString);
        } else {
            LOG.warn("User did not successfully fetch certificate via {}", loginUrl);
            return null;
        }
    }


    private UserLoginResult askUnprotectedCertificateSave(UserLoginResult userLoginResult) {
        Alert choiceDialog = new Alert(Alert.AlertType.CONFIRMATION);
        choiceDialog.setTitle("The downloaded certificate is unprotected");
        choiceDialog.setHeaderText("The downloaded certificate is unprotected");
        choiceDialog.setContentText("If you want to use this certificate in future sessions, we recommend that you save it encrypted locally.");

        ButtonType dontSaveButtonType = new ButtonType("Don't Save");
        ButtonType saveEncryptedButtonType = new ButtonType("Save Encrypted");
        ButtonType saveUnencryptedButtonType = new ButtonType("Save Unencrypted");

        choiceDialog.getDialogPane().getButtonTypes().setAll(dontSaveButtonType, saveEncryptedButtonType, saveUnencryptedButtonType);

        Optional<ButtonType> answer = choiceDialog.showAndWait();

        if (answer.isPresent() && answer.get() != dontSaveButtonType) {

            char[] password = null;
            if (answer.get() == saveEncryptedButtonType) {

                ConfirmingPasswordInputDialog cpid = new ConfirmingPasswordInputDialog();
                cpid.setHeaderText("Securing login certificate");
                cpid.setContentText("Please provide a password to encrypt your login certificate.");
                Optional<String> passwordAnswer = cpid.showAndWait();

                if (passwordAnswer.isPresent())
                    password = passwordAnswer.get().toCharArray();
                else
                    return userLoginResult;
            }

            char[] privateKey =
                    KeyUtil.privateKeyToAnyPem(userLoginResult.getLoggedInUser().getPrivateKey(), password);

            StringBuilder newCertificate = new StringBuilder();
            newCertificate.append(privateKey);

            String certificateChain =
                    KeyUtil.x509certificateChainToPem(userLoginResult.getLoggedInUser().getClientCertificateChain());
            newCertificate.append(certificateChain);

            File certFile = createFileForLoginCertificate(userLoginResult.getUserLoginInfo().getCertificateContent());

            saveCertificate(certFile, newCertificate.toString());

            try {
                UserLoginInfo protectedUserLoginInfo = userLoginInfoFactory.createKeyCertFileUserLoginInfo(certFile);
                GeniUser loggedInUser = protectedUserLoginInfo.login(password);
                return new UserLoginResult(protectedUserLoginInfo, loggedInUser);
            } catch (IOException e) {
                LOG.error("Error while saving UserLoginInfo to file {}", certFile.getAbsolutePath());

                //fallback to existing logged in user
                return userLoginResult;
            } catch (InvalidLoginException e) {
                LOG.error("Error while decrypting UserLoginInfo {}. This should NOT happen!", certFile.getAbsolutePath());

                //fallback to existing logged in user
                return userLoginResult;
            }
        } else
            return userLoginResult;
    }


    private UserLoginResult showSimpleUserLoginDialog() {
        SimpleUserLoginDialog simpleUserLoginDialog = simpleUserLoginDialogProvider.get();

        SimpleUserLoginController.Result result = simpleUserLoginDialog.showAndWait(oldUserLoginInfo);

        if (result != null) {
            if (result instanceof SimpleUserLoginController.URLResult) {
                return showAndProcessURLLogin(((SimpleUserLoginController.URLResult) result).getUrl());
            } else if (result instanceof SimpleUserLoginController.ShowAdvancedResult) {
                return showAdvancedUserLoginDialog();
            } else if (result instanceof SimpleUserLoginController.LoggedInResult) {
                final SimpleUserLoginController.LoggedInResult loggedInResult =
                        (SimpleUserLoginController.LoggedInResult) result;
                return new UserLoginDirector.UserLoginResult(loggedInResult.getUserLoginInfo(),
                        loggedInResult.getLoggedInUser());
            } else {
                throw new RuntimeException("Unexpected type of SimpleUserLoginController.Result: " + result.getClass().getSimpleName());
            }
        } else
            return null;
    }

    private UserLoginResult showLocalLoginDialog(UserLoginInfo userLoginInfo) {
        SimpleUserLoginDialog simpleUserLoginDialog = simpleUserLoginDialogProvider.get();

        return simpleUserLoginDialog.showLocalLoginAndWait(userLoginInfo);
    }

    private UserLoginResult showAdvancedUserLoginDialog() {
        final AdvancedUserLoginDialog advancedUserLoginDialog = advancedUserLoginDialogProvider.get();
        GeniUser loggedInUser = advancedUserLoginDialog.showUserLogin(null);

        if (loggedInUser != null) {
            return new UserLoginResult(advancedUserLoginDialog.getUserLoginInfo(), loggedInUser);
        } else
            return null;
    }


    public static class UserLoginResult {
        private final UserLoginInfo userLoginInfo;
        private final GeniUser loggedInUser;

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

        public UserLoginInfo getUserLoginInfo() {
            return userLoginInfo;
        }

        public GeniUser getLoggedInUser() {
            return loggedInUser;
        }
    }

}
