package be.iminds.ilabt.jfed.ssh_terminal_tool.putty;

import be.iminds.ilabt.jfed.lowlevel.connection.JFedConnection;
import be.iminds.ilabt.jfed.lowlevel.ssh_key_info.GeniUserSshKeyInfo;
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.lowlevel.ssh_key_info.putty.PuTTYPrivateKeyFile;
import be.iminds.ilabt.jfed.preferences.GuiPreferenceKey;
import be.iminds.ilabt.jfed.preferences.JFedPreferences;
import be.iminds.ilabt.jfed.util.IOUtils;
import be.iminds.ilabt.jfed.util.tmp_file_helpers.TmpFile;
import org.apache.commons.codec.binary.Base64;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;

/**
 * PuttySession helper to store a putty session in registry
 * <p>
 * Note:  in r888 and before, this used awt.Robot to call puttgen.exe and convert the key.
 * since then, PuTTYPrivateKeyFile has been written to natively read/write PuTTY files
 */
public class PuttyHelper {
    private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(PuttyHelper.class);
    private final static Pattern ipv6pat = Pattern.compile("([0-9a-f]{1,4}:){7}([0-9a-f]){1,4}");

    private final JFedPreferences jFedPreferences;
    private final PageantHelper pageantHelper;
    private final SshKeyInfoUnlocker sshKeyInfoUnlocker;

    @Inject
    public PuttyHelper(JFedPreferences jFedPreferences, PageantHelper pageantHelper, SshKeyInfoUnlocker sshKeyInfoUnlocker) {
        this.jFedPreferences = jFedPreferences;
        this.pageantHelper = pageantHelper;
        this.sshKeyInfoUnlocker = sshKeyInfoUnlocker;
    }


    /* inspired by http://svn.tartarus.org/sgt/putty/contrib/kh2reg.py?view=markup */
    static String convertSshHostKeyToPuttyFormat(String openSshKnownHost) {
        StringBuilder res = new StringBuilder();

        String[] parts = openSshKnownHost.split(" ");
        if (parts.length < 3)
            throw new IllegalArgumentException("The provided known host line is not in the correct format: " + openSshKnownHost);
//        String hostname = parts[0];
//        String sshkeytype = parts[1];
        String encodedKey = parts[2];

//        String puttySshKeyType;
//        if (sshkeytype.equals("ssh-rsa"))
//            puttySshKeyType = "rsa2";
//        else {
//            if (sshkeytype.equals("dss-rsa"))
//                puttySshKeyType = "dss";
//            else
//                throw new IllegalArgumentException("The provided known host line has an unknown key type: " + sshkeytype);
//        }

        byte[] all_restored = Base64.decodeBase64(encodedKey);
        ByteBuffer bb = ByteBuffer.wrap(all_restored).order(ByteOrder.BIG_ENDIAN);
        List<String> fields = new ArrayList<>();
        while (bb.remaining() > 0) {
            int len = bb.getInt();

            if (len > bb.remaining())
                throw new IllegalArgumentException("The provided known host line is not in the correct format. field " + fields.size() + " has len=" + len + " but only " + bb.remaining() + " bytes remain. key=" + encodedKey);

            StringBuilder field = new StringBuilder();
            for (int i = 0; i < len; i++) {
                byte b = bb.get();
                String hex = String.format("%02x", b);

                //skip leading zeroes
                if (field.length() == 0) {
                    if (b == 0) {
                        //do not append
                    } else {
                        if (hex.startsWith("0")) {
                            //remove leading zero
                            hex = hex.substring(1);
                            field.append(hex);
                        } else
                            field.append(hex);
                    }
                } else
                    field.append(hex);


            }
            fields.add(field.toString());
        }

        if (fields.size() < 2)
            throw new IllegalArgumentException("The provided known host line is not in the correct format. field count: " + fields.size());

        for (int i = 1; i < fields.size(); i++) {
            if (i > 1)
                res.append(",");
            res.append("0x").append(fields.get(i));
        }

        return res.toString();
    }

