package cn.reghao.bnt.common.docker;

import cn.reghao.bnt.common.docker.model.DockerAuth;
import cn.reghao.jutil.jdk.exception.ExceptionUtil;
import cn.reghao.jutil.jdk.io.TextFile;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.async.ResultCallback;
import com.github.dockerjava.api.command.*;
import com.github.dockerjava.api.model.*;
import com.github.dockerjava.core.DefaultDockerClientConfig;
import com.github.dockerjava.core.DockerClientConfig;
import com.github.dockerjava.core.DockerClientImpl;
import com.github.dockerjava.httpclient5.ApacheDockerHttpClient;
import com.github.dockerjava.transport.DockerHttpClient;
import lombok.extern.slf4j.Slf4j;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * Docker 客户端
 *
 * @author reghao
 * @date 2021-10-27 03:41:38
 */
@Slf4j
public class DockerImpl implements Docker {
    private DockerClient dockerClient;
    private final TextFile textFile = new TextFile();
    private final String unixSock = "/var/run/docker.sock";

    public DockerImpl() {
        DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder()
                .withDockerHost("unix://" + unixSock)
                .withDockerTlsVerify(false)
                //.withDockerCertPath(String.format("%s/.docker", System.getProperty("user.home")))
                //.withRegistryUsername("username")
                //.withRegistryPassword("password")
                //.withRegistryEmail(registryMail)
                //.withRegistryUrl(registryUrl)
                .build();
        init(config);
    }

    public DockerImpl(DockerAuth dockerAuth) {
        DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder()
                .withDockerHost("unix://" + unixSock)
                .withDockerTlsVerify(false)
                //.withDockerCertPath(String.format("%s/.docker", System.getProperty("user.home")))
                .withRegistryUrl(dockerAuth.getRegistryUrl())
                .withRegistryUsername(dockerAuth.getUsername())
                .withRegistryPassword(dockerAuth.getPassword())
                //.withRegistryEmail(registryMail)
                .build();
        init(config);
    }

    private void init(DockerClientConfig config) {
        DockerHttpClient httpClient = new ApacheDockerHttpClient.Builder()
                .dockerHost(config.getDockerHost())
                .sslConfig(config.getSSLConfig())
                .maxConnections(100)
                .connectionTimeout(Duration.ofSeconds(60))
                .responseTimeout(Duration.ofSeconds(60))
                .build();
        this.dockerClient = DockerClientImpl.getInstance(config, httpClient);
    }

    @Override
    public void auth() {
        dockerClient.authCmd().exec();
    }

    @Override
    public Version version() {
        return dockerClient.versionCmd().exec();
    }

    @Override
    public void build(String repoTag, String compileHome, String dockerfileContent) throws Exception {
        File dockerfile = new File(compileHome + "/Dockerfile.tmp");
        try {
            textFile.write(dockerfile, dockerfileContent);
            String imageId = dockerClient.buildImageCmd()
                    .withDockerfile(dockerfile)
                    // repoTag 格式为 docker.reghao.cn/devops:12345678
                    .withTags(Set.of(repoTag))
                    .start()
                    .awaitImageId(300, TimeUnit.SECONDS);
        } catch (Exception e) {
            throw new Exception(ExceptionUtil.errorMsg(e));
        }
    }

    public void build(String repoTag, String compileHome) throws Exception {
        String dockerfilePath = compileHome + "/Dockerfile";
        dockerClient.buildImageCmd()
                .withDockerfile(new File(dockerfilePath))
                .withTags(Set.of(repoTag))
                .start()
                .awaitImageId(300, TimeUnit.SECONDS);
    }

    @Override
    public void push(String image) throws Exception {
        try {
            ResultCallback.Adapter<PushResponseItem> callback = new PushCallback();
            dockerClient.pushImageCmd(image)
                    //.exec(new ResultCallback.Adapter<>())
                    .exec(callback)
                    .awaitCompletion(300, TimeUnit.SECONDS);
            PushCallback pushCallback = (PushCallback) callback;
            int code = pushCallback.getCode();
            if (code != 0) {
                throw new Exception(pushCallback.getMsg());
            }
        } catch (InterruptedException e) {
            throw new Exception(ExceptionUtil.errorMsg(e));
        }
    }

    @Override
    public void pull(String image) throws Exception {
        try {
            dockerClient.pullImageCmd(image).exec(new PullImageResultCallback()).awaitCompletion();
        } catch (InterruptedException e) {
            throw new Exception(ExceptionUtil.errorMsg(e));
        }
        /*dockerClient.pullImageCmd(image).exec(new ResultCallback<PullResponseItem>() {
            public void onStart(Closeable closeable) {
                System.out.println("开始下载!");
            }

            public void onNext(PullResponseItem object) {
                // 实时显示出下载信息
                System.out.println(object.getStatus());
            }

            public void onError(Throwable throwable) {
                throwable.printStackTrace();
            }

            public void onComplete() {
                System.out.println("下载完毕!");
            }

            public void close() throws IOException {
                System.out.println("下载完毕...");
            }
        }).onComplete();*/
    }

