package com.lanyuanxiaoyao.service.cli.runner;

import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.template.Template;
import cn.hutool.extra.template.TemplateConfig;
import cn.hutool.extra.template.TemplateEngine;
import cn.hutool.extra.template.TemplateUtil;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.lanyuanxiaoyao.service.cli.core.HostInfo;
import com.lanyuanxiaoyao.service.cli.core.ServiceInfo;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * 启动类
 *
 * @author lanyuanxiaoyao
 * @date 2023-05-17
 */
@SpringBootApplication
public class RunnerApplication implements ApplicationRunner {
    private static final Logger logger = LoggerFactory.getLogger(RunnerApplication.class);

    private final DeployInformationProperties deployInformationProperties;
    private final ObjectMapper mapper;

    public RunnerApplication(DeployInformationProperties deployInformationProperties) {
        this.deployInformationProperties = deployInformationProperties;
        mapper = new ObjectMapper();
    }

    public static void main(String[] args) {
        SpringApplication.run(RunnerApplication.class, args);
    }

    private Map<String, Object> escape(Map<String, Object> map) {
        // https://github.com/spring-projects/spring-framework/issues/9628
        // spring boot 的配置文件没有办法转义 $，使用 $\{ -> ${ 来绕过一下
        return map.entrySet()
                .stream()
                .peek(entry -> {
                    if (entry.getValue() instanceof String) {
                        String value = (String) entry.getValue();
                        entry.setValue(value.replace("$\\{", "${"));
                    }
                })
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
    }

    private List<String> selectHosts(ServiceInfo serviceInfo) {
        return serviceInfo.getReplicas() == 0
                ? deployInformationProperties.getHosts()
                .stream()
                .map(HostInfo::getIp)
                .sorted(Comparator.naturalOrder())
                .collect(Collectors.toList())
                : ListUtil.sort(
                RandomUtil.randomEleList(
                        deployInformationProperties.getHosts()
                                .stream()
                                .map(HostInfo::getIp)
                                .collect(Collectors.toList()
                                ), serviceInfo.getReplicas()
                ), Comparator.naturalOrder());
    }