    public static String sshHostKeyToRegistryKey(String openSshKnownHost, int port) {
        String[] parts = openSshKnownHost.split(" ");
        if (parts.length < 3)
            throw new IllegalArgumentException("The provided known host line is not in the correct format: " + openSshKnownHost);
        String hostname = parts[0];
        String sshkeytype = parts[1];

        String puttySshKeyType;
        if (Objects.equals(sshkeytype, "ssh-rsa"))
            puttySshKeyType = "rsa2";
        else {
            if (Objects.equals(sshkeytype, "dss-rsa"))
                puttySshKeyType = "dss";
            else
                throw new IllegalArgumentException("The provided known host line has an unknown key type: " + sshkeytype);
        }

        return puttySshKeyType + "@" + port + ":" + hostname;
    }

    private static void storeOpenSshHostKeyToPuttyRegistry(String openSshKnownHost, int port) throws IOException {
        WinRegistry.setValue("HKCU\\Software\\SimonTatham\\PuTTY\\SshHostKeys",
                sshHostKeyToRegistryKey(openSshKnownHost, port),
                convertSshHostKeyToPuttyFormat(openSshKnownHost));
    }

    /**
     * This assumes:
     * - the private keys are stored in files in the puttygen ppk format.
     * - the private key for the proxy is unlocked
     * - the proxy host key has been registered to putty
     *
     * @param username is not needed for putty session, it may be null
     */
    public static void createPuttySession(String sessionName,
                                          String hostname, int port, File puttyPrivateKeyFile, String username,
                                          String proxyHostname, int proxyPort, File proxyPuttyPrivateKeyFile, String proxyUsername,
                                          boolean enableX11Forward, boolean enableAgentForward) throws IOException {
        StringBuilder regContent = new StringBuilder();
        regContent.append("Windows Registry Editor Version 5.00\n")
                .append("\n")
                .append("[HKEY_CURRENT_USER\\Software\\SimonTatham\\PuTTY\\Sessions]\n")
                .append("\n")
                .append("[HKEY_CURRENT_USER\\Software\\SimonTatham\\PuTTY\\Sessions\\").append(sessionName).append("]\n");

        URL sessionTemplateUrl = PuttyHelper.class.getResource("PuttySessionRegistryTemplate");
        String sessionTemplate = IOUtils.streamToString(sessionTemplateUrl.openStream(), "UTF-8");
        regContent.append(sessionTemplate);
        if (!sessionTemplate.endsWith("\n"))
            regContent.append("\n");

        regContent.append("\"X11Forward\"=dword:0000000").append(enableX11Forward ? "1" : "0").append("\n");

        if (hostname != null)
            regContent.append("\"HostName\"=\"").append(hostname).append("\"").append("\n");
        else
            regContent.append("\"HostName\"=\"\"" + "\n");
        if (port >= 0)
            regContent.append("\"PortNumber\"=dword:").append(Integer.toHexString(port)).append("\n");
        if (puttyPrivateKeyFile != null)
            regContent.append("\"PublicKeyFile\"=\"").append(puttyPrivateKeyFile.getPath().replace("\\", "\\\\")).append("\"\n");
        else
            regContent.append("\"PublicKeyFile\"=\"\"\n");

        if (proxyHostname != null) {
            regContent.append("\"ProxyMethod\"=dword:00000005\n");
            regContent.append("\"ProxyHost\"=\"").append(proxyHostname).append("\"\n");
            regContent.append("\"ProxyPort\"=dword:").append(Integer.toHexString(proxyPort)).append("\n");
            if (proxyPuttyPrivateKeyFile != null)
                regContent.append("\"ProxyTelnetCommand\"=\"plink -ssh -i \\\"")
                        .append(proxyPuttyPrivateKeyFile.getPath().replace("\\", "\\\\\\\\"))
                        .append("\\\" ").append("-l ").append(proxyUsername).append(" -P %proxyport %proxyhost -nc %host:%port\"\n");
            else
                regContent.append("\"ProxyTelnetCommand\"=\"plink -ssh -l ").append(proxyUsername).append(" -P %proxyport %proxyhost -nc %host:%port\"\n");
        } else {
            regContent.append("\"ProxyMethod\"=dword:00000000\n");
            regContent.append("\"ProxyHost\"=\"\"\n");
            regContent.append("\"ProxyPort\"=dword:00000000\n");
            regContent.append("\"ProxyTelnetCommand\"=\"\"\n");
        }

        if (username != null)
            regContent.append("\"UserName\"=\"").append(username).append("\"\n");
        else
            regContent.append("\"UserName\"=\"\"\n");


        //Allow agent forwarding (cfr. FEDIBBTDEV-280)
        if (enableAgentForward) {
            regContent.append("\"AgentFwd\"=dword:00000001\n");
        }

        WinRegistry.importInRegistry(regContent.toString());
    }