    private String getContainerIdByName(String containerName) {
        List<Container> list = dockerClient.listContainersCmd()
                .withShowAll(true)
                .withNameFilter(List.of(containerName))
                .exec();

        for (Container container : list) {
            Set<String> set = new HashSet<>(Arrays.asList(container.getNames()));
            if (set.contains("/" + containerName)) {
                return container.getId();
            }
        }

        return null;
    }

    @Override
    public void stopAndDelete(String containerName) {
        String containerId = getContainerIdByName(containerName);
        if (containerId != null) {
            InspectContainerResponse containerInfo = dockerClient.inspectContainerCmd(containerId).exec();
            InspectContainerResponse.ContainerState state = containerInfo.getState();
            boolean isRunning = state.getRunning() != null ? state.getRunning() : false;
            if (isRunning) {
                dockerClient.stopContainerCmd(containerId).exec();
            }
            dockerClient.removeContainerCmd(containerId).exec();
        }
    }

    @Override
    public InspectContainerResponse createAndRun(String containerName, cn.reghao.bnt.common.docker.model.Config containerConfig) throws Exception {
        stopAndDelete(containerName);

        String image = containerConfig.getImage();
        CreateContainerCmd createContainerCmd = dockerClient.createContainerCmd(image)
                .withName(containerName);

        List<String> env = containerConfig.getEnv();
        if (env != null) {
            createContainerCmd.withEnv(env);
        }

        RestartPolicy restartPolicy = RestartPolicy.onFailureRestart(3);
        HostConfig hostConfig = HostConfig.newHostConfig()
                // --init 参数
                //.withInit(true)
                .withNetworkMode("host")
                .withRestartPolicy(restartPolicy);
        if (containerConfig.getVolumes() != null) {
            Map<String, String> map = containerConfig.getVolumes().getMap();
            List<Bind> list = new ArrayList<>();
            for (Map.Entry<String, String> entry : map.entrySet()) {
                String key = entry.getKey();
                String value = entry.getValue();
                Volume volume2 = new Volume(value);
                Bind bind = new Bind(key, volume2);
                list.add(bind);
            }
            hostConfig.withBinds(list);
        }

        if (containerConfig.getHostConfig().getInit() != null) {
            hostConfig.withInit(containerConfig.getHostConfig().getInit());
        }

        createContainerCmd.withHostConfig(hostConfig);
        CreateContainerResponse response = createContainerCmd.exec();
        String containerId = response.getId();
        dockerClient.startContainerCmd(containerId).exec();
        return dockerClient.inspectContainerCmd(containerId).exec();
    }

    public void runAndRm(cn.reghao.bnt.common.docker.model.Config containerConfig) throws Exception {
        String image = containerConfig.getImage();
        CreateContainerCmd createContainerCmd = dockerClient.createContainerCmd(image).withCmd("rm");

        List<String> env = containerConfig.getEnv();
        if (env != null) {
            createContainerCmd.withEnv(env);
        }

        HostConfig hostConfig = HostConfig.newHostConfig()
                .withNetworkMode("host")
                .withRestartPolicy(RestartPolicy.noRestart());
        if (containerConfig.getVolumes() != null) {
            Map<String, String> map = containerConfig.getVolumes().getMap();
            List<Bind> list = new ArrayList<>();
            for (Map.Entry<String, String> entry : map.entrySet()) {
                String hostPath = entry.getValue();
                String containerPath = entry.getKey();

                Volume volume2 = new Volume(containerPath);
                Bind bind = new Bind(hostPath, volume2);
                list.add(bind);
            }
            hostConfig.withBinds(list);
        }

        if (containerConfig.getCmd() != null) {
            createContainerCmd.withCmd(containerConfig.getCmd());
        }

        createContainerCmd.withHostConfig(hostConfig);
        CreateContainerResponse response = createContainerCmd.exec();
        String containerId = response.getId();
        dockerClient.startContainerCmd(containerId).exec();

        List<String> list = new ArrayList<>();
        LogContainerCmd logContainerCmd = dockerClient.logContainerCmd(containerId);
        logContainerCmd.withStdOut(true).withStdErr(true).withFollowStream(true);
        logContainerCmd.exec(new ResultCallback<Frame>() {
            @Override
            public void onStart(Closeable closeable) {
            }

            @Override
            public void onNext(Frame object) {
                if (object.getStreamType().equals(StreamType.STDERR)) {
                    list.add(object.toString());
                }
            }

            @Override
            public void onError(Throwable throwable) {
                log.info(throwable.toString());
            }

            @Override
            public void onComplete() {
            }

            @Override
            public void close() throws IOException {
            }
        });

        dockerClient.waitContainerCmd(containerId).exec(new ResultCallback.Adapter<>())
                .awaitCompletion(300, TimeUnit.SECONDS);

        InspectContainerResponse response1 = dockerClient.inspectContainerCmd(containerId).exec();
        InspectContainerResponse.ContainerState state = response1.getState();
        Long exitCode = state.getExitCodeLong();
        if (Boolean.TRUE.equals(state.getRunning())) {
            dockerClient.stopContainerCmd(containerId).exec();
            dockerClient.removeContainerCmd(containerId).exec();
            throw new Exception("docker build timeout after 300 seconds");
        } else if (exitCode != null && exitCode == 0) {
            dockerClient.removeContainerCmd(containerId).exec();
        } else {
            dockerClient.removeContainerCmd(containerId).exec();
            StringBuilder sb = new StringBuilder();
            list.forEach(line -> {
                sb.append(line).append(System.lineSeparator());
            });
            throw new Exception("docker build failed:\n" + sb.toString());
        }
    }

