package com.datapipe.jenkins.vault.util;

import com.bettercloud.vault.Vault;
import com.bettercloud.vault.VaultConfig;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.shaded.org.apache.commons.lang.SystemUtils;
import org.testcontainers.utility.PathUtils;
import org.testcontainers.utility.TestEnvironment;
import org.testcontainers.vault.VaultContainer;

import static com.github.dockerjava.api.model.Capability.IPC_LOCK;
import static org.apache.commons.io.FileUtils.writeStringToFile;
import static org.testcontainers.utility.MountableFile.forHostPath;

@SuppressWarnings("WeakerAccess")
public class VaultTestUtil implements TestConstants {

    private final static Logger LOGGER = Logger.getLogger(VaultTestUtil.class.getName());


    private static boolean configured = false;
    private static Network network = Network.newNetwork();

    public static void runCommand(VaultContainer container, final String... command)
        throws IOException, InterruptedException {
        LOGGER.log(Level.FINE, String.join(" ", command));
        container.execInContainer(command);
    }

    public static boolean hasDockerDaemon() {
        try {
            return TestEnvironment.dockerApiAtLeast("1.10");
        } catch (IllegalStateException e) {
            return false;
        }
    }

    public static VaultContainer createVaultContainer() {
        if (!hasDockerDaemon()) {
            return null;
        }
        String path = getTestPath("vaultTest_adminPolicy.hcl");
        return new VaultContainer<>(VaultTestUtil.VAULT_DOCKER_IMAGE)
            .withVaultToken(VaultTestUtil.VAULT_ROOT_TOKEN)
            .withNetwork(network)
            .withNetworkAliases("vault")
            .withCopyFileToContainer(forHostPath(
                path),
                "/admin.hcl")
            .withExposedPorts(8200)
            .waitingFor(Wait.forHttp("/v1/sys/seal-status").forStatusCode(200));
    }

    public static String getTestPath(String resource) {
        URI uri = null;
        try {
            uri = VaultTestUtil.class.getResource(resource).toURI();
        } catch (URISyntaxException e) {
            // ignored
        }
        if (uri == null) return "FileNotFound"; // Should never happen.
        String result = Paths.get(uri).toFile().getAbsoluteFile().toString();
        if (SystemUtils.IS_OS_WINDOWS && result.startsWith("/")) {
            result = PathUtils.createMinGWPath(result).substring(1);
        }
        return result;
    }

    public static VaultContainer createVaultAgentContainer(
        Path roleIDPath,
        Path secretIDPath) {
        return new VaultContainer<>("vault:1.2.1")
            .withNetwork(network)
            .withCreateContainerCmdModifier(cmd -> cmd.withCapAdd(IPC_LOCK))
            .withCopyFileToContainer(forHostPath(
                getTestPath("vaultTest_agent.hcl")),
                "/agent.hcl")
            .withCopyFileToContainer(forHostPath(roleIDPath),
                "/home/vault/role_id")
            .withCopyFileToContainer(forHostPath(secretIDPath),
                "/home/vault/secret_id")
            .withCommand("vault agent -config=/agent.hcl -address=http://vault:8200")
            .withExposedPorts(8200)
            .waitingFor(Wait.forLogMessage(".*renewed auth token.*", 1));
    }

    public static String getAddress(VaultContainer container) {
        return String.format("http://%s:%d", container.getContainerIpAddress(), container.getMappedPort(8200));
    }

    public static void configureVaultContainer(VaultContainer container) {
        if (configured) {
            return;
        }
        try {
            // Create Secret Backends
            runCommand(container, "vault", "secrets", "enable", "-path=kv-v2",
                "-version=2", "kv");
            runCommand(container, "vault", "secrets", "enable", "-path=long/path/kv-v2",
                "-version=2", "kv");
            runCommand(container, "vault", "secrets", "enable", "-path=kv-v1",
                "-version=1", "kv");

            // Create user/password credential
            runCommand(container, "vault", "auth", "enable", "userpass");
            runCommand(container, "vault", "write", "auth/userpass/users/" + VAULT_USER,
                "password=" + VAULT_PW, "policies=admin");

            // Create policies
            runCommand(container, "vault", "policy", "write", "admin", "/admin.hcl");

            // Create AppRole
            runCommand(container, "vault", "auth", "enable", "approle");
            runCommand(container, "vault", "write", "auth/approle/role/admin",
                "secret_id_ttl=10m", "token_num_uses=0", "token_ttl=50ms", "token_max_ttl=50ms",
                "secret_id_num_uses=1000", "policies=admin");

            // Retrieve AppRole credentials
            VaultConfig config = new VaultConfig().address(getAddress(container))
                .token(VAULT_ROOT_TOKEN).engineVersion(1).build();
            Vault vaultClient = new Vault(config);
            final String roleID = vaultClient.logical().read("auth/approle/role/admin/role-id")
                .getData().get("role_id");
            final String secretID = vaultClient.logical().write("auth/approle/role/admin/secret-id",
                new HashMap<>()).getData().get("secret_id");

            Properties properties = new Properties();
            properties.put("CASC_VAULT_APPROLE", roleID);
            properties.put("CASC_VAULT_APPROLE_SECRET", secretID);

            Path filePath = Paths.get(System.getProperty("java.io.tmpdir"), VAULT_APPROLE_FILE);
            File file = filePath.toFile();
            FileOutputStream fos = new FileOutputStream(file);
            properties.store(fos, null);
            Path roleIDPath = Paths.get(System.getProperty("java.io.tmpdir"), "role_id");
            Path secretIDPath = Paths.get(System.getProperty("java.io.tmpdir"), "secret_id");
            writeStringToFile(roleIDPath.toFile(), roleID);
            writeStringToFile(secretIDPath.toFile(), secretID);

            // add secrets for v1 and v2
            runCommand(container, "vault", "kv", "put", VAULT_PATH_KV1_1, "key1=123",
                "key2=456");
            runCommand(container, "vault", "kv", "put", VAULT_PATH_KV1_2, "key3=789");
            runCommand(container, "vault", "kv", "put", VAULT_PATH_KV2_1, "key1=123",
                "key2=456");
            runCommand(container, "vault", "kv", "put", VAULT_PATH_KV2_2, "key3=789");
            runCommand(container, "vault", "kv", "put", VAULT_PATH_KV2_3, "key2=321");
            runCommand(container, "vault", "kv", "put", VAULT_PATH_KV2_AUTH_TEST,
                "key1=auth-test");
            runCommand(container, "vault", "kv", "put", VAULT_PATH_LONG_KV2_1, "key1=123",
                "key2=456");
            runCommand(container, "vault", "kv", "put", VAULT_PATH_LONG_KV2_2, "key3=789");
            VaultContainer vaultAgentContainer = createVaultAgentContainer(roleIDPath,
                secretIDPath);
            assert vaultAgentContainer != null;
            vaultAgentContainer.start();
            filePath = Paths.get(System.getProperty("java.io.tmpdir"), VAULT_AGENT_FILE);
            file = filePath.toFile();
            fos = new FileOutputStream(file);
            properties.clear();
            properties.put("CASC_VAULT_AGENT_ADDR", getAddress(vaultAgentContainer));
            properties.store(fos, null);
            LOGGER.log(Level.INFO, "Vault is configured");
            configured = true;
        } catch (Exception e) {
            LOGGER.log(Level.WARNING, e.getMessage());
        }
    }
}