    public void runPutty(final PuttyRunParameters parameters) {
        /*
        Always use a partial session when connecting via a proxy.

        Also: when a private key is set in the session, PuTTY forgets to try to use the keys present in Pageant.
        To prevent this from being an issue, we check if such a key is present in the default session,
        and create a custom session if necessary.
        */
        if (parameters.hasProxy() || defaultSessionHasPrivateKeySet())
            runPuttyWithPartialSession(parameters);
        else {
            runPuttyWithoutSession(parameters);
        }
    }

    private boolean defaultSessionHasPrivateKeySet() {
        try {
            String value =
                    WinRegistry.getValue("HKCU\\Software\\SimonTatham\\PuTTY\\Sessions\\Default%20Settings", "PublicKeyFile");

            return value != null && !value.isEmpty();
        } catch (IOException | WinRegistry.RegistryException ignored) {
            return true;
        }
    }

    /**
     * supports both with and without proxy, new session, which includes destination
     * <p>
     * Specifies target details in session.
     * Specifies proxy details in session.
     */
    public void runPuttyWithFullSession(final PuttyRunParameters parameters) {
        LOG.debug("Creating PuTTY session and starting PuTTY with it.");
        final boolean usePageant = jFedPreferences.getBoolean(GuiPreferenceKey.PREF_SSHAGENT_USE, true);

        boolean hasEncryptedPuttyPrivateKeyFile = false;
        final List<TmpFile> puttyPrivateKeyFiles = new ArrayList<>();
        if (usePageant) {
            pageantHelper.registerKeys(parameters.getSshKeyInfos(), sshKeyInfoUnlocker);
        } else {
            for (PuTTYFilesKeyInfo keyInfo : parameters.getSshKeyInfos()) {
                TmpFile puttyPrivateKeyFile = keyInfo.getPuttyKeyFile();
                puttyPrivateKeyFiles.add(puttyPrivateKeyFile);
                puttyPrivateKeyFile.store();
                try {
                    if (PuTTYPrivateKeyFile.read(puttyPrivateKeyFile.getFile()).isEncrypted())
                        hasEncryptedPuttyPrivateKeyFile = true;
                } catch (IOException e) {
                    LOG.error("Error reading puttyPrivateKeyFile. Will ignore for now and assume it is an " +
                            "encrypted file. But this will probably case a failure later.");
                    hasEncryptedPuttyPrivateKeyFile = true;
                }
            }
        }

        TmpFile proxyPuttyPrivateKeyFile = null;
        try {
            if (parameters.hasProxy() &&
                    (puttyPrivateKeyFiles.isEmpty() || !parameters.samePrivateKeyForProxy() || hasEncryptedPuttyPrivateKeyFile)) {
                if (usePageant) {
                    pageantHelper.registerKeys(parameters.getSshKeyInfos(), sshKeyInfoUnlocker);
                    proxyPuttyPrivateKeyFile = null; //use no key file
                } else {
                    proxyPuttyPrivateKeyFile = parameters.getProxySshKeyInfo().getUnlockedPuttyKeyFile();
                }
            } else
                proxyPuttyPrivateKeyFile = null;
            final TmpFile finalProxyPuttyPrivateKeyFile = proxyPuttyPrivateKeyFile;

            if (finalProxyPuttyPrivateKeyFile != null)
                finalProxyPuttyPrivateKeyFile.store();

            //store proxyOpenSshKey
            if (parameters.hasProxy() && parameters.getProxyOpenSshKeyHostKey() != null)
                storeOpenSshHostKeyToPuttyRegistry(parameters.getProxyOpenSshKeyHostKey(), parameters.getProxyPort());

            //create session
            final String sessionName = "jFed" + (new Date().getTime());
            TmpFile puttyPrivateKeyFile = puttyPrivateKeyFiles.isEmpty() ? null : puttyPrivateKeyFiles.get(0);
            if (puttyPrivateKeyFiles.size() > 1) {
                LOG.warn("Using PuTTY with multiple keys but without pageant is not possible. Will use first key only!");
            }
            createPuttySession(
                    sessionName,
                    parameters.getHostname(),
                    parameters.getPort(),
                    puttyPrivateKeyFile == null ? null : puttyPrivateKeyFile.getFile(),
                    parameters.getUsername(),
                    parameters.getProxyHostname(), parameters.getProxyPort(),
                    finalProxyPuttyPrivateKeyFile == null ? (puttyPrivateKeyFile == null ? null
                            : puttyPrivateKeyFile.getFile()) : finalProxyPuttyPrivateKeyFile.getFile(),
                    parameters.getProxyUsername(),
                    parameters.isEnableX11Forward(),
                    parameters.isEnableAgentForward());

            //run session
            Runnable processOutputMonitor = () -> {
                Process p = null;
                try {
                    ArrayList<String> command = new ArrayList<>();
                    command.add(jFedPreferences.getFile(GuiPreferenceKey.PREF_PUTTY_DIRECTORY).getPath() + File.separator + "putty.exe");

                    command.add("-load");
                    command.add(sessionName);

                    LOG.debug("Starting PuTTY and waiting for it to stop");
                    LOG.info("PuTTY command: " + command);
                    p = Runtime.getRuntime().exec(command.toArray(new String[command.size()]));
                    BufferedReader input = new BufferedReader(new InputStreamReader(p.getInputStream()));
                    String line = null;
                    while ((line = input.readLine()) != null) {
                        LOG.debug("putty.exe output: " + line);
                    }
                    input.close();
                    try {
                        p.waitFor();
                    } catch (InterruptedException e) {
                        //ignore
                    }
                    LOG.debug("PuTTY stopped");
                } catch (IOException e) {
                    LOG.error("IOException while trying to launch external terminal: " + e.getMessage(), e);
                } finally {
                    LOG.debug("removing unencrypted putty key files");
                    puttyPrivateKeyFiles.stream()
                            .filter(Objects::nonNull)
                            .forEach(TmpFile::delete);
                    if (finalProxyPuttyPrivateKeyFile != null) finalProxyPuttyPrivateKeyFile.delete();
                }
            };
            Thread t = new Thread(processOutputMonitor);
            t.setName("PuTTY-Thread");
            t.setDaemon(true); //=> this application may exit before putty stops
            t.start();
            LOG.debug("Started PuTTY thread.");
        } catch (IOException e) {
            //remove private key files
            puttyPrivateKeyFiles.stream()
                    .filter(Objects::nonNull)
                    .forEach(TmpFile::delete);
            if (proxyPuttyPrivateKeyFile != null) proxyPuttyPrivateKeyFile.delete();
        }
    }

