package be.iminds.ilabt.jfed.connectivity_tester;


import be.iminds.ilabt.jfed.lowlevel.TestbedInfoSource;
import be.iminds.ilabt.jfed.lowlevel.connection.JFedConnection;
import be.iminds.ilabt.jfed.util.*;
import org.slf4j.LoggerFactory;

import javax.net.ssl.*;
import java.io.IOException;
import java.net.*;
import java.security.cert.X509Certificate;

public class SslHostAndPortTest implements ConnectivityTest {
    private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(SslHostAndPortTest.class);

    private final JFedConnection.ProxyInfo proxyInfo;
    private final String hostname;
    private final int port;

    private final String description;
    private final String category;


    private final TestbedInfoSource testbedInfoSource;

    public SslHostAndPortTest(String hostname, int port, String description, String category, JFedConnection.ProxyInfo proxyInfo, TestbedInfoSource testbedInfoSource) {
        super();
        this.hostname = hostname;
        this.port = port;
        this.proxyInfo = proxyInfo;

        this.description = description;
        this.category = category;
        this.testbedInfoSource = testbedInfoSource;
    }

    public SslHostAndPortTest(String hostname, int port, String description, String category, TestbedInfoSource testbedInfoSource) {
        this(hostname, port, description, category, null, testbedInfoSource);

    }


    public SslHostAndPortTest(URL url, String description, String category, JFedConnection.ProxyInfo proxyInfo, TestbedInfoSource testbedInfoSource) {
        this(url.getHost(), url.getPort() != -1 ? url.getPort() : url.getDefaultPort(),
                description, category, proxyInfo, testbedInfoSource);
    }

    public SslHostAndPortTest(URL url, String description, String category, TestbedInfoSource testbedInfoSource) {
        this(url, description, category, null, testbedInfoSource);
    }


    @Override
    public String getName() {
        return String.format("[%s] %s", category, description);
    }

    @Override
    public boolean isLoginRequired() {
        return false;
    }

    public static JFedTrustStore allTrustStore = null;

    @Override
    public ConnectivityTestResult call() throws Exception {
        if (allTrustStore == null)
            allTrustStore = new JFedTrustStore(testbedInfoSource);

        SSLSocket socket = null;
        try {
            SSLContext context = SSLContext.getInstance("TLS");
            TrustManagerFactory tmf =
                    TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            tmf.init(allTrustStore.getTrustStore());
            X509TrustManager defaultTrustManager = (X509TrustManager) tmf.getTrustManagers()[0];
            SSLCertificateDownloader.SavingTrustManager tm =
                    new SSLCertificateDownloader.SavingTrustManager(defaultTrustManager); //wil cause java.lang.UnsupportedOperationException
            context.init(null, new TrustManager[]{tm}, null);
            SSLSocketFactory factory = context.getSocketFactory();

            if (proxyInfo != null) {
                Socket underlying = null;
                if (proxyInfo instanceof JFedConnection.SshProxyInfo) {
                    SshProxySocketImplFactory sshHSocketImplFactory =
                            SshProxySocketImplFactory.getFactory((JFedConnection.SshProxyInfo) proxyInfo);

                    underlying = new SSHSocket(sshHSocketImplFactory);
                } else {
                    Proxy proxy =
                            new Proxy(Proxy.Type.SOCKS, new InetSocketAddress(proxyInfo.getHostname(), proxyInfo.getPort()));
                    underlying = new Socket(proxy);
                }
                underlying.connect(new InetSocketAddress(hostname, port), 10000);
                socket =
                        (SSLSocket) factory.createSocket(underlying, proxyInfo.getHostname(), proxyInfo.getPort(), true);
            } else
                socket = (SSLSocket) factory.createSocket(hostname, port);
            socket.setSoTimeout(10000); //doesn't seem to work on MAC
            Throwable ex = null;
            try {
                socket.startHandshake();
            } catch (SSLException e) {
                if (e.getCause() != null && e.getCause() instanceof UnsupportedOperationException) {
                    //this is expected.
                    LOG.trace("SslHostAndPortTest socket.startHandshake() got UnsupportedOperationException as was expected. (=all is fine)", e);
                    ex = null;
                } else {
                    ex = e;
                    LOG.debug("SslHostAndPortTest socket.startHandshake() got unexpected exception: " + e.getMessage(), e);
                }
            } finally {
                try {
                    socket.close();
                } catch (IOException e) {
                    //ignore
                }
                socket = null;
            }

            X509Certificate[] chain = tm.chain;
            if (chain == null) {
                return new ConnectivityTestResult(Status.FAILED, "Connection succeeded, but failed to fetch SSL certificate chain @ " + hostname + ":" + port, ex);

            }

            //connectivity ok
            return new ConnectivityTestResult(Status.SUCCEEDED, "Successfully did SSL handshake with '" + hostname + "'", ex);
        } catch (Exception e) {
            return new ConnectivityTestResult(Status.FAILED, "Failed to fetch SSL certificate @ " + hostname + ":" + port + "  msg:" + e.getMessage(), e);
        } finally {
            if (socket != null)
                try {
                    socket.close();
                } catch (IOException ignored) {
                }
        }
    }

    private static class SSHSocket extends Socket {
        public SSHSocket(SshProxySocketImplFactory sshProxySocketImplFactory) throws SocketException {
            super(sshProxySocketImplFactory.createSocketImpl());
        }
    }
}
