package be.iminds.ilabt.jfed.connectivity_tester;

import be.iminds.ilabt.jfed.lowlevel.connection.JFedConnection;
import be.iminds.ilabt.jfed.util.SshProxySocketImplFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xbill.DNS.Address;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.net.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * User: twalcari
 * Date: 12/23/13
 * Time: 10:20 AM
 */
public class HostAndPortTest implements ConnectivityTest {
    private static final Logger LOG = LoggerFactory.getLogger(HostAndPortTest.class);

    private static final int TIMEOUT = 5000; //5 seconds
    private final JFedConnection.ProxyInfo proxyInfo;

    @Nonnull
    private final String hostname;
    private final int port;

    private final String description;
    private final String category;

    static {
        java.security.Security.setProperty("networkaddress.cache.ttl", "10");
    }


    public HostAndPortTest(@Nonnull String hostname, int port, String description, String category) {
        this(hostname, port, description, category, null);
    }

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

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


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

    public HostAndPortTest(URL url, String description, String category) {
        this(url, description, category, null);
    }

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

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

    @Override
    public ConnectivityTestResult call() {
        InetAddress[] inetAddresses;
        try {
            inetAddresses = Address.getAllByName(hostname);
        } catch (UnknownHostException e) {
            LOG.trace("Got UnknownHostException when trying to reach " + hostname, e);
            return new ConnectivityTestResult(Status.FAILED, e);
        }

        List<String> errorMessages = new ArrayList<>();
        Throwable lastError = null;

        for (InetAddress inetAddress : inetAddresses) {
            LOG.trace("Trying to connect using InetAddress {} and port {} to host {}", inetAddress, port, hostname);

            SocketAddress socketAddress = new InetSocketAddress(inetAddress, port);

            Socket socket = null;
            try {
                if (proxyInfo != null) {
                    if (proxyInfo instanceof JFedConnection.SshProxyInfo) {

                        JFedConnection.SshProxyInfo sshProxyInfo = (JFedConnection.SshProxyInfo) proxyInfo;

                        SshProxySocketImplFactory sshHSocketImplFactory =
                                SshProxySocketImplFactory.getFactory(sshProxyInfo);

                        socket = new SSHSocket(sshHSocketImplFactory);
                    } else {
                        Proxy proxy =
                                new Proxy(Proxy.Type.SOCKS, new InetSocketAddress(proxyInfo.getHostname(), proxyInfo.getPort()));
                        socket = new Socket(proxy);
                    }
                } else {
                    socket = new Socket();
                }

                //try to connect
                socket.connect(socketAddress, TIMEOUT);

                if (socket.isConnected()) {
                    LOG.trace("Could successfully connect to {}:{}", hostname, port);
                    return new ConnectivityTestResult(Status.SUCCEEDED, "Successfully opened socket to " + hostname + ":" + port);

                } else {
                    LOG.trace("Could not connect socket when trying to reach " + hostname);
                    errorMessages.add("Socket isn't connected to " + hostname + ":" + port);
                }
            } catch (SocketTimeoutException e) {
                LOG.trace("Socket connection timed out when trying to reach " + hostname, e);
                errorMessages.add("Connection to " + hostname + ":" + port + " timed out");
                lastError = e;
            } catch (IOException e) {
                LOG.trace("Could not open a socket when trying to reach " + hostname, e);
                errorMessages.add("Could not open a socket to " + hostname + ":" + port);
                lastError = e;
            } finally {
                try {
                    if (socket != null)
                        socket.close();
                } catch (IOException ignored) {
                }
            }
        }

        return new ConnectivityTestResult(Status.FAILED, errorMessages.stream().collect(Collectors.joining(", ")), lastError);

    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        HostAndPortTest that = (HostAndPortTest) o;

        if (port != that.port) return false;
        if (category != null ? !Objects.equals(category, that.category) : that.category != null) return false;
        if (description != null ? !Objects.equals(description, that.description) : that.description != null) return false;
        if (!Objects.equals(hostname, that.hostname)) return false;
        if (proxyInfo != null ? !Objects.equals(proxyInfo, that.proxyInfo) : that.proxyInfo != null) return false;

        return true;
    }

    @Override
    public int hashCode() {
        int result = proxyInfo != null ? proxyInfo.hashCode() : 0;
        result = 31 * result + hostname.hashCode();
        result = 31 * result + port;
        result = 31 * result + (description != null ? description.hashCode() : 0);
        result = 31 * result + (category != null ? category.hashCode() : 0);
        return result;
    }

    private static class SSHSocket extends Socket {


        public SSHSocket(SshProxySocketImplFactory sshProxySocketImplFactory) throws SocketException {
            super(sshProxySocketImplFactory.createSocketImpl());
        }
    }
}