    /**
     * supports both with and without proxy, reuses same session for same proxy.
     * <p>
     * Specifies target details on command line.
     * Specifies proxy details in session.
     */
    public void runPuttyWithPartialSession(final PuttyRunParameters parameters) {
        LOG.debug("Creating PuTTY session and starting PuTTY with it.");

        final boolean usePageant = jFedPreferences.getBoolean(GuiPreferenceKey.PREF_SSHAGENT_USE, true);
        boolean hasEncryptedPuttyPrivateKeyFile = false;
        final List<TmpFile> puttyPrivateKeyFiles = new ArrayList<>();
        if (usePageant) {
            pageantHelper.registerKeys(parameters.getSshKeyInfos(), sshKeyInfoUnlocker);
        } else {
            for (PuTTYFilesKeyInfo keyInfo : parameters.getSshKeyInfos()) {
                TmpFile puttyPrivateKeyFile = keyInfo.getPuttyKeyFile();
                puttyPrivateKeyFiles.add(puttyPrivateKeyFile);
                puttyPrivateKeyFile.store();
                try {
                    if (PuTTYPrivateKeyFile.read(puttyPrivateKeyFile.getFile()).isEncrypted())
                        hasEncryptedPuttyPrivateKeyFile = true;
                } catch (IOException e) {
                    LOG.error("Error reading puttyPrivateKeyFile. Will ignore for now and assume it is an " +
                            "encrypted file. But this will probably case a failure later.");
                    hasEncryptedPuttyPrivateKeyFile = true;
                }
            }
        }

        TmpFile proxyPuttyPrivateKeyFile = null;
        try {
            if (parameters.hasProxy() &&
                    (puttyPrivateKeyFiles.isEmpty() || !parameters.samePrivateKeyForProxy() || hasEncryptedPuttyPrivateKeyFile)) {
                if (usePageant) {
                    proxyPuttyPrivateKeyFile = null; //use no key file
                } else {
                    proxyPuttyPrivateKeyFile = parameters.getProxySshKeyInfo().getUnlockedPuttyKeyFile();
                }
            } else
                proxyPuttyPrivateKeyFile = null;
            final TmpFile finalProxyPuttyPrivateKeyFile = proxyPuttyPrivateKeyFile;

            if (finalProxyPuttyPrivateKeyFile != null)
                finalProxyPuttyPrivateKeyFile.store();

            //store proxyOpenSshKey
            if (parameters.hasProxy() && parameters.getProxyOpenSshKeyHostKey() != null)
                storeOpenSshHostKeyToPuttyRegistry(parameters.getProxyOpenSshKeyHostKey(), parameters.getProxyPort());

            //create session, without target details
            String sessionNameHelper;
            if (parameters.hasProxy())
                sessionNameHelper = "jFed-proxy-" + parameters.getProxyHostname().replace(".", "_");
            else
                sessionNameHelper = "jFed-no-proxy"; //won't be usefull to use a session in this case
            final String sessionName = sessionNameHelper;

            TmpFile puttyPrivateKeyFileForSession = puttyPrivateKeyFiles.isEmpty() ? null : puttyPrivateKeyFiles.get(0);
            if (puttyPrivateKeyFiles.size() > 1) {
                LOG.warn("Using PuTTY with multiple keys but without pageant is not possible. Will use first key only!");
            }
            createPuttySession(sessionName,
                    null/*hostname*/, -1/*port*/, null/*puttyPrivateKeyFile*/, null/*username*/,
                    parameters.getProxyHostname(), parameters.getProxyPort(),
                    finalProxyPuttyPrivateKeyFile == null ? (puttyPrivateKeyFileForSession == null ? null
                            : puttyPrivateKeyFileForSession.getFile()) : finalProxyPuttyPrivateKeyFile.getFile(),
                    parameters.getProxyUsername(),
                    parameters.isEnableX11Forward(),
                    parameters.isEnableAgentForward());

            //run session
            Runnable processOutputMonitor = () -> {
                Process p = null;
                try {
                    ArrayList<String> command = new ArrayList<>();
                    command.add(jFedPreferences.getFile(GuiPreferenceKey.PREF_PUTTY_DIRECTORY).getPath() + File.separator + "putty.exe");

                    command.add("-load");
                    command.add(sessionName);

                    command.add(parameters.getUsername() + "@" + parameters.getHostname());
                    command.add("-P");
                    command.add(Integer.toString(parameters.getPort()));

                    if (parameters.getSshKeyInfos().size() > 1) {
                        LOG.warn("Will add multiple -i options to PuTTY. PuTTY will probably not support this!");
                    }
                    for (PuTTYFilesKeyInfo keyInfo : parameters.getSshKeyInfos()) {
                        TmpFile puttyPrivateKeyFile =
                                keyInfo == null ? null : keyInfo.getPuttyKeyFile();
                        if (puttyPrivateKeyFile != null) {
                            puttyPrivateKeyFile.store();
                            puttyPrivateKeyFiles.add(puttyPrivateKeyFile);
                            command.add("-i");
                            command.add(puttyPrivateKeyFile.getFilename());
                        }
                    }

                    LOG.debug("Starting PuTTY and waiting for it to stop");
                    LOG.info("PuTTY command: " + command);
                    p = Runtime.getRuntime().exec(command.toArray(new String[command.size()]));
                    BufferedReader input = new BufferedReader(new InputStreamReader(p.getInputStream()));
                    String line = null;
                    while ((line = input.readLine()) != null) {
                        LOG.debug("putty.exe output: " + line);
                    }
                    input.close();
                    try {
                        p.waitFor();
                    } catch (InterruptedException e) {
                        //ignore
                    }
                    LOG.debug("PuTTY stopped");
                } catch (IOException e) {
                    LOG.error("IOException while trying to launch external terminal: " + e.getMessage(), e);
                } finally {
                    LOG.debug("removing unencrypted putty key files");
                    puttyPrivateKeyFiles.stream().filter(Objects::nonNull).forEach(TmpFile::delete);
                    if (finalProxyPuttyPrivateKeyFile != null) finalProxyPuttyPrivateKeyFile.delete();
                }
            };
            Thread t = new Thread(processOutputMonitor);
            t.setName("PuTTY-Thread");
            t.setDaemon(true); //=> this application may exit before putty stops
            t.start();
            LOG.debug("Started PuTTY thread.");
        } catch (IOException e) {
            puttyPrivateKeyFiles.stream().filter(Objects::nonNull).forEach(TmpFile::delete);
            if (proxyPuttyPrivateKeyFile != null) proxyPuttyPrivateKeyFile.delete();
        }
    }

