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

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFilePermission;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import org.arquillian.cube.docker.impl.client.config.Await;
import org.arquillian.cube.docker.impl.docker.DockerClientExecutor;
import org.arquillian.cube.docker.impl.util.Ping;
import org.arquillian.cube.impl.util.IOUtil;
import org.arquillian.cube.spi.Cube;
import org.arquillian.cube.spi.CubeOutput;
import org.arquillian.cube.spi.metadata.HasPortBindings;
import org.arquillian.cube.spi.metadata.HasPortBindings.PortAddress;

public class PollingAwaitStrategy extends SleepingAwaitStrategyBase {

    public static final String TAG = "polling";
    public static final String CONTAINER_DIRECTORY = "/tmp";
    private static final Logger log = Logger.getLogger(PollingAwaitStrategy.class.getName());
    private static final String MESSAGE = "Service is Up";
    private static final String WAIT_FOR_IT_SCRIPT = "wait-for-it.sh";
    private static final int DEFAULT_POLL_ITERATIONS = 80;
    private static final String DEFAULT_POLL_TYPE = "sscommand";
    private int pollIterations = DEFAULT_POLL_ITERATIONS;
    private String type = DEFAULT_POLL_TYPE;

    private DockerClientExecutor dockerClientExecutor;
    private Cube<?> cube;
    private List<Integer> ports = null;

    // To avoid having to copy the script for all ports, state is saved.
    private boolean alreadyCopiedWaitForIt = false;

    public PollingAwaitStrategy(Cube<?> cube, DockerClientExecutor dockerClientExecutor, Await params) {
        super(params.getSleepPollingTime());

        this.cube = cube;
        this.dockerClientExecutor = dockerClientExecutor;

        if (params.getIterations() != null) {
            this.pollIterations = params.getIterations();
        }

        if (params.getType() != null) {
            this.type = params.getType();
        }

        if (params.getPorts() != null && params.getPorts().size() > 0) {
            this.ports = params.getPorts();
        }
    }

    public int getPollIterations() {
        return pollIterations;
    }

    public String getType() {
        return type;
    }

    public List<Integer> getPorts() {
        return ports;
    }

    @Override
    public boolean await() {
        HasPortBindings portBindings = cube.getMetadata(HasPortBindings.class);
        if (portBindings == null) {
            log.fine("Cube does not have any ports to ping.");
            return true;
        }

        Collection<Integer> pingPorts = this.ports;
        if (ports == null) {
            pingPorts = portBindings.getBoundPorts();
        }
        for (Integer port : pingPorts) {
            switch (this.type) {
                case "ping": {
                    PortAddress mapping = portBindings.getMappedAddress(port);
                    if (mapping == null) {
                        throw new IllegalArgumentException(
                            "Can not use polling of type " + type + " on non externally bound port " + port);
                    }
                    log.fine(String.format("Pinging host %s and port %s with type", mapping.getIP(), mapping.getPort(),
                        this.type));
                    if (!Ping.ping(mapping.getIP(), mapping.getPort(), this.pollIterations, this.getSleepTime(),
                        this.getTimeUnit())) {
                        return false;
                    }
                }

                break;
                case "sscommand": {
                    try {
                        if (!Ping.ping(dockerClientExecutor, cube.getId(), resolveCommand("ss", port),
                            this.pollIterations, this.getSleepTime(), this.getTimeUnit())) {
                            return false;
                        }
                    } catch (UnsupportedOperationException e) {
                        // In case of not having ss command installed on container, it automatically fall back to waitforit approach
                        try {
                            if (!executeWaitForIt(getReadyCheckIp(portBindings), port)) {
                                return false;
                            }
                        } catch (UnsupportedOperationException ex) {
                            PortAddress mapping = portBindings.getMappedAddress(port);
                            if (mapping == null) {
                                throw new IllegalArgumentException(
                                    "Can not use polling of type " + type + " on non externally bound port " + port);
                            }
                            log.fine(
                                String.format("Pinging host %s and port %s with type", mapping.getIP(), mapping.getPort(),
                                    this.type));
                            if (!Ping.ping(mapping.getIP(), mapping.getPort(), this.pollIterations, this.getSleepTime(),
                                this.getTimeUnit())) {
                                return false;
                            }
                        }
                    }
                }
                break;
                case "waitforit": {
                    if (!executeWaitForIt(getReadyCheckIp(portBindings), port)) {
                        return false;
                    }
                }
            }
        }

        return true;
    }

