package com.atlassian.bitbucket.installer.validation;

import java.io.IOException;
import java.io.Serializable;
import java.net.InetAddress;
import java.net.ServerSocket;

public class PortValidator implements Serializable {

    static final int LOW_PORT_RANGE_EXC = 0;
    static final int HIGH_PORT_RANGE_INC = 65535;
    static final int SSH_SERVER_PORT = 7999;
    static final int ELASTICSEARCH_HTTP_PORT = 7992;
    static final int ELASTICSEARCH_TRANSPORT_PORT = 7993;
    static final InetAddress LOCAL_ADDRESS = InetAddress.getLoopbackAddress();

    public void validate(int httpPort, int controlPort) {
        validate(httpPort, controlPort, true);
    }

    public void validate(int httpPort, int controlPort, boolean shouldValidateElasticsearch) {
        // If the user chose the same port for HTTP and Server then reject the values
        if (controlPort == httpPort) {
            throw new HardValidationException(HardValidationException.ERROR_PORT_SAME);
        }

        if (httpPort == SSH_SERVER_PORT) {
            throw new HardValidationException(HardValidationException.ERROR_PORT_HTTP_SSH);
        }

        if (controlPort == SSH_SERVER_PORT) {
            throw new HardValidationException(HardValidationException.ERROR_PORT_CONTROL_SSH);
        }

        checkRange(httpPort, controlPort);

        checkFree(httpPort, controlPort);

        if (shouldValidateElasticsearch) {
            if (httpPort == ELASTICSEARCH_HTTP_PORT) {
                throw new HardValidationException(HardValidationException.ERROR_PORT_HTTP_ELASTICSEARCH_HTTP);
            }

            if (controlPort == ELASTICSEARCH_HTTP_PORT) {
                throw new HardValidationException(HardValidationException.ERROR_PORT_CONTROL_ELASTICSEARCH_HTTP);
            }

            if (httpPort == ELASTICSEARCH_TRANSPORT_PORT) {
                throw new HardValidationException(HardValidationException.ERROR_PORT_HTTP_ELASTICSEARCH_TRANSPORT);
            }

            if (controlPort == ELASTICSEARCH_TRANSPORT_PORT) {
                throw new HardValidationException(HardValidationException.ERROR_PORT_CONTROL_ELASTICSEARCH_TRANSPORT);
            }

            checkElasticsearchPortsFree();
        }
    }

    public boolean isPortNumberLow(int httpPort, int controlPort, boolean isLinux) {
        if (isLinux && (httpPort < 1024 || controlPort < 1024)) {
            return true;
        }
        return false;
    }

    private void checkFree(int httpPort, int controlPort) {
        boolean httpPortFree = isFree(httpPort) && isFree(httpPort, LOCAL_ADDRESS);
        boolean controlPortFree = isFree(controlPort) && isFree(controlPort, LOCAL_ADDRESS);

        // If either (or neither) port is free reject the values
        if (!httpPortFree && !controlPortFree) {
            throw new HardValidationException(HardValidationException.ERROR_PORT_USED);
        } else if (!httpPortFree) {
            throw new HardValidationException(HardValidationException.ERROR_PORT_HTTP_USED);
        } else if (!controlPortFree) {
            throw new HardValidationException(HardValidationException.ERROR_PORT_CONTROL_USED);
        }
    }

    private void checkRange(int httpPort, int controlPort) {
        boolean httpPortInRange = isValidPortRange(httpPort);
        boolean controlPortInRange = isValidPortRange(controlPort);

        // If either (or neither) port is in range 1-65535 reject the values
        if (!httpPortInRange && !controlPortInRange) {
            throw new HardValidationException(HardValidationException.ERROR_PORT_RANGE);
        } else if (!httpPortInRange) {
            throw new HardValidationException(HardValidationException.ERROR_PORT_HTTP_RANGE);
        } else if (!controlPortInRange) {
            throw new HardValidationException(HardValidationException.ERROR_PORT_CONTROL_RANGE);
        }
    }

    private void checkElasticsearchPortsFree() {
        boolean elasticsearchHttpPortFree = isFree(ELASTICSEARCH_HTTP_PORT)
                && isFree(ELASTICSEARCH_HTTP_PORT, LOCAL_ADDRESS);
        boolean elasticsearchTransportPortFree = isFree(ELASTICSEARCH_TRANSPORT_PORT)
                && isFree(ELASTICSEARCH_TRANSPORT_PORT, LOCAL_ADDRESS);

        if (!elasticsearchHttpPortFree && !elasticsearchTransportPortFree) {
            throw new SoftValidationException(SoftValidationException.ERROR_PORT_ELASTICSEARCH_USED);
        } else if (!elasticsearchHttpPortFree) {
            throw new SoftValidationException(SoftValidationException.ERROR_PORT_ELASTICSEARCH_HTTP_USED);
        } else if (!elasticsearchTransportPortFree) {
            throw new SoftValidationException(SoftValidationException.ERROR_PORT_ELASTICSEARCH_TRANSPORT_USED);
        }
    }

    private boolean isFree(int port) {
        return isFree(port, null);
    }

    private boolean isFree(int port, InetAddress address) {
        try (ServerSocket serverSocket = address == null ? new ServerSocket(port) : new ServerSocket(port, 0, address)) {
            return serverSocket.isBound();
        } catch (IOException | SecurityException e) {
            return false;
        }
    }

    private boolean isValidPortRange(int port) {
        return (port > LOW_PORT_RANGE_EXC && port <= HIGH_PORT_RANGE_INC);
    }
}