    /**
     * supports only without proxy
     * <p>
     * Specifies target details on command line.
     * Specifies NO proxy details
     */
    public void runPuttyWithoutSession(final PuttyRunParameters parameters) {
        LOG.debug("Starting PuTTY without session.");
        final boolean usePageant = jFedPreferences.getBoolean(GuiPreferenceKey.PREF_SSHAGENT_USE, true);
        assert !parameters.hasProxy();

        if (usePageant) {
            pageantHelper.registerKeys(parameters.getSshKeyInfos(), sshKeyInfoUnlocker);
        }

        Runnable processOutputMonitor = () -> {
            Process p = null;
            List<TmpFile> puttyPrivateKeyFiles = new ArrayList<>();
            try {
                ArrayList<String> command = new ArrayList<>();
                command.add(jFedPreferences.getFile(GuiPreferenceKey.PREF_PUTTY_DIRECTORY).getPath() + File.separator + "putty.exe");

                command.add("-v");
                command.add("-ssh");
                command.add(parameters.getUsername() + "@" + parameters.getHostname());
                command.add("-P");
                command.add(Integer.toString(parameters.getPort()));
                if (parameters.isEnableX11Forward())
                    command.add("-X");
                if (parameters.isEnableAgentForward())
                    command.add("-A");

                if (!usePageant) {
                    if (parameters.getSshKeyInfos().size() > 1) {
                        LOG.warn("Will add multiple -i options to PuTTY. PuTTY will probably not support this!");
                    }
                    for (PuTTYFilesKeyInfo keyInfo : parameters.getSshKeyInfos()) {
                        TmpFile puttyPrivateKeyFile =
                                keyInfo == null ? null : keyInfo.getPuttyKeyFile();
                        if (puttyPrivateKeyFile != null) {
                            puttyPrivateKeyFile.store();
                            puttyPrivateKeyFiles.add(puttyPrivateKeyFile);
                            command.add("-i");
                            command.add(puttyPrivateKeyFile.getFilename());
                        }
                    }
                }

                LOG.debug("Starting PuTTY and waiting for it to stop");
                LOG.info("PuTTY command: " + command);
                p = Runtime.getRuntime().exec(command.toArray(new String[command.size()]));
                BufferedReader input = new BufferedReader(new InputStreamReader(p.getInputStream()));
                String line = null;
                while ((line = input.readLine()) != null) {
                    LOG.debug("putty.exe output: " + line);
                }
                input.close();
                try {
                    p.waitFor();
                } catch (InterruptedException e) {
                    //ignore
                }
                LOG.debug("PuTTY stopped");
            } catch (IOException e) {
                LOG.error("IOException while trying to launch external terminal: " + e.getMessage(), e);
            } finally {
                LOG.debug("removing unencrypted putty key files");
                for (TmpFile puttyPrivateKeyFile : puttyPrivateKeyFiles) {
                    if (puttyPrivateKeyFile != null) puttyPrivateKeyFile.delete();
                }
            }
        };
        Thread t = new Thread(processOutputMonitor);
        t.setName("PuTTY-Thread");
        t.setDaemon(true); //=> this application may exit before putty stops
        t.start();
        LOG.debug("Started PuTTY thread.");
    }