    private String getReadyCheckIp(HasPortBindings portBindings) {
        String ip = portBindings.getInternalIP();
        if (ip == null || "".equals(ip)) {
            ip = portBindings.getContainerIP();
        }
        if (ip == null || "".equals(ip)) {
            ip = "localhost";
        }
        return ip;
    }

    private boolean executeWaitForIt(String containerIp, int port) {

        // We copy our wait-for-it-sh.sh file form classpath
        // Our wait-for-it-sh.sh script also works with busybox/alpine which is not true with the official one.

        try {

            if (!alreadyCopiedWaitForIt) {
                final Path waitForItLocation = copyWaitForItScriptToTempDir();
                // Then copy this into the container
                dockerClientExecutor.copyStreamToContainer(cube.getId(), waitForItLocation.toFile(),
                    new File(CONTAINER_DIRECTORY));
                alreadyCopiedWaitForIt = true;
            }

            String command = resolveWaitForItCommand(containerIp, port);
            final String[] commands = {"sh", "-c", command};
            CubeOutput result = dockerClientExecutor.execStart(cube.getId(), commands);

            if (result.getError() != null && result.getError().contains("can't execute")) {
                throw new UnsupportedOperationException(result.getError());
            }

            return result.getStandard() != null && result.getStandard().trim().contains(MESSAGE);
        } catch (IOException e) {
            throw new IllegalArgumentException(e);
        }
    }

    private Path copyWaitForItScriptToTempDir() throws IOException {
        final Path arquilliancube = Files.createTempDirectory("arquilliancube");
        final Path waitForItLocation = arquilliancube.resolve(Paths.get(WAIT_FOR_IT_SCRIPT));
        Files.copy(
            PollingAwaitStrategy.class.getResourceAsStream("/org/arquillian/cube/docker/impl/await/wait-for-it.sh"),
            waitForItLocation);
        Files.setPosixFilePermissions(waitForItLocation, getScriptPermissions());

        return waitForItLocation;
    }

    private Set<PosixFilePermission> getScriptPermissions() {

        final PosixFilePermission ownerExecute = PosixFilePermission.OWNER_EXECUTE;
        final PosixFilePermission groupExecute = PosixFilePermission.GROUP_EXECUTE;
        final PosixFilePermission othersExecute = PosixFilePermission.OTHERS_EXECUTE;
        final PosixFilePermission ownerRead = PosixFilePermission.OWNER_READ;
        final PosixFilePermission groupRead = PosixFilePermission.GROUP_READ;
        final PosixFilePermission othersRead = PosixFilePermission.OTHERS_READ;

        final Set<PosixFilePermission> perms = new HashSet<>();
        perms.addAll(Arrays.asList(
            ownerExecute, ownerRead,
            groupExecute, groupRead,
            othersExecute, othersRead
            )
        );

        return perms;
    }

    private String resolveWaitForItCommand(String containerIp, int port) {
        return String.format("%s/%s %s:%s -s -- echo %s", CONTAINER_DIRECTORY, WAIT_FOR_IT_SCRIPT, containerIp, port,
            MESSAGE);
    }

    private String resolveCommand(String command, int port) {
        Map<String, String> values = new HashMap<String, String>();
        values.put("port", Integer.toString(port));
        String templateContent =
            IOUtil.asStringPreservingNewLines(PollingAwaitStrategy.class.getResourceAsStream(command + ".sh"));
        return IOUtil.replacePlaceholders(templateContent, values);
    }
}