    @Override
    public void run(ApplicationArguments args) throws IOException {
        logger.info("Loading runtime info: {}", mapper.writeValueAsString(deployInformationProperties.getRuntime()));
        Path planPath = Paths.get("deploy.plan");
        Map<String, List<String>> deployPlans = new HashMap<>();
        if (Files.exists(planPath) && !deployInformationProperties.getShuffler()) {
            deployPlans = mapper.readValue(new String(Files.readAllBytes(planPath)), new TypeReference<Map<String, List<String>>>() {
            });
        }

        Path root = Paths.get("");
        Files.createDirectories(root);
        TemplateEngine engine = TemplateUtil.createEngine(new TemplateConfig("template", TemplateConfig.ResourceMode.CLASSPATH));
        Template deployTemplate = engine.getTemplate("deploy.ftl");
        for (ServiceInfo serviceInfo : deployInformationProperties.getServices()) {
            logger.info("Generate script for {}", serviceInfo.getName());
            serviceInfo.setEnvironments(escape(serviceInfo.getEnvironments()));
            serviceInfo.setArguments(escape(serviceInfo.getArguments()));
            List<String> selectedHosts;
            if (deployPlans.containsKey(serviceInfo.getName())) {
                selectedHosts = deployPlans.get(serviceInfo.getName());
                if (ObjectUtil.notEqual(selectedHosts.size(), serviceInfo.getReplicas())) {
                    selectedHosts = selectHosts(serviceInfo);
                    deployPlans.put(serviceInfo.getName(), selectedHosts);
                }
            } else {
                selectedHosts = selectHosts(serviceInfo);
                deployPlans.put(serviceInfo.getName(), selectedHosts);
            }
            String deployScript = deployTemplate.render(MapUtil.builder()
                    .put("hosts", deployInformationProperties.getHosts()
                            .stream()
                            .map(HostInfo::getIp)
                            .sorted(Comparator.naturalOrder())
                            .collect(Collectors.toList()))
                    .put("selectedHosts", selectedHosts)
                    .put("runtime", deployInformationProperties.getRuntime())
                    .put("info", serviceInfo)
                    .put("arguments", serviceInfo.getArguments())
                    .put("environments", serviceInfo.getEnvironments())
                    .build());
            Path deployScriptFile = Paths.get(
                    root.toString(),
                    StrUtil.format("deploy-{}.sh", serviceInfo.getName())
            );
            Files.deleteIfExists(deployScriptFile);
            Files.write(deployScriptFile, deployScript.getBytes());

            Template stopTemplate = engine.getTemplate("stop.ftl");
            String stopScript = stopTemplate.render(MapUtil.builder()
                    .put("hosts", deployInformationProperties.getHosts()
                            .stream()
                            .map(HostInfo::getIp)
                            .sorted(Comparator.naturalOrder())
                            .collect(Collectors.toList()))
                    .put("runtime", deployInformationProperties.getRuntime())
                    .put("info", serviceInfo)
                    .put("arguments", serviceInfo.getArguments())
                    .put("environments", serviceInfo.getEnvironments())
                    .build());
            Path stopScriptFile = Paths.get(
                    root.toString(),
                    StrUtil.format("stop-{}.sh", serviceInfo.getName())
            );
            Files.deleteIfExists(stopScriptFile);
            Files.write(stopScriptFile, stopScript.getBytes());

            Template logTemplate = engine.getTemplate("log.ftl");
            String logScript = logTemplate.render(MapUtil.builder()
                    .put("hosts", deployInformationProperties.getHosts()
                            .stream()
                            .map(HostInfo::getIp)
                            .sorted(Comparator.naturalOrder())
                            .collect(Collectors.toList()))
                    .put("selectedHosts", selectedHosts)
                    .put("runtime", deployInformationProperties.getRuntime())
                    .put("info", serviceInfo)
                    .put("arguments", serviceInfo.getArguments())
                    .put("environments", serviceInfo.getEnvironments())
                    .build());
            Path logScriptFile = Paths.get(
                    root.toString(),
                    StrUtil.format("log-{}.sh", serviceInfo.getName())
            );
            Files.deleteIfExists(logScriptFile);
            Files.write(logScriptFile, logScript.getBytes());
        }

        // language=JavaScript
        String stopScript =
                "original_command=\"$0\"\n" +
                "application_name=\"$1\"\n" +
                "\n" +
                "function get_pid() {\n" +
                "  ID=$(ps -ef | grep \"$application_name\" | grep -v grep | grep -v $original_command | awk '{ print $2 }')\n" +
                "  if [[ -z \"$ID\" ]]; then\n" +
                "    ID=$(ps -C java -f --width 1000 | grep \"$application_name\" | grep -v grep | grep -v $original_command | awk '{print $2}')\n" +
                "    if [[ -z \"$ID\" ]]; then\n" +
                "      ID=$(ps aux | grep \"$application_name\" | grep -v grep | grep -v $original_command | awk '{print $2}')\n" +
                "    fi\n" +
                "  fi\n" +
                "  echo ${ID}\n" +
                "}\n" +
                "\n" +
                "pid=$(get_pid)\n" +
                "\n" +
                "if [[ -z \"$pid\" ]]; then\n" +
                "  echo \"Application is already stopped.\"\n" +
                "  exit 0\n" +
                "fi\n" +
                "\n" +
                "echo \"Stopping $pid...\"\n" +
                "kill ${pid}\n" +
                "\n" +
                "LOOPS=0\n" +
                "while (true); do\n" +
                "  cpid=$(get_pid)\n" +
                "  if [[ -z \"$cpid\" ]]; then\n" +
                "    echo \"Oook! cost:$LOOPS\"\n" +
                "    break\n" +
                "  fi\n" +
                "  LOOPS=$(($LOOPS+1))\n" +
                "  if [[ $(($LOOPS%15)) -eq 0 ]]; then\n" +
                "    echo \"Wait timeout, try to force kill ${pid}...\"\n" +
                "    kill -9 ${pid}\n" +
                "  fi\n" +
                "  sleep 1s\n" +
                "done";
        Files.write(Paths.get(root.toString(), "stop.sh"), stopScript.getBytes());

        Files.deleteIfExists(planPath);
        Files.write(planPath, mapper.writeValueAsString(deployPlans).getBytes());
    }
}