    public static class PuttyRunParameters {
        private String hostname;
        private int port;
        private List<? extends PuTTYFilesKeyInfo> sshKeyInfos;
        private String username;

        private String proxyHostname;
        private int proxyPort;
        private PuTTYFilesKeyInfo proxySshKeyInfo;
        private String proxyUsername;

        private String proxyOpenSshKeyHostKey;

        public boolean enableX11Forward = false;
        public boolean enableAgentForward = true;

        public String getHostname() {
            return hostname;
        }

        public int getPort() {
            return port;
        }

        public List<? extends PuTTYFilesKeyInfo> getSshKeyInfos() {
            return sshKeyInfos;
        }

        public String getUsername() {
            return username;
        }

        public String getProxyHostname() {
            return proxyHostname;
        }

        public int getProxyPort() {
            return proxyPort;
        }

        public PuTTYFilesKeyInfo getProxySshKeyInfo() {
            return proxySshKeyInfo;
        }

        public String getProxyUsername() {
            return proxyUsername;
        }

        public String getProxyOpenSshKeyHostKey() {
            return proxyOpenSshKeyHostKey;
        }


        public boolean hasProxy() {
            return proxyHostname != null;
        }

        public boolean samePrivateKeyForProxy() {
            if (proxySshKeyInfo == null) return false;
            if (sshKeyInfos == null) return false;
            for (PuTTYFilesKeyInfo sshKeyInfo : sshKeyInfos) {
                if (sshKeyInfo == null) continue;
                if (sshKeyInfo.getPrivateKey() == null) continue;
                if (Objects.equals(sshKeyInfo.getPrivateKey(), proxySshKeyInfo.getPrivateKey()))
                    return true;
            }
            return false;
        }


