package org.arquillian.cube.docker.impl.util;

import java.util.Collection;
import java.util.Map;
import java.util.Set;
import org.arquillian.cube.docker.impl.client.CubeDockerConfiguration;
import org.arquillian.cube.docker.impl.client.config.CubeContainer;
import org.arquillian.cube.docker.impl.client.config.DockerCompositions;
import org.arquillian.cube.docker.impl.client.config.PortBinding;

/**
 * Utility class that helps on autoresolution of binding ports in case of running Arquillian as client mode and not using
 * enrichments for getting the host/bind port of exposed one.
 * <p>
 * Usually this class is used by extensions that needs autoresolution.
 */
public class SinglePortBindResolver {

    private static final int NO_PORT = -1;

    private SinglePortBindResolver() {
        super();
    }

    /**
     * Method that tries to resolve a bind port for a given exposed port.
     *
     * @param cubeDockerConfiguration
     *     where all docker configuration is exposed
     * @param exposedPort
     *     used to find the binding port
     * @param excludedContainers
     *     where binding port search is ignored
     *
     * @return binding port
     */
    public static int resolveBindPort(CubeDockerConfiguration cubeDockerConfiguration, int exposedPort,
        String... excludedContainers) {
        final PortBindInfo portBinding = resolvePortBindPort(cubeDockerConfiguration, exposedPort, excludedContainers);

        if (portBinding == null) {
            return exposedPort;
        }

        return portBinding.getBindPort();
    }

    /**
     * Method that tries to resolve a bind port for a given exposed port.
     *
     * @param cubeDockerConfiguration
     *     where all docker configuration is exposed
     * @param exposedPort
     *     used to find the binding port
     * @param excludedContainers
     *     where binding port search is ignored
     *
     * @return binding port or null if couldn't be found
     */
    public static PortBindInfo resolvePortBindPort(CubeDockerConfiguration cubeDockerConfiguration, int exposedPort,
        String... excludedContainers) {

        final DockerCompositions dockerContainersContent = cubeDockerConfiguration.getDockerContainersContent();
        final Set<Map.Entry<String, CubeContainer>> containers = dockerContainersContent.getContainers().entrySet();

        // user specified an exposed port
        PortBindInfo portBindInfo = null;
        for (Map.Entry<String, CubeContainer> cubeContainerEntry : containers) {

            // need to skip vnc and selenium container
            if (shouldBeIgnored(cubeContainerEntry.getKey(), excludedContainers)) {
                continue;
            }

            final CubeContainer cubeContainer = cubeContainerEntry.getValue();
            final Collection<PortBinding> portBindings = cubeContainer.getPortBindings();
            if (portBindings != null) {
                for (PortBinding portBinding : portBindings) {
                    if (portBinding.getExposedPort().getExposed() == exposedPort) {
                        if (noPreviousBindPortFound(portBindInfo)) {
                            int bindPort = portBinding.getBound();
                            portBindInfo = new PortBindInfo(
                                portBinding.getExposedPort().getExposed(),
                                bindPort, cubeContainerEntry.getKey());
                        } else {
                            throw new IllegalArgumentException(
                                String.format("More than one docker container with port binding having exposed port %s.",
                                    exposedPort));
                        }
                    }
                }
            }

            if (noPreviousBindPortFound(portBindInfo)) {
                return null;
            }
        }
        return portBindInfo;
    }

    /**
     * Method that tries to resolve a bind port by searching if there is only one binding port across all running
     * containers
     *
     * @param cubeDockerConfiguration
     *     where all docker configuration is exposed
     * @param excludedContainers
     *     where binding port search is ignored
     *
     * @return binding port
     */
    public static int resolveBindPort(CubeDockerConfiguration cubeDockerConfiguration, String... excludedContainers) {
        final PortBindInfo portBinding = resolvePortBindPort(cubeDockerConfiguration, excludedContainers);

        if (portBinding == null) {
            throw new IllegalArgumentException("There isn't any bind port.");
        }
        return portBinding.getBindPort();
    }

    /**
     * Method that tries to resolve a bind port by searching if there is only one binding port across all running
     * containers
     *
     * @param cubeDockerConfiguration
     *     where all docker configuration is exposed
     * @param excludedContainers
     *     where binding port search is ignored
     *
     * @return binding port
     */
    public static PortBindInfo resolvePortBindPort(CubeDockerConfiguration cubeDockerConfiguration,
        String... excludedContainers) {

        final DockerCompositions dockerContainersContent = cubeDockerConfiguration.getDockerContainersContent();
        final Set<Map.Entry<String, CubeContainer>> containers = dockerContainersContent.getContainers().entrySet();

        //if no port, we check if there is only one cube with one bind port and if not, return default one

        PortBindInfo portBindInfo = null;
        for (Map.Entry<String, CubeContainer> cubeContainerEntry : containers) {

            // need to skip excluded containers
            if (shouldBeIgnored(cubeContainerEntry.getKey(), excludedContainers)) {
                continue;
            }

            final CubeContainer cubeContainer = cubeContainerEntry.getValue();
            if (hasMoreThanOneBindPort(cubeContainer)) {
                throw new IllegalArgumentException("No port was specified and a container has more than one bind port.");
            }

            if (hasOnlyOneBindPort(cubeContainer)) {
                if (noPreviousBindPortFound(portBindInfo)) {
                    final PortBinding portBinding = cubeContainer.getPortBindings()
                        .iterator().next();
                    int bindPort = portBinding.getBound();
                    int exposedPort = portBinding.getExposedPort().getExposed();
                    portBindInfo = new PortBindInfo(exposedPort, bindPort, cubeContainerEntry.getKey());
                } else {
                    throw new IllegalArgumentException(
                        "No port was specified and in all containers there are more than one bind port.");
                }
            }
        }

        if (noPreviousBindPortFound(portBindInfo)) {
            throw new IllegalArgumentException("There isn't any bind port.");
        }

        return portBindInfo;
    }

    private static boolean shouldBeIgnored(String containerId, String... excludedContainers) {
        for (String excludedContainer : excludedContainers) {
            if (excludedContainer.equals(containerId)) {
                return true;
            }
        }

        return false;
    }

    private static boolean noPreviousBindPortFound(PortBindInfo bindPort) {
        return bindPort == null;
    }

    private static boolean hasOnlyOneBindPort(CubeContainer cubeContainer) {

        final Collection<PortBinding> portBindings = cubeContainer.getPortBindings();
        if (portBindings == null) {
            return false;
        }

        return portBindings.size() == 1;
    }

    private static boolean hasMoreThanOneBindPort(CubeContainer cubeContainer) {

        final Collection<PortBinding> portBindings = cubeContainer.getPortBindings();
        if (portBindings == null) {
            return false;
        }

        return portBindings.size() > 1;
    }

    public static class PortBindInfo {
        private int bindPort;
        private int exposedPort;
        private String containerName;

        public PortBindInfo(int exposedPort, int bindPort, String containerName) {
            this.exposedPort = exposedPort;
            this.bindPort = bindPort;
            this.containerName = containerName;
        }

        public int getBindPort() {
            return bindPort;
        }

        public int getExposedPort() {
            return exposedPort;
        }

        public String getContainerName() {
            return containerName;
        }
    }
}