    @Override
    public InspectContainerResponse start(String containerName) {
        String containerId = getContainerIdByName(containerName);
        dockerClient.startContainerCmd(containerId).exec();
        return dockerClient.inspectContainerCmd(containerId).exec();
    }

    @Override
    public InspectContainerResponse stop(String containerName) {
        String containerId = getContainerIdByName(containerName);
        dockerClient.stopContainerCmd(containerId).exec();
        return dockerClient.inspectContainerCmd(containerId).exec();
    }

    @Override
    public InspectContainerResponse restart(String containerName) {
        String containerId = getContainerIdByName(containerName);
        dockerClient.restartContainerCmd(containerId).exec();
        return dockerClient.inspectContainerCmd(containerId).exec();
    }

    @Override
    public InspectContainerResponse inspectContainer(String containerName) {
        String containerId = getContainerIdByName(containerName);
        return dockerClient.inspectContainerCmd(containerId).exec();
    }

    @Override
    public List<Image> images() {
        List<Image> images = dockerClient.listImagesCmd().exec();
        return images;
        /*return images.stream().map(image -> {
            long created = image.getCreated();
            String[] repoTags = image.getRepoTags();
            if (repoTags.length == 0) {
                return null;
            }

            String imageId = image.getId();
            String[] repoTag = repoTags[0].split(":");
            String repo = repoTag[0];
            String tag = repoTag[1];
            LocalDateTime created1 = DateTimeConverter.localDateTime(created*1000);
            return new ImageInfo(repo, tag, imageId, created1);
        }).filter(Objects::nonNull).collect(Collectors.toList());*/
    }

    @Override
    public void imageRm(String imageId) {
        dockerClient.removeImageCmd(imageId).exec();
    }

    @Override
    public void rm(String containerId) {
        dockerClient.removeContainerCmd(containerId).exec();
    }

    @Override
    public List<InspectContainerResponse> psAll() {
        List<Container> list = dockerClient.listContainersCmd()
                .withShowAll(true)
                .exec();
        return list.stream().map(container -> {
            String containerId = container.getId();
            return dockerClient.inspectContainerCmd(containerId).exec();
        }).collect(Collectors.toList());
    }

    @Override
    public InspectContainerResponse ps(String appId) {
        List<Container> list = dockerClient.listContainersCmd()
                .withNameFilter(List.of(appId))
                .exec();

        if (list.isEmpty()) {
            return null;
        }

        Container container = list.get(0);
        String containerId = container.getId();
        return dockerClient.inspectContainerCmd(containerId).exec();
    }

    static class PushCallback extends ResultCallback.Adapter<PushResponseItem> {
        private int code;
        private String msg;

        public PushCallback() {
            this.code = 0;
        }

        public int getCode() {
            return code;
        }

        public String getMsg() {
            return msg;
        }

        @Override
        public void onNext(PushResponseItem object) {
            PushResponseItem.ErrorDetail errorDetail = object.getErrorDetail();
            if (errorDetail != null) {
                //log.info("error: {} {}", errorDetail.getCode(), errorDetail.getMessage());
                code = 1;
                msg = errorDetail.getMessage();
            } else {
                //log.info("info: {} {} {}", object.getStatus(), object.getId(), object.getProgressDetail());
            }
        }

        @Override
        public void onComplete() {
            super.onComplete();
        }

        @Override
        public void onError(Throwable throwable) {
            throwable.printStackTrace();
        }
    }
}