        public void setTarget(String hostname, int port, List<? extends PuTTYFilesKeyInfo> sshKeyInfos, String username) {
            if (hostname != null && ipv6pat.matcher(hostname).matches()) {
                //enclose IPv6 literal with square brackets to make it work
                this.hostname = "[" + hostname + "]";
            } else
                this.hostname = hostname;
            this.port = port;
            this.sshKeyInfos = sshKeyInfos;
            this.username = username;
        }

        public void setProxy(JFedConnection.ProxyInfo proxyInfo) {
            if (proxyInfo == null) {
                this.proxyHostname = null;
                this.proxyPort = -1;
                this.proxySshKeyInfo = null;
                this.proxyUsername = null;
                this.proxyOpenSshKeyHostKey = null;
            } else {
                assert proxyInfo instanceof JFedConnection.SshProxyInfo;
                JFedConnection.SshProxyInfo sshProxyInfo = (JFedConnection.SshProxyInfo) proxyInfo;
                if (sshProxyInfo.getHostname() != null && ipv6pat.matcher(sshProxyInfo.getHostname()).matches()) {
                    //enclose IPv6 literal with square brackets to make it work
                    this.proxyHostname = "[" + sshProxyInfo.getHostname() + "]";
                } else
                    this.proxyHostname = sshProxyInfo.getHostname();
                this.proxyPort = sshProxyInfo.getPort();

                if (sshProxyInfo.getSshKeyInfo() instanceof PuTTYFilesKeyInfo) {
                    this.proxySshKeyInfo = (PuTTYFilesKeyInfo) sshProxyInfo.getSshKeyInfo();
                } else if (sshProxyInfo.getSshKeyInfo() instanceof GeniUserSshKeyInfo) {
                    //TODO: remove this workaround!
                    final be.iminds.ilabt.jfed.lowlevel.GeniUser geniUser =
                            ((GeniUserSshKeyInfo) sshProxyInfo.getSshKeyInfo()).getGeniUser();
                    this.proxySshKeyInfo =
                            SshKeyInfoFactory.createGeniUserSshKeyInfo(geniUser);
                } else if (sshProxyInfo.getSshKeyInfo() != null) {
                    throw new RuntimeException("Unsupported type of SshKeyInfo!");
                } else {
                    LOG.warn("sshProxyInfo is null!");
                }
                this.proxyUsername = sshProxyInfo.getUsername();
                this.proxyOpenSshKeyHostKey = sshProxyInfo.getHostKey();
            }
        }

        public void setEnableX11Forward(boolean enableX11Forward) {
            this.enableX11Forward = enableX11Forward;
        }

        public boolean isEnableX11Forward() {
            return enableX11Forward;
        }

        public void setEnableAgentForward(boolean enableAgentForward) {
            this.enableAgentForward = enableAgentForward;
        }

        public boolean isEnableAgentForward() {
            return enableAgentForward;
        }
    }
}
